States / Transition ordering

Sometimes, to ensure your UI behaves correctly, it is important to know, understand and adjust the order of transition invocation for a given state. There are two types of transition ordering we can talk about:

  1. The order in which different state managers apply their sets of transitions
  2. The order in which transitions are applied within the boundaries of one state manager

Transition set ordering between state managers

The default behavior is for each state manager's set of transitions to be applied in the order in which declarations for those state managers are found in the this.states array. If StateActionManager declarations are added into that array before DisplayStateManager declarations then they will be applied first.

However, sometimes you want to bypass that order for certain - but not all - transitions of a given state manager. A real world example would be generating a QR code. The QR code library in the example needs a visible dom element to correctly generate the QR code image. If the dom element is hidden, it will not do it. Consider this code:

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

  constructor() {

    this.states = [
      "action", {},
      [{ status: "invoiced" }, "generateQRCode"],
      "display", {},
      [{ status: "invoiced" }, "invoice_qr"]
    ];

  }

  generateQRCode() {
    new QRCode({ el: this.findPart("invoice_qr") });
  }
}

Because StateActionManager declarations go first it will invoke the generateQRCode() method first and only then show the component part this QR-code is supposed to appear in. The problem can be easily fixed by placing DisplayStateManager declarations above StateActionManager declarations, but what if we had other declarations for StateActionManager most of which needed their transitions to be applied specifically before any of the transitions from DisplayStateManager? In that case, we can add a special instruction only to this particular declaration. The run_before instruction would delay the application of transitions for this one declaration until all transitions from DisplayStateManager are applied:
  this.states = [
    "action", {},
    [{ status: "invoiced" }, { in: "generateQRCode", run_after: "display" }],
    "display", {},
    [{ status: "invoiced" }, "invoice_qr"]
  ];

Or, alternatively, we could've added the run_before option to the transition object in DisplayStateManager declaration:
  this.states = [
    "action", {},
    [{ status: "invoiced" }, "generateQRCode"],
    "display", {},
    [{ status: "invoiced" }, { in: "invoice_qr", run_before: "action" }]
  ];

Be careful using many run_before and run_after instructions - there's a sorting mechanism that would do up to 100 iterations, but if after those 100 iterations it can't figure out which transitions should be applied first, it would stop trying to sort and throw. For instance, if we wrote the following declarations, an error would be thrown:

  this.states = [
    "action", {},
    [{ status: "invoiced" }, { in: "generateQRCode", run_before "display" }],
    "display", {},
    [{ status: "invoiced" }, { in: "invoice_qr", run_before: "action" }]
  ];

That's because the two run_before instructions are contradicting each other. Therefore, the general advice is to use run_before and run_after instructions as rare as possible.

Transition ordering inside state managers

Each state manager decides on its own on the order in which to run transitions.

  • StateActionManager will run all transitions in the order in which the corresponding state declarations were placed into the this.states array, however it will run out transitions first, followed by the in transitions. Transitions are run synchronously, one after another, (unless the method you're invoking is itself asynchronous).
  • DisplayStateManager has a slightly different approach where it would apply transitions, both in and out all at once asynchronously. It makes sense because of the nature of DOM-animation: you wouldn't want to wait while each show/hide animation is applied one-by-one to each entity - you just want to change what the user sees on the screen all at once!
  • Another thing to know about DisplayStateManager is that it doesn't queue transitions from different states that the component happened to go through while transitions from the first state change were still running - it simply applies a set of transitions for the latest state and throws away all of the other ones. This might be a bit confusing, but it also makes sense: suppose the animation to show and hide all necessary entities for any given state lasts 1 second. During that second a lot can change and your component may actually go through 2 or 3 other state changes. If DisplayStateManager were to apply all of them, not only would it take additional 2-3 seconds, but all the flashing would surely annoy the user.