States / State definitions

Webface's states functionality provides a very extensive tool set to describe states via attribute values. Not only can you specify literal values for attributes, but you can also use matchers such is more_than() and pass values to these matchers. You can also use functions that will run when a new state is being evaluated (as opposed to component initialization time). There are also two modes of matching that the state manager can work in and we'll discuss them below.

Specifying attribute values

The simplest case of defining a state is when we specify literal values for each listed attribute:

this.states = [
  // ...
  [{ country: "Dictatorstan", age: 18 }, "some_transition"]
];

This is obviously not a very good way to define a state which involves age - unless you're only interested in users of 18 years old. An improvement would be to use an array for the age attribute:
this.states = [
  // ...
  [{ country: "Dictatorstan", age: [18, 19, 20, 21, 22 ... 99]}, "some_transition"]
];


You can, of course, see, that this approach, while useful for listing limited number of values, is not the nicest one if you have lots and lots of values to list. Instead, we'd be better off using assertions defined in Webface - one of those is called more_than:
this.states = [
  // ...
  [{ country: ["Dictatorstan", "USA"], age: { more_than: 17 }}, "some_transition"]
];

The code above also shows good example of using array of values - we have two countries on the list of matching values for this particular state definition.

Now, suppose you have a case where you have different age requirements for different countries (maybe you're a bar serving alcohol!). You would then do an equivalent of a logical OR operator by wrapping the state definition into another array where each element is a state definition:

this.states = [
  // ...
  [[
    { country: "Dictatorstan", age: { more_than: 17 }},
    { country: "USA",          age: { more_than: 20 }}
  ], "some_transition"]
];


In this case, some_transition() will get invoked if user is from Dictatorstan and over 18 years old or from US and over 21.

Assertions for attribute values

We've already mentioned one assertion more_than() that can be used in place of attribute values in state definitions. Here's a full list of assertions you can use:

  • any()
  • is_null()
  • not_null()
  • is_not(value) or its alias not(value)
  • more_than(value)
  • less_than(value)
  • is_in(array_of_values) - this one is the same as passing an array so the following two declarations are equivalent:
    [{ country: ["Dictatorstan", "USA"]}, "some_transition"]
    // is the same as
    [{ country: { is_in: ["Dictatorstan", "USA"] }}, "some_transition"]
    
    
  • not_in(array_of_values)
Assertions that require a value argument must always be wrapped by an object that's put in place of attribute value, so it looks like this:
[{ country: { is_in: ["Dictatorstan", "USA"] } }, "some_transition"]
//          |                                |
//          |----------------^---------------|
//                 object for assertions

However, when no value is required for the assertion you want to use - for instance not_null() doesn't need any - then you can simply pass assertion name as a string and append () at the end:
[{ country: "not_null()" }, "some_transition"]
// is the same as
[{ country: { not_null: true }, "some_transition"]

The any() assertion is interesting and one might wonder why use it, but it is marginally useful when you you want a state definition with more specificity to take priority over other state definitions.

Functions as attribute values

Sometimes, for attribute values in your state definitions, you may want to have functions that are invoked and return a value at a time when your component changes state and we're trying to match it against your declaration (we'll call this time state evaluation time). For example, you may want to have a state that's defined by two attributes being equal, but the exact values are not important:

this.states = [
  // ...
  [{ country_of_origin: () => this.get("country") }, "bornAndRaisedGreeting"]
];

In this case, UserComponent.bornAndRaisedGreeting() will only be invoked when a country of origin (where user was born) is the same as their current country.

Referencing attributes in other components

While right now Webface's states do not allow you to reference child components based on their roles (as this would create ambiguity if there's more than 1 child with the same role - then which attribute values should we match against, child #1 or child #2?), it does allow you to reference attributes in another component if that component is assigned as a property of the current component. For instance, if we have UserComponent with an AccountComponent as a child with the role "account", then we could assign that component to the UserComponent.account property and reference it using the dot (.) notation in state definitions:

class UserComponent extends extend_as("UserComponent").mix(Component).with() {

  constructor() {
    this.states = [
      "action", {}
      [{ "account.email": null }, { in: () => alert("Email can't be blank!") }]
    ];
  }

  afterInitialize() {
    super.afterInitialize();
    this.account = this.findFirstChildByRole("account");
  }

}

All changes to the email attribute in the instance of AccountComponent that is assigned to this.account will be automatically tracked, no extra code is necessary. When email changes to null, it'll alert the user.