JS.State
JS.State is an implementation of the State pattern in JavaScript.
Simply put, the State pattern is a way of making the behaviour of an object (i.e. what its
methods do) dependent on its state – see the linked Wikipedia article for a good example.
JS.State does not implement a finite state machine. That would require enforcing rules
about which states can transition to which other states, which events trigger transitions
and what occurs during said transitions. The State pattern simply says that you can say
an object is in a certain state, and some of its behaviour changes accordingly. The
responsibility of changing an object’s state is left entirely up to the developer.
Using the JS.State pattern
To borrow from the Wikipedia article on the subject, let’s imagine you’re building a drawing application. I’m just going to get the methods to return strings to indicate what’s going on. First, let’s define some state objects. Each one represents the behaviour of a set of methods in a given state, and each state should implement the same methods.
var Tools = {
Pen: {
mouseDown: function() {
return 'Starting to draw';
},
mouseUp: function() {
return 'Finished drawing';
}
},
Selection: {
mouseDown: function() {
return 'Making selection';
},
mouseUp: function() {
return 'Completing selection';
}
}
};
Now let’s define a class that uses the states. It must not define mouseUp() or
mouseDown() itself – this is left up to JS.State. It should define an initial
state for itself when initialized.
var DrawingController = new JS.Class({
include: JS.State,
initialize: function() {
this.setState(Tools.Pen);
}
});
JS.State adds two methods to the class: setState() and inState(). Calling setState()
makes sure that the instance has all the methods defined in the given state, and sets the
state of the object. inState() takes one or more state objects, and returns true iff the
object is in any one of them.
var d = new DrawingController();
d.inState(Tools.Pen) // -> true
d.inState(Tools.Selection) // -> false
d.inState(Tools.Selection, Tools.Pen) // -> true
As setState() has added the required methods for us, we can call state-dependent methods
on the object, change its state, and see what happens.
d.mouseDown();
// -> "Starting to draw"
d.mouseUp();
// -> "Finished drawing"
d.setState(Tools.Selection);
d.mouseDown();
// -> "Making selection"
Remember, JS.State does not enforce state change rules so it’s up to you to make sure
your code makes sense. It does however make it easy to manage the behaviour of a complex
object without loads of if and switch statements, and to add new behaviours without
modifying an object’s code directly.
State definition shortcuts
include-ing JS.State in a class allows you to use a shorthand in your class definition
for adding all the class’ states. Including states in the class itself also allows you to
refer to them by name rather than as objects, so we could rewrite the above as:
var DrawingController = new JS.Class({
include: JS.State,
initialize: function() {
this.setState('Pen');
},
states: {
Pen: {
mouseDown: function() {
return 'Starting to draw';
},
mouseUp: function() {
return 'Finished drawing';
}
},
Selection: {
mouseDown: function() {
return 'Making selection';
},
mouseUp: function() {
return 'Completing selection';
}
}
}
});
var d = new DrawingController();
d.inState('Pen') // -> true
d.inState('Selection') // -> false
d.inState('Selection', 'Pen') // -> true
d.mouseDown();
// -> "Starting to draw"
d.mouseUp();
// -> "Finished drawing"
d.setState('Selection');
d.mouseDown();
// -> "Making selection"
Behind the scenes, JS.State makes sure that all the states implement the same methods.
If one state has a missing method, a method is added to it that simply returns the object.
For example:
var Twiddle = new JS.Class({
include: JS.State,
initialize: function() {
this.name = 'Twiddle';
this.setState('Incomplete');
},
states: {
Complete: {
getName: function() {
return this.name;
}
},
Incomplete: {
}
}
});
var twid = new Twiddle();
twid.getName() === twid // -> true
twid.setState('Complete');
twid.getName() // -> "Twiddler"
Notice that inside Complete.getName, the this keyword refers to the object calling
the method, just as for regular ‘stateless’ methods.
State inheritance
JS.State also supports inheritance using the following set of rules: Given a class
Controller with states Pen and Selection, and a subclass ChildController,
ChildControllermust have at least all the statesPenandSelection, and may have some more of its own.- All the states in any class must implement the same methods.
- If a state method (e.g.
Pen.mouseDown()) exists in bothControllerandChildController, the method inChildControllercan usethis.callSuper()to callPen.mouseDown()in the parent class.
These rules are enforced by JS.State so that when you write a subclass, you only need
to specify the ways in which it differs from its parent. Let’s take an example implementation
of ChildController.
var ChildController = new JS.Class(DrawingController, {
states: {
Selection: {
mouseDown: function() {
return this.callSuper().toUpperCase();
}
},
Eraser: {
mouseMove: function() {
return 'Removing stuff';
}
}
}
});
So now ChildController will have three states, Pen, Selection and Eraser, all
of which have three methods, mouseUp, mouseDown and mouseMove. mouseMove does
nothing in states Pen and Selection, and likewise for mouseUp and mouseDown in
the Eraser state. The Selection.mouseDown method calls the super method from the
DrawingController class during its execution. Let’s test out our new class:
c = new ChildController();
c.inState('Pen') // -> true
c.mouseDown() // -> "Starting to draw"
c.mouseMove() === c // -> true
c.setState('Selection');
c.mouseDown() // -> "MAKING SELECTION"
c.setState('Eraser');
c.mouseUp() === c // -> true
c.mouseMove() // -> "Removing stuff"
Note that you are allowed to implement mouseMove in the Selection state if you
so wish. The thing to remember is that anything you leave unspecified will be filled
in for you using the rules given above.