State.js outfits JavaScript objects with a specific behavior model, powered by a hierarchical and prototypal graph of states.
var state = require('state');
var person = {};
state( person, {
Formal: {
greet: function () { return "How do you do?"; }
},
Casual: {
greet: function () { return "Hi!"; }
}
});
person.hasOwnProperty('state'); // >>> true
person.state('-> Formal');
person.state(); // >>> State 'Formal'
person.greet(); // >>> "How do you do?"
person.state('-> Casual');
person.state(); // >>> State 'Casual'
person.greet(); // >>> "Hi!"
state = require 'state'
person = {}
state person,
Formal:
greet: -> "How do you do?"
Casual:
greet: -> "Hi!"
person.hasOwnProperty 'state' # >>> true
person.state '-> Formal'
person.state() # >>> State 'Formal'
person.greet() # >>> "How do you do?"
person.state '-> Casual'
person.state() # >>> State 'Casual'
person.greet() # >>> "Hi!"
State instances define behavior for an owner object. They have a distinct object model, which allows them both to be hierarchically nested, and to inherit from States held by prototypes of their owner.
function Person () {}
state( Person.prototype, {
Formal: {
greet: function () { return "How do you do?"; },
Highbrow: {
greet: function () { return "Enchanté."; }
}
},
Casual: {
greet: function () { return "Hi!"; }
}
});
var person = new Person;
var friend = new Person;
person.hasOwnProperty('state'); // >>> false
person.state('-> Highbrow');
friend.state('-> Casual');
person.greet(); // >>> "Enchanté."
friend.greet(); // >>> "Hi!"
class Person
state @::
Formal:
greet: -> "How do you do?"
Highbrow:
greet: -> "Enchanté."
Casual:
greet: -> "Hi!"
person = new Person
friend = new Person
person.hasOwnProperty 'state' # >>> false
person.state '-> Highbrow'
friend.state '-> Casual'
person.greet() # >>> "Enchanté."
friend.greet() # >>> "Hi!"
Attributes concisely empower or constrain certain aspects of a State, such as mutability, abstraction, and destination.
function Person () {}
state( Person.prototype, 'abstract', {
Formal: state( 'default initial', {
greet: function () { return "How do you do?"; }
}),
Casual: state( 'final', {
greet: function () { return "Hi!"; }
})
});
var person = new Person;
person.greet(); // >>> "How do you do?"
person.state('->');
person.state(); // >>> State 'Formal'
person.greet(); // >>> "How do you do?"
person.state('-> Casual')
person.greet(); // >>> "Hi!"
person.state('-> Formal')
person.state(); // >>> State 'Casual'
class Person
state @::, 'abstract'
Formal: state 'default initial'
greet: -> "How do you do?"
Casual: state 'final'
greet: -> "Hi!"
person = new Person
person.greet() # >>> "How do you do?"
person.state '->'
person.state() # >>> State 'Formal'
person.greet() # >>> "How do you do?"
person.state '-> Casual'
person.greet() # >>> "Hi!"
person.state '-> Formal'
person.state() # >>> State 'Casual'
State methods express or override behavior of the owner object. Methods are contextually bound to the State in which they are defined, and may optionally be decorated with additional bindings that describe the method’s complete state–lexical environment.
function inherit ( child, parent ) {
child.prototype = Object.create( parent.prototype );
return child.prototype.constructor = child;
}
inherit( Sophisticate, Person );
function Sophisticate ( name ) {
this.name = name;
}
state( Sophisticate.prototype, 'abstract', {
Formal: state( 'default initial', {
Cordial: {
greet: state.method( function ( person ) {
var greeting = superstate.call('greet');
var name = person && person.name;
return name ?
"Hello " + name + ". " + greeting :
greeting;
})
}
}),
Casual: {
greet: state.method( function ( person ) {
var name = person && person.name;
if ( name === 'Lane' ) return "How’s it hanging?";
if ( name ) return "Hi " + name + ".";
return protostate.call('greet');
})
}
});
var sterling = new Sophisticate('Roger');
var cooper = new Sophisticate('Bert');
var draper = new Sophisticate('Don');
var pryce = new Sophisticate('Lane');
draper.state(); // >>> State 'Formal'
draper.greet( new Person ); // >>> "How do you do?"
draper.state('-> Cordial');
draper.greet( new Person ); // >>> "How do you do?"
draper.greet( cooper ); // >>> "Hello Bert. How do you do?"
draper.greet( sterling ); // >>> "Hello Roger. How do you do?"
draper.state('-> Casual');
draper.greet( new Person ); // >>> "Hi!"
draper.greet( sterling ); // >>> "Hi Roger."
draper.greet( pryce ); // >>> "How’s it hanging?"
class Sophisticate extends Person
constructor: (@name) ->
state @::, 'abstract'
Formal: state 'default initial'
Cordial:
greet: state.method (person) ->
greeting = superstate.call 'greet'
if name = person?.name then "Hello #{name}. #{greeting}"
else greeting
Casual:
greet: state.method (person) ->
name = person?.name
return "How’s it hanging?" if name is 'Lane'
return "Hi #{name}." if name
protostate.call 'greet'
[ sterling, cooper, draper, pryce ] =
new Sophisticate n for n in ['Roger', 'Bert', 'Don', 'Lane']
draper.state() # >>> State 'Formal'
draper.greet new Person # >>> "How do you do?"
draper.state '-> Cordial'
draper.greet new Person # >>> "How do you do?"
draper.greet cooper # >>> "Hello Bert. How do you do?"
draper.greet sterling # >>> "Hello Roger. How do you do?"
draper.state '-> Casual'
draper.greet new Person # >>> "Hi!"
draper.greet sterling # >>> "Hi Roger."
draper.greet pryce # >>> "How’s it hanging?"
Events relate the progress of a transition, and signal the construction, destruction, or mutation of a State. They 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
}
},
// Use the root state’s `construct` event to programmatically
// set up all of the states to log their transitional events.
construct: function () {
var states, events, s, e, i, ls, j, le;
function bindEventToState ( e, s ) {
s.on( e, function () {
console.log( e + " " + this.name() );
});
}
states = [ this ].concat( this.substates( true ) );
events = ['depart', 'exit', 'enter', 'arrive'];
for ( i = 0, ls = states.length; i < ls; i++ ) {
s = states[i];
for ( j = 0, le = events.length; j < le; j++ ) {
e = events[j];
bindEventToState( e, s );
}
}
}
});
var m = new Mover;
m.state('-> Alert');
// log <<< "depart Idle"
// log <<< "exit Idle"
// log <<< "enter Alert"
// log <<< "arrive Alert"
m.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
# Use the root state’s `construct` event to programmatically
# set up all of the states to log their transitional events.
construct: ->
events = ['depart', 'exit', 'enter', 'arrive']
for s in [this].concat @substates true
for e in events
do ( s, e ) -> s.on e, -> console.log "#{e} #{@name()}"
m = new Mover
m.state '-> Alert'
# log <<< "depart Idle"
# log <<< "exit Idle"
# log <<< "enter Alert"
# log <<< "arrive Alert"
m.state '-> Sprinting'
# log <<< "depart Alert"
# log <<< "exit Alert"
# log <<< "exit Stationary"
# log <<< "enter Moving"
# log <<< "enter Running"
# log <<< "enter Sprinting"
# log <<< "arrive Sprinting"
State instances are immutable by default, but may optionally be configured as mutable, allowing modular pieces of behavior to be implemented dynamically into a state.
var theRomansDo = {
Formal: {
greet: function () { return "Quid agis?"; }
},
Casual: {
greet: function () { return "Salve!"; }
}
};
function doAs ( behavior ) {
return function () {
return this.mutate( behavior );
};
}
inherit( Traveler, Person );
function Traveler () {}
state( Traveler.prototype, 'mutable abstract', {
goTo: function ( place ) {
this.emit( 'in' + place );
},
events: {
inRome: doAs( theRomansDo )
},
Formal: state('default')
});
var traveler = new Traveler;
traveler.greet(); // >>> "How do you do?"
traveler.goTo('Rome');
traveler.greet(); // >>> "Quid agis?"
traveler.state('-> Casual');
traveler.greet(); // >>> "Salve!"
theRomansDo =
Formal:
greet: -> "Quid agis?"
Casual:
greet: -> "Salve!"
doAs = (behavior) -> -> @mutate behavior
class Traveler extends Person
state @::, 'mutable abstract'
goTo: (place) -> @emit "in#{place}"
events:
inRome: doAs theRomansDo
Formal: state 'default'
traveler = new Traveler
traveler.greet() # >>> "How do you do?"
traveler.goTo 'Rome'
traveler.greet() # >>> "Quid agis?"
traveler.state '-> Casual'
traveler.greet() # >>> "Salve!"
Transitions between states can be conditionally guarded, defined generically across multiple states, and either synchronous or asynchronous.
function ContainerView () {}
state( ContainerView.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 ContainerView
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()