action.skip

jQuery-Machine, finite state machines that rock

===============================================

The jQuery plugin jquery-machine implements a simple and clean DSL to attach finite state machines to DOM elements. You can use it to build awesome multy-state UI elements and widgets with ease.

Motivation

==========

As Web developers striving to build beautiful and usable apps, we are often confronted with the design of fairly complex client-side UI widgets, transitioning between different states in response to various events. Often that leads to spaghetti JavaScript code which is difficult to maintain, and sometimes we even find ourselves surrending to second-best but more manageable solutions.

Surrender no more to complexity! Here is jquery-machine, our neat weapon.

jquery-machine provides you with a beautiful DSL to attach finite state machines to DOM objects, and automatically handles states transitions and events for you.

Usage

=====

Attach a finite state machine to a jQuery selection:

$("#my_element").machine( { /* state machine description */ } )

The state machine is described by an object literal with the state names as keys:

$("#my_element").machine({
  stateOne: { /* object describing stateOne */ },
  stateTwo: { /* object describing stateTwo */ },
  stateThree: { /* object describing stateThree */ }
});

Each state is described by these optional properties:

  • events: an event map whose keys are the events that cause a transition out from this state, and the values are strings (or functions evaluating to strings) with the name of the next state to transition into
  • onEnter: a callback to be executed when transitioning into this state
  • onExit: a callback to be executed when transitioning out from this state

You must also define a Default State.

  • defaultState: a boolean specifying whether this state is the default (the state in which the machine is in at the beginning, before any event is triggered) At least one state must be the default state.

A full example:

$("#myelement").machine({
  // Define state 'stateOne'
  stateOne: {
    defaultState: true, // stateOne is the default state. Alternatively, just call the default state "defaultState"
    onEnter: function() {
      // Callback function executed when entering stateOne. Here `this` is $("#myelement")
    },
    onExit: function() {
      // Callback function executed when exiting stateOne. Here `this` is $("#myelement")
    },
    events: {
      // Here you define an event map, specifying the events that trigger a transition from stateOne
      // to another state in the form `event: "nextState"`.
      click: "stateTwo", // When in stateOne and event `click` is triggered, state transitions to stateTwo
      dblclick: "stateThree", // When in stateOne and event `dblclick` is triggered, state transitions to stateThree
      "click .handle": "stateThree" // You can also specify selectors (in this case the transition
                                    // to stateThree is triggered when element of class handle is clicked)
    }
  },

  // Define state 'stateTwo'
  stateTwo: {
    onEnter: function( evt, previousState ) {
      // Executed when entering stateTwo. Note that the argument `evt` is the
      // event object that triggered the state transition into stateTwo.
      // The second argument is the previous state, the starting point of the transition.
    },
    onExit: function( evt, nextState ) {
      // Executed when exiting stateTwo. Note that the argument `evt` is the
      // event object that triggered the state transition out from stateTwo.
      // The second argument is the next state, the end point of the transition.
    },
    events: {
      // Events triggering transitions from stateTwo
      click: "stateThree",
      customevent: "stateOne",
      keypress: function( evt ) {
        // You can use a function for more advanced exit conditions
        // Here `evt` is the event object. You can thus access event attributes like `evt.which`, etc.
        // Return a string with the name of the state to transition to,
        // or false to stay in the current state
      }
    }
  },

  // Define state 'stateThree'
  stateThree: {
    // And so on...
  }
});

Getting the state


You can read the state of an element in any moment with $("#myelement").data("state").

Setting the state


You should not set the state programmatically. Let the events trigger automatically the state transitions you specified: you shouldn't use $("#myelement").data("state", "myForcedState") to force the state, because that puts the state machine out of sync (onExit and onEnter don't get called). When needed, you can always trigger events programmatically via the jQuery function .trigger().

Configuration options

=====================

The machine() function optionally accepts an object containing configuration options as the second argument. The full list of options is the following:

  • defaultState (string or function): a string specifying the default or starting state, or a function evaluating to a string. (At least one state must be set as the defaultState)

  • setClass (boolean): whether a class corresponding to the current state should be automatically set, allowing for easy styling of different states. By default it is set to false.

  • scope (string): a scope for the state machine, useful to attach multiple independent state machines to the same element avoiding name conflicts.

Further explanation of these configuration options follows.

Default state


There are three ways of specifying the state initially set by default:

  1. By naming a state defaultState

  2. By specifying defaultState: true in the definition of a state. This overrides the first way.

  3. By setting the defaultState option value to a string specifying the default state, or to a function evaluating to a string (in the scope of the function, this refers to the jQuery selector on which machine() is called). This overrides the first and second methods.

The last method let you attach the same state machine to different elements, with different starting states.

However you choose to define it, you must make sure that one of the states is the defaultState.

Classes corresponding to the current state


Often it is useful to set classes corresponding to the present state of an element. This way, it is easy to change the style of the element depending on its state. You can get this behavior automatically setting the setClass option to true:

$("#myelement").machine({
  active: {
    // ...
  },
  inactive: {
    // ...
  }
}, { setClass: true });

Scoping and multiple state machines on the same element


You can attach multiple independent state machines to the same element by giving them different scopes:

$("#myelement").machine({
  active: {
    // definition of state 'active'
  },
  inactive: {
    // definition of state 'inactive'
  }
}, { scope: "activation" })
.machine({
  open: {
    // definition of state 'open'
  },
  closed: {
    // definition of state 'closed'
  }
}, { scope: "openess" });

If you give a scope to a state machine, a prefix will be used to avoid name collision:

$("#myelement").data("activation-state"); // it may return 'active' or 'inactive'
$("#myelement").data("openess-state"); // it may return 'open' or 'closed'

Classes set by jquery-machine when you set the option setClass to true will be prefixed as well.

Testing


Tests are written using quint.

npm install
grunt test

Building (which also runs the tests)


npm install
grunt

Changelog

=========

  • v1.0.1 Enforce the requirement for at least one defaultState, modified the build process to use grunt. Other small tidy ups. (thanks to davesag)
  • v1.0.0 New event map with support for multiple events and selectors. Requires jQuery 1.7+
  • v0.1.5 Support for namespaced events and bugfixes (thanks to leemhenson)
  • v0.1.4 Attach an independent machine to each DOM element in the jQuery selection instead of a single one

Last git commit to the markdown source of this page:

Author: John Freeman

Date: Wed Oct 12 19:35:40 2022 -0500

If you see a problem with the documentation on this page, please file an Issue at https://github.com/DUNE-DAQ/nanorc/issues