States / Overview

States are Webface's implementation of a state machine. It allows you to write declarative statements in your component class and invoke certain code or show/hide certain component parts and child-components when component changes its state.

Check states usage example and experiment with code using the example page for states.

Basic mechanism of work

Component state is defined by ALL of its attributes and their values at any given moment. Let's say we have a UserComponent with three attributes: email, password and country. Whenever any of those attributes is changed with either UserComponent.set() or UserComponent.updateAttributes() the component checks two things:

  1. Which state the component left
  2. Which new state the component has entered
Based on this, it will invoke the specified code or show/hide certain parts and child-components. The code that's being run for those two cases is called a transition. You can have an in transition (code to invoked on entering a certain state) and an out transition (code invoked when leaving the state).

IMPORTANT! This is a bit different from the common definition of a state machine. Normally, in a state machine a transition is something that actually changes the state after receiving a so called signal. There is no signal in Webface.js - whenever attributes are updated, the component checks whether any of the state declarations match the new state and then decides what to do about based on the declarative instructions given - the act of doing that is called the transition in Webface.js terminology. This makes a lot more sense in UI-context. DO NOT CONFUSE how it works. Once again, in Webface.js change of attribute values (state) causes transitions to be invoked, not the other way round. You can think of it this way: a component tells you "Hey, I have this much money now and I'm N years old" and you help it pick the best outfit and give financial advisors phone - that is, you help it do things that fit its circumstances.

Defining states and transitions

Webface.js components process states with instances of the StateManager class, of which there are two descendants: StateActionManager and DisplayStateManager. StateDispatcher does the groundwork of detecting certain attributes changed and routing requests to these manager instances. You don't have to look inside those classes or even know those classes exist, but it's good to know what they are to understand things better.

Notice a bit of an inconsistency in naming the classes: the word State appears before the word Action in StateActionManager and after the word Display in DisplayStateManager - that's because in English it reads better that way, but you probably want to name StateManager descendants so that the word State goes after the first word that indicates the purpose of that state manager class (unlikely you need to code your own manager class though!).

To define a state and a transition for it, you define a property inside your component class' constructor:

class UserComponent extends extend_as("UserComponent").mix(Component).with() {
  constructor() {
    this.attribute_names = ["country", "age", "occupation"];
    this.states = [
      ...
    ];
  }
}

this.states is an array where almost each element is a state declaration - however there are exceptions. We also need to let the StateDispatcher know which StateManager instance to use. So, to improve the readability of state declarations array, instead of using nested arrays and objects, we simply have to make sure that we specify state manager name and options before we list all state declarations. So, in essence, this.states becomes a flat array that looks like this:
this.states = [
  "action", { pick_states_with_longest_definition_only: false }, // <-- state manager name and options
  [{ country: "Dictatorstan" }, "blockByCountry"],               // <-- state declaration
  [{ age: { less_than: 17 }  }, "blockByAge"]                    // <-- state declaration
];

The declarations above will invoke UserComponent.blockByCountry() and UserComponent.blockByAge() functions when attribute values change to the specified values. Only in transitions are specified in these two declrations - we'll look at how to specify out transitions later.

Terminology

Before we move on, let's go over the correct terminilogy for state declarations and what's inside it. Here's an annotated state declaration:

/* declaration -->*/ [  { attr1: "value1", attr2: "value2" }, "some_important_button"  ]
//                   |  |-----------------^----------------|  |----------^----------|  |
//                   |            state definition                state transition     |
//                   |------------------------------^----------------------------------|
//                                            state declaration

Let's go over the more precise descriptions of each term:
  • state definition - a set of attributes and their values that defines a state for the component. It's how we know a component is in a certain state by checking those attributes and values and seeing that their values match the ones in the definition. For example, this is a state definition: { attr1: "value1", attr2: ["value2", "value3"] } It says that in order for the transitions (listed next) to be invoked, the component needs to have its attr1 value be equal to "value1" AND its attr2 value be equal to either "value2" or "value3".
  • state transition - this can be whatever it needs to be. Its use is defined by classes that extend StateManager. In case of DisplayStateManager it can be an array of entity names (component parts and children roles, see DisplayStateManager page) that need to be hidden or shown, but for StateActionManager it's just the function names defined in your component that need to be invoked.
  • state declaration is state definition + state transition + folded states (folded states to be explained further).