/stated

Simply put state management for your JavaScript application

Primary LanguageJavaScriptMIT LicenseMIT

Build Status

Stated

Simply put state management for your JavaScript application. Pushdown automata (FSM with history) in less than 1KB.

Installation

$ npm install --save @mjstahl/stated
// as a factory function
const stated = require('@mjstahl/stated')

// as a class expression
const { Stated } = require('@mjstahl/stated')

API

stated(states: Object[, persistent: Boolean]) -> Stated

new Stated(states: Object[, persistent: Boolean]) -> Stated

To create an instance of Stated pass a 'states' object. A valid states object must have an, at least, a single state. initial states can be set after instantiation by assign a valid state to the initial property.

By default each Stated object is not persistent. If you wish to store each state change pass 'true' as the second argument to the constructor.

const h20 = stated({
  initial: 'water',
  water: {
    // actions must reference existing states
    FROZEN: 'ice',
    BOILED: 'steam',
    // Number, Array, Function, Object, etc
    value: '60F'
  },
  ice: {
    BOILED: 'steam',
    WARMED: 'water',
    value: '32F'
  },
  steam: {
    COOLED: 'water',
    FROZEN: 'ice',
    value: {
      temp: '212F'
    }
  }
})

h20.state //-> 'water'
h20.value //-> '60F'

<stated>.to(action: String[, updateValue: Any]) -> Stated

Transition from the current state to a new state. If called with a second argument. The value of the new state will be updated with the value. If value is an Object, the current value and updateValue will be merged. If the updateValue is not an Object, value will be replaced with updateValue.

  h20.to(h20.actions.FROZEN)

  h20.state //-> 'ice'
  h20.value //-> '32F'

  h20.to(h20.actions.BOILED, { state: 'gas' })

  h20.value //-> { state: 'gas', temp: '212F' }

<stated>.reset() -> Stated

Set the Stated object's state to initial state.

  h20.reset()

  h20.state //-> 'water'
  h20.value //-> '60F'

<stated>.actions -> Object

Return an object with actions as properties and associated values. Provided to avoid typos when traversing states. For example:

h20.to(h20.actions.BOILED)
h20.actions

//-> { 'FROZEN': 'FROZEN', 'COOLED': 'COOLED' }

actions only includes actions related to the current state. So attempting to call to with an invalid action will cause a runtime error as that action does not exist on <stated>.actions.

For a state that does not have any actions all the states of the Stated object will available from actions.

<stated>.state -> String

Return the name of the Stated's current state.

  h20.state

  //-> 'steam'

<stated>.value -> Any

Returns the value of the current state if one exists; returns undefined if not.

  h20.to(h20.actions.WARMED)
  h20.value

  //-> '60F'

<stated>.via -> Object

Return an object with actions as properties and the state they point to as values.

h20.to(h20.actions.FROZEN)
h20.via

//-> { BOILED: 'steam', WARMED: 'water' }

Guards

Each state may have a canEnter and canLeave function that is executed when attempting to leave one state and enter another. Each function takes one argument, the Stated object, for use during its execution.

When a transition is occurring (via the on method), if there is a canLeave function on the current state, it is executed. If the canLeave function returns true and the future state has a canEnter function, the canEnter function is executed. If the canEnter function of the future state returns true, the Stated object is transitioned successfully.

If either the canLeave or canEnter functions exist and return false, the transition does not occur.

.canLeave(self: Stated) -> Boolean

.canEnter(self: Stated) -> Boolean

Transition Functions

Each state can have an onLeave and onEnter function that is executed when leaving one state, and having entered another. This are useful for performing side-effects.

.onLeave(self: Stated) -> Void

Executed when a transition is to occur but before the current state has been replaced with the next state.

const h20 = stated({
  initial: 'water',
  water: {
    FROZEN: 'ice',
    value: '60F'
    onLeave: () => console.log('Winter is Coming!!!')
  },
  ice: {
    WARMED: 'water',
    value: '32F'
  },
})

h20.to(h20.actions.FROZEN)

//-> 'Winter is Coming!!!'

.onEnter(self: Stated) -> Void

Executed after the state and values have been updated, history has recorded (if persistent), and 'transition' event has been emitted.

const h20 = stated({
  initial: 'water',
  water: {
    FROZEN: 'ice',
    value: '60F'
    onLeave: () => console.log('Winter is Coming!!!')
  },
  ice: {
    WARMED: 'water',
    value: '32F'
    onEnter: () => console.log('Winter is Here!!!')
  },
})

h20.to(h20.actions.FROZEN)

//-> 'Winter is Coming!!!'
//-> 'Winter is Here!!!'

Events

Each stated object is also an EventEmitter. When the Stated object transitions from one state to another all callbacks passed to the onTransition function are evaluated with the Stated object passed the only argument to the callback. onTransition returns a function that unsubscribes when executed.

<stated>.onTransition(callback: Function) -> unsubscribe: Function

  const unbind = h20.onTransition(({ state, value }) => {
    console.log(state) //-> 'water'
    console.log(value) //-> '60F'
  })
  h20.reset()

  // when you are finished listening
  unbind()

Persistence

Each Stated object is can be persistent and store each state change, allowing the user to undo and redo states. Use can turn persistence on and off with the persistent property. You can make Stated object persistent by default by passing true as the second argument to the constructor.

<stated>.persistent -> Boolean

new Stated({... states, true})

Toggle whether the Stated object will store each state change.

h20.persistant //-> false, Stated objects are not persistent by default

// To turn on persistence, simply set 'persistent' to 'true'
h20.persistent = true

<stated>.undo() -> Stated

Return to a previous state. If the current state is the initial state and undo is called, the Stated object will be returned without performing a state transition (in the initial state).

  h20.to(h20.actions.FROZEN)
  h20.state //-> 'ice'

  h20.undo()
  h20.state //-> 'water'

<stated>.redo() -> Stated

Re-apply a later state. If the current state is the top of the stack of states and redo is called, the Stated object will be returned without performing a state transition (the current state will remain equal to the state at the top of the stack).

  h20.to(h20.actions.FROZEN)
  h20.state //-> 'ice'

  h20.undo()
  h20.state //-> 'water'

  h20.redo()
  h20.state //-> 'ice'