Clone on Gitea
Introduction
Explained by ducks
FAQ
Getting Started
Components
Component Basics
Auto Initialization From DOM
Templates
Children and Parents
Roles
Strictness
Fields
What is a Field
Binding Fields to DOM
Reading and Writing Fields
Value Casting
Flags
Events
Overview
Event Handlers
DOM Events
Custom Events
Propagation and Flow
Advanced Event Control
Beyond Components
States
Overview
StateEngine
DisplayStateEngine
Field Matchers
Ordering and priority
Debugging
Advanced usage
Ajax
ComponentUI
VirtualForm
Unit Testing
Helpers
Overview
Cookies
Arrays

Advanced usage

Most components only need simple rules. However, as your UI grows, you may need finer control over how states compete and how switches are scheduled. This section covers:

  • winner semantics
  • overlay and eligible states
  • switch() vs requestSwitch()

Winner semantics

By default, StateEngine operates in winner mode. When multiple states match at the same time, only one of them becomes the winner. That winner is the state whose transitions are applied. You therefore don't accidentally run multiple conflicting transitions at once.

But, sometimes you want additional states to participate without becoming the winner. This is when overlay option in a rule might come in handy.

overlay

An overlay state never becomes the winner, but its transitions still run if the state is active. Example:

static states = [
    [ { status: "paid" }, (c) => c.enableShipping(), { name: "paid" } ],
    [ { flags: ["saving"] },
        (c) => c.showSpinner(),
        { overlay: true, name: "saving_overlay" }
    ]
];

In this snippet:

  • The paid state may become the winner.
  • The saving_overlay state will never replace the winner.
  • But when saving is true, its transition still runs.

This is useful for cross-cutting concerns such as loading indicators, temporary warnings, or diagnostic modes. However, if you find yourself excessively using overlay, consider switching to active_mode="winner" for your state engine.

eligible

By default, states compete and only one state becomes the winner. Only the winner's transitions run. The eligible flag changes this behavior in a very specific way.

An eligible state participates in winner resolution(it competes normally). and may still run its transitions even if it does not win. This makes it different from overlay

overlay :

  • Does NOT participate in winner selection.
  • Never becomes the winner.
  • If it matches, it always runs alongside the winner.

eligible :

  • DOES participate in winner selection.
  • Can become the winner.
  • If it loses, it may still run.

So the difference is subtle, but important: overlay is outside the competition, eligible is inside the competition and, thus, affects if transitions for other rules run or not. Consider this example:

static states = [

    // Main business flow
    [ { status: "paid" }, (c) => c.enableShipping(), { name: "paid" } ],

    // High value orders
    [ { total: (c, v) => v > 1000 },
      (c) => c.enablePriorityHandling(),
      { eligible: true, name: "high_value" }
    ]
];

Assume status = "paid" and total = 1500, so both states match.

Case 1 : "high_value" is NOT eligible and only one state wins. Suppose "paid" wins — then only enableShipping() runs.

Case 2 : "high_value" IS eligible. Then, if "paid" wins:

  • enableShipping() runs
  • enablePriorityHandling() also runs

If "high_value" wins (for example due to higher priority):

  • enablePriorityHandling() runs
  • enableShipping() does NOT run

Now compare that with overlay : if "high_value" were marked as { overlay: true } then:

  • It would never "win".
  • But if it matches (activated), it always runs.
  • enablePriorityHandling() would always run alongside the winner.

In practice, eligible should be used rarely. Most components are clearer and easier to reason about with a single winner and occasional overlays. Reach for eligible only when you truly need cooperative competition between states — when the state is conceptually part of the same resolution space but should be allowed to cooperate when matched.

switch() vs requestSwitch()

Both engines expose two methods: switch() and requestSwitch()— let's see how these two methods differ.

While switch() evaluates and applies transitions immediately requestSwitch() schedules a switch in a safe, batched way. Inside BaseComponent, field and flag changes call requestSwitch() instead of switch() directly. This has two advantages:

  1. Multiple rapid updates are coalesced into a single reevaluation.
  2. It prevents recursive or unstable re-entries into the engine.

If you ever need to use it (which is unlikely), you should almost always use requestSwitch(). Use switch() only if you are manually controlling engine timing and you are certain that no further changes will happen synchronously.

Manual control example. if you instantiate an engine manually:

this.state_engine = new StateEngine(this, rules);

you may call:

this.state_engine.switch();

to force immediate evaluation. But in normal component usage, you don't need to call either method — BaseComponent wires everything automatically and triggers requestSwitch() whenever fields or flags change their value.

When to reach for advanced features

If your rules feel like they are fighting each other, or you find yourself encoding complex priority chains, pause and simplify. Most of the time clear conditions, a single winner and occasional overlays are more than enough. Advanced features exist to solve real edge cases, but they are not meant to be the default way of thinking here.

Summary

At this point you have seen the full state system:

  • Rule structure.
  • Field and flag matchers.
  • Display control.
  • Debugging.
  • Advanced resolution semantics.

The rest is application design. Good luck, fren.