State

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.

// In the browser
JS.require('JS.State', function(State) { ... });

// In CommonJS
var State = require('jsclass/src/state').State;

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 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 State. It should define an initial state for itself when initialized.

var DrawingController = new Class({
    include: State,

    initialize: function() {
        this.setState(Tools.Pen);
    }
});

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, 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 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 Class({
    include: State,

    initialize: function() {
        this.setState('Pen');
    }
});
DrawingController.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, 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 Class({
    include: State,

    initialize: function() {
        this.name = 'Twiddle';
        this.setState('Incomplete');
    }
});
Twiddle.states({
    Complete: {
        getName: function() {
            return this.name;
        }
    },
    Incomplete: {

    }
});

var twid = new Twiddle();
twid.getName() === twid     // -> true
twid.setState('Complete');
twid.getName()      // -> "Twiddle"

Notice that inside Complete.getName, the this keyword refers to the object calling the method, just as for regular ‘stateless’ methods.

State inheritance

State also supports inheritance using the following set of rules: Given a class Controller with states Pen and Selection, and a subclass ChildController,

These rules are enforced by 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 Class(DrawingController);
ChildController.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.