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
state
function 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 ofState
s 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
state
method, which closes over the owner’s new state tree and serves as the accessor to itsState
s; 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. State
s 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 State
s.
-
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
State
are 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 State
s.
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
behavior
into a receivingState
.
function doAs ( behavior ) {
return state.bind( function () {
this.mutate( behavior );
});
}
doAs = ( behavior ) -> state.bind -> @mutate behavior
A
traveler
assimilates 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 State
s.
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()