Fork me on GitHub

Second Story

SecondStoryJS: State Machine (v1.0.0)

SecondStoryJS is our front-end development framework built as a collection of plugins for JavascriptMVC3. This plugin, the State Machine, is an extension of the jQuery.Controller which acts as a Finite State Machine. Each instance of a SS.Controller.StateMachine stores its current state, provides a means of changing states based on input and executing code on state change. State Machines are pretty technical, but once you start using them to represent widgets, you'll never go back.

Features

The State Map Object

All states, their events, transitions and callback functions are described in the states object. The format and its features are as follows

Initial State

All instances of a state machine will start in the initial state. This must be present in your states object.

  states: {
    initial: { }
  }

State Definitions

The initial state is exactly like any other state. To define another state, simply create another object in your states map.

  states: {
    initial:  { },
    stateTwo: { }
  }

Definition a Transition

States can only move to another state if they explicitly declare that transition possible. They must also declare which "event" triggers the transition. In the following definition, the state machine will move from initial to stateTwo when it recieves a pleaseMoveToStateTwo event.

  states: {
    initial:  { "pleaseMoveToStateTwo": "stateTwo" },
    stateTwo: { }
  }

In the above example, someone must manually call instance.publishState("pleaseMoveToStateTwo") to trigger the transition

Alternative Events

The state machine can also trigger transitions on jQuery DOM events and JavascriptMVC Controller (read: OpenAjax) events.

So, let's say that clicking an HTML element called move can also run the transition

  states: {
    initial:  { "pleaseMoveToStateTwo": "stateTwo",
                "#move click":          "stateTwo" },
    stateTwo: { }
  }

Finally, there is also a generic app.everyBodyMove OpenAjax event that could trigger the transition.

  states: {
    initial:  { "pleaseMoveToStateTwo":        "stateTwo",
                "#move click":                 "stateTwo",
                "app.everyBodyMove subscribe": "stateTwo" },
    stateTwo: { }
  }

State-specific events

All of the above examples will only trigger a transition when the state machine is in the initial state. Once it has been moved to the stateTwo state, it will stay there because we have not defined any transitions to either another state or back to the initial state.

So, let's define a transition to move us back to the initial state when we click an HTML element called back.

  states: {
    initial:  { "pleaseMoveToStateTwo":        "stateTwo",
                "#move click":                 "stateTwo",
                "app.everyBodyMove subscribe": "stateTwo" },
    stateTwo: { "#back click":                 "initial" }
  }

Global Events

Finally, we may need some transitions that execute no matter which state we are in. Perhaps something like a reset or an event to pause the state machine. We can define these in the special global state.

Let's assume we have an HTML element called reset which will return us to the initial state no matter where we are.

  states: {
    global:   { "#reset click":                "initial" },
    initial:  { "pleaseMoveToStateTwo":        "stateTwo",
                "#move click":                 "stateTwo",
                "app.everyBodyMove subscribe": "stateTwo" },
    stateTwo: { "#back click":                 "initial" }
  }

Entry and Exit Callbacks

Finally, we will want to actually DO something when states are changed. Each state definition has an onEnter and an onExit value which can be used in two different ways.

The first way is similar to JavascriptMVC's this.callback which executes a function in the current scope. So, let's say we want to show an alert box when we move to stateTwo. We'll start showing the whole controller definition now.

SS.Controller.StateMachine.extend("Example", {
  states: {
    global:   { "#reset click":                "initial" },
    initial:  { "pleaseMoveToStateTwo":        "stateTwo",
                "#move click":                 "stateTwo",
                "app.everyBodyMove subscribe": "stateTwo" },
    stateTwo: { onEnter:                       "showAlert",
                "#back click":                 "initial" }
  }
},
{
  showAlert: function() {
    alert("we are in stateTwo");
  }
});

The showAlert function will be excuted when we enter stateTwo which can happen my direct event (pleaseMoveToStateTwo), HTML click event (on #move) or OpenAjax event (app.everyBodyMove).

It is also possible to publish OpenAjax events instead of calling a local function onEnter or onExit. So let's send a app.leftStateTwo event when we leave stateTwo.

SS.Controller.StateMachine.extend("Example", {
  states: {
    global:   { "#reset click":                "initial" },
    initial:  { "pleaseMoveToStateTwo":        "stateTwo",
                "#move click":                 "stateTwo",
                "app.everyBodyMove subscribe": "stateTwo" },
    stateTwo: { onEnter:                       "showAlert",
                "#back click":                 "initial",
                onExit:                        "app.leftStateTwo" }
  }
},
{
  showAlert: function() {
    alert("we are in stateTwo");
  }
});

Now the app.leftStateTwo event will be published when we leave stateTwo which can happen by either clicking the #reset HTML element or the #back element.

Automatically Setting CSS Classes

Let's say we have a controller which can be enabled and disabled and we want the disabled state to have a red background. We can automatically update the element's class when states change with mapStatesToClass function. Here's a full example:

SS.Controller.StateMachine.extend("EnableOrDisableWidget", {
  states: {
    initial:  { "disableMe": "disabled" },
    disabled: { }
  },
  
  setup: function() {
    this._super.apply(this, arguments);
    this.mapStatesToClass("iAmEnabled", {
      disabled: "iAmDisabled"
    });
  }
},
{
});

The above will set an iAmEnabled class on the element when it is in the initial state and change that class to iAmDisabled when it is in the disabled state. The first parameter is the "default" parameter and the element will have that class unless it is in a state described by the second parameter. If there were a third state, called "indeterminate" then the element would have the "iAmEnabled" class because no overwrite for that state is described in the second parameter.

Installation

The source code is available on GitHub at http://github.com/secondstory/secondstoryjs-statemachine. Please, fork the project, make your own changes and we'll happily pull bug fixes and new features into the official repository.

However, JavascriptMVC3 provides an easier way to download plugins. From your project folder, simply run:

./steal/js steal/getjs ss/state_machine

This will download the full plugin into your working directory.

License

This code is provided under the MIT License.

Bugs

Please submit any bug reports to the project issue tracker on GitHub at: http://github.com/secondstory/secondstoryjs-statemachine/issues.