State.js is a JavaScript library for embedding composable, heritable first-class states into arbitrary owner objects.
var state = require('state'); // >>> [Function]state = require 'state' # >>> [Function]The exported
statefunction will be used here in two ways: (1) to define structures called state expressions, which describe behavior in terms of methods, data, events, etc.; and (2) to implement a composite state expression on anowner, producing a tree ofStates that belong to the owner and describe its potential exhibited behaviors.
var owner = {};
state( owner, {
A: state({
aMethod: function () {};
}),
B: state({
aMethod: function () {};
})
});owner = {}
state owner,
A: state
aMethod: ->
B: state
aMethod: ->The implementing form has two effects: (1) the owner is given a new
statemethod, which closes over the owner’s new state tree and serves as the accessor to itsStates; and (2) for any method defined at least once in the state tree, a corresponding dispatcher method is created and added to the owner.
owner.state; // >>> [Function]
owner.aMethod; // >>> [Function]owner.state # >>> [Function]
owner.aMethod # >>> [Function]States and currency
State objects are modules of behavior that may be exhibited interchangeably by their owner object. States may define method overrides, arbitrary data, event listeners, guards, substates, and transition expressions.
var owner = {};
state( owner, {
A: state({
aMethod: function () { return "alpha"; }
}),
B: state({
aMethod: function () { return "beta"; }
})
});
var root = owner.state(''); // >>> RootState
var stateA = owner.state('A'); // >>> State 'A'
var stateB = owner.state('B'); // >>> State 'B'state owner = {},
A: state
aMethod: -> "alpha"
B: state
aMethod: -> "beta"
root = owner.state '' # >>> RootState
stateA = owner.state 'A' # >>> State 'A'
stateB = owner.state 'B' # >>> State 'B'
Exactly one State is designated as the owner’s current state, whose own and inherited behavior is exhibited by the owner.
owner.state(); // >>> RootState
owner.state() === root; // >>> true
owner.state() === root.current(); // >>> true (invariant)owner.state() # >>> RootState
owner.state() is root # >>> true
owner.state() is root.current() # >>> true (invariant)The owner may alter its behavior by undergoing transitions, which carry the current state reference, or currency, to a different State.
owner.state('-> A');
owner.state() === stateA; // >>> true
owner.aMethod(); // >>> "alpha"
owner.state('-> B');
owner.state() === stateB; // >>> true
owner.aMethod(); // >>> "beta"
owner.state('->');
owner.state() === root; // >>> true
owner.aMethod(); // >>> undefined
owner.state().owner === owner; // >>> true (invariant)
owner.state().root === root; // >>> true (invariant)owner.state '-> A'
owner.state() is stateA # >>> true
owner.aMethod() # >>> "alpha"
owner.state '-> B'
owner.state() is stateB # >>> true
owner.aMethod() # >>> "beta"
owner.state '->'
owner.state() is root # >>> true
owner.aMethod() # >>> undefined
owner.state().owner is owner # >>> true (invariant)
owner.state().root is root # >>> true (invariant)Object model
State objects may inherit and be composed from other States.
-
Hierarchical single-inheritance is provided by the superstate–substate relation, which defines a state tree rooted from the owner’s unique root state.
-
Compositional multiple-inheritance is provided simultaneously by parastates. The parastate and superstate ancestors of a
Stateare C3–linearized into a monotonic, unambiguous resolution order. -
Indirect prototypal inheritance is also defined by the protostate–epistate relation, an implication of the owner’s prototype chain.
var p = {};
state( p, {
A: state,
B: state({
BA: state,
BB: state
})
});
var o = Object.create( p );
state( o, {
A: state({
AA: state.extend('X, Y')
}),
X: state,
Y: state
});class Class
state p = @::,
A: state
B: state
BA: state
BB: state
state o = new Class,
A: state
AA: state.extend 'X, Y'
X: state
Y: state
Attributes
Attributes are a set of keyword strings that may precede the body of a state expression. These concisely empower or constrain certain aspects of a State, such as abstraction, destination, and mutability.
function Developer () {}
state( Developer.prototype, 'abstract', {
develop: function () { this.state('-> Seasoned'); },
Juvenile: state( 'initial', {
greet: function () { return "sup"; }
}),
Seasoned: state( 'final', {
greet: function () { return "Hello."; }
})
});
var dev = new Developer;
dev.state(); // >>> State 'Juvenile'
dev.greet(); // >>> "sup"
dev.develop();
dev.state(); // >>> State 'Seasoned'
dev.greet(); // >>> "Hello."
dev.state('-> Juvenile'); // (No effect)
dev.state(); // >>> State 'Seasoned'
dev.greet(); // >>> "Hello."class Developer
state @::, 'abstract',
develop: -> @state '-> Seasoned'
Juvenile: state 'initial',
greet: -> "sup"
Seasoned: state 'final',
greet: -> "Hello."
dev = new Developer
dev.state() # >>> State 'Juvenile'
dev.greet() # >>> "sup"
do dev.develop
dev.state() # >>> State 'Seasoned'
dev.greet() # >>> "Hello."
dev.state '-> Juvenile' # (No effect)
dev.state() # >>> State 'Seasoned'
dev.greet() # >>> "Hello."Methods
State methods express or override behavior of the owner. Method calls received by the owner are dispatched to its current state’s own or inherited implementation of the corresponding method.
By default, state methods are invoked in the context of the owner, just like normal methods. To gain insight into its place in the owner’s state tree, a method definition can instead be contextually bound to the State for which the method acts. (This will be the State in which the method is defined, unless the method is inherited from a protostate, in which case the context will be the inheriting epistate.)
function Avenger ( name ) {
this.name = name;
}
Avenger.prototype.greet = function () { return "Hello."; };
state( Avenger.prototype, 'abstract', {
Terse: state('default'),
Verbose: state({
greet: state.bind( function () {
return this.superstate.call('greet') +
" My name is " + this.owner.name + "...";
})
})
});
var inigo = new Avenger('Inigo');
inigo.state(); // >>> State 'Terse'
inigo.greet(); // >>> "Hello."
inigo.state('-> Verbose'); // >>> State 'Verbose'
inigo.greet(); // >>> "Hello. My name is Inigo..."class Avenger
constructor: ( @name ) ->
greet: -> "Hello."
state @::, 'abstract',
Terse: state 'default'
Verbose: state
greet: state.bind ->
"#{ @superstate.call 'greet' } My name is #{ @owner.name }..."
inigo = new Avenger 'Inigo'
inigo.state() # >>> State 'Terse'
inigo.greet() # >>> "Hello."
inigo.state '-> Verbose' # >>> State 'Verbose'
inigo.greet() # >>> "Hello. My name is Inigo..."A state method can also be wrapped in a decorator that fixes the method with bindings to the precise State where it is defined (invariant across the protostate–epistate relation).
Events
State provides built-in events that relate the progress of a transition as it traverses the state hierarchy, along with events that signal the construction, destruction, or mutation of a State. Events can also be emitted for any custom event type.
function Mover () {}
state( Mover.prototype, {
Stationary: {
Idle: state('initial'),
Alert: state
},
Moving: {
Walking: state,
Running: {
Sprinting: state
}
}
});
// Set up each state to log its transitional events.
( function () {
var states, eventNames, i, j;
function bindEventToState ( e, s ) {
function log () { console.log( e + " " + s.name ); }
s.on( e, log );
}
states = Mover.prototype.state('**');
eventNames = ['depart', 'exit', 'enter', 'arrive'];
for ( i = 0; i < states.length; i++ ) {
for ( j = 0; j < eventNames.length; j++ ) {
bindEventToState( eventNames[j], states[i] );
}
}
}() );
var mover = new Mover;
mover.state('-> Alert');
// log <<< "depart Idle"
// log <<< "exit Idle"
// log <<< "enter Alert"
// log <<< "arrive Alert"
mover.state('-> Sprinting');
// log <<< "depart Alert"
// log <<< "exit Alert"
// log <<< "exit Stationary"
// log <<< "enter Moving"
// log <<< "enter Running"
// log <<< "enter Sprinting"
// log <<< "arrive Sprinting"class Mover
state @::,
Stationary:
Idle: state 'initial'
Alert: state
Moving:
Walking: state
Running:
Sprinting: state
# Set up each state to log its transitional events.
eventNames = ['depart', 'exit', 'enter', 'arrive']
for substate in Mover::state '**'
for eventName in eventNames
do ( substate, eventName ) -> substate.on eventName, ->
console.log "#{ eventName } #{ substate.name }"
mover = new Mover
mover.state '-> Alert'
# log <<< "depart Idle"
# log <<< "exit Idle"
# log <<< "enter Alert"
# log <<< "arrive Alert"
mover.state '-> Sprinting'
# log <<< "depart Alert"
# log <<< "exit Alert"
# log <<< "exit Stationary"
# log <<< "enter Moving"
# log <<< "enter Running"
# log <<< "enter Sprinting"
# log <<< "arrive Sprinting"Mutability
State instances are nominally immutable by default. This restriction forces any changes in the owner’s State-based behavior to be expressed in terms of transitions between States.
Alternatively, a State may be explicitly expressed as mutable, such that modular pieces of behavior may be inserted and implemented dynamically into a live State, thereby allowing changes in behavior to be exhibited via mutations as well.
function Actor () {}
state( Actor.prototype, 'abstract', {
Casual: state({
greet: function () { return "Hi!"; }
}),
Formal: state( 'default', {
greet: function () { return "How do you do?"; }
})
});class Actor
state @::, 'abstract',
Casual: state
greet: -> "Hi!"
Formal: state 'default',
greet: -> "How do you do?"A free bit of behavior can be expressed with just a plain object.
var theRomansDo = {
Casual: {
greet: function () { return "Salve!"; }
},
Formal: {
greet: function () { return "Quid agis?"; }
}
};theRomansDo =
Casual:
greet: -> "Salve!"
Formal:
greet: -> "Quid agis?"This factory produces a boxed function that will instill an enclosed
behaviorinto a receivingState.
function doAs ( behavior ) {
return state.bind( function () {
this.mutate( behavior );
});
}doAs = ( behavior ) -> state.bind -> @mutate behaviorA
travelerassimilates itself by overwriting previously defined behavior with the appropriately chosen new behavior.
function Traveler () {}
Traveler.prototype = Object.create( Actor );
Traveler.prototype.constructor = Traveler;
state( Traveler.prototype, 'mutable', {
travelTo: state.bind( function ( place ) {
this.emit( 'in' + place );
}),
events: {
inRome: doAs( theRomansDo )
}
});
var traveler = new Traveler;
traveler.greet(); // >>> "How do you do?"
traveler.travelTo('Rome');
traveler.greet(); // >>> "Quid agis?"
traveler.state('-> Casual'); // >>> State 'Casual'
traveler.greet(); // >>> "Salve!"class Traveler extends Actor
state @::, 'mutable',
travelTo: state.bind ( place ) -> @emit "in#{ place }"
events:
inRome: doAs theRomansDo
traveler = new Traveler
traveler.greet() # >>> "How do you do?"
traveler.travelTo 'Rome'
traveler.greet() # >>> "Quid agis?"
traveler.state '-> Casual' # >>> State 'Casual'
traveler.greet() # >>> "Salve!"Transitions
A Transition instance is an ephemeral type of State that is automatically created and occupied as an owner’s current state while it is in the process of moving between States.
Transitions are defined by transition expressions, which are an optional component of a state expression. A transition may be defined generically across multiple states, is defined as either synchronous or asynchronous, and can be conditionally guarded.
function Container () {}
Container.prototype = Object.create( View );
Container.prototype.constructor = Container;
state( Container.prototype, 'abstract', {
Grid: state('default initial'),
List: state,
transitions: {
GridToList: {
origin: 'Grid', target: 'List',
action: function () {
// Rearrange subviews into a vertical column
// Change states of the subviews, if applicable
// Load model data as appropriate for this state
this.end();
}
},
ListToGrid: {
origin: 'List', target: 'Grid',
action: function () {
// ...
this.end();
}
}
}
});class Container extends View
state @::, 'abstract',
Grid: state 'default initial'
List: state
transitions:
GridToList:
origin: 'Grid', target: 'List'
action: ( args... ) ->
# Rearrange subviews into a vertical column
# Change states of the subviews, if applicable
# Load model data as appropriate for this state
@end()
ListToGrid:
origin: 'List', target: 'Grid'
action: ( args... ) ->
# ...
@end()