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

StateEngine

In the overview you saw states in action. Now we will break down the rule structure and explain how StateEngine works. A state rule has this shape:

[ when, then, nested_or_metadata ]

You usually define them inside a component like this:

static states = [
    [ when, then, metadata ],
    ...
];

Whenever Qite sees component has a static member called state it initializes a StateEngine for it and feeds it all the rules listed in that state member.

The "when" part

The when part defines the conditions under which a state becomes active. In practice, you will most often use the object form:

[ { status: "paid" }, ... ]

This means the state becomes active when component.fields.get("status") === "paid". But you can also match multiple fields:

[ { fields: { status: "shipped", tracking_number: "isPresent()" } }, ... ]

You can also use flags:

[ { flags: ["saving"] }, ... ]
[ { flags: ["!saving"] }, ... ]

There is a shorthand object form. If you omit explicit fields or flags keys, Qite infers them:

[ { status: "paid", saving: true }, ... ]

Here status is treated as a field and saving as a flag (because it was declared in static flags). If a flag value is a function, it becomes a flag matcher:

[
    // here, c is a component instance and v is current flag value
    { saving: (c, v) => v === true },
    ...
]

You can also use a function as the condition:

[
    (c) => c.fields.get("status") === "paid" &&
           c.flags.get("saving") === false,
    ...
]

Or an array of functions. In that case, all functions must return true. And a state itself becomes active only if all its conditions (and each function in each condition) evaluate to true.

The "then" part

The then part defines transitions — those run when a state changes its active status. When the state becomes active, its "in" transitions run. When the state becomes inactive, its "out" transitions run. In its simplest form it's just a single function, which is treated as an "in" transition:

[ { status: "paid" }, (c) => c.generateInvoice() ]

But you can explicitly define both "in" and "out" transitions:

[
    { status: "submitted" },
    {
        in:  (c) => c.flags.set("saving", true),
        out: (c) => c.flags.set("saving", false)
    }
]

Or pass an array of functions, which are all treated as "in" transitions:

[
    { status: "paid" },
    [
        (c) => c.generateInvoice(),
        (c) => c.logAnalytics()
    ]
]

Each transition receives the component instance as its only argument.

Nested rules

Nested rules let you share conditions and transitions without repeating them. Consider this code:

[
    { status: "submitted" }, (c) => c.logSubmit(),
    [
        [ { flags: ["saving"] },  (c) => c.showSpinner() ],
        [ { flags: ["!saving"] }, (c) => c.hideSpinner() ]
    ]
]

The parent's condition { status: "submitted" } applies to all nested rules. Likewise, the parent's transition logSubmit() is inherited as well. The code above effectively translates into the following two states internally:

[
    [
      { fields: { status: "submitted" }, flags: ["saving"] }, 
      [(c) => c.showSpinner(), (c) => c.logSubmit()]
    ], [
      { fields: { status: "submitted" }, flags: ["!saving"] },
      [(c) => c.hideSpinner(), (c) => c.logSubmit()]
    ]
]

The first notation literally translates into the two states shown in the second option. No standalone rule like [{ status: "submitted" }, (c) => c.logSubmit()] is implied. And even though in this particular example it doesn't matter, because flag is always true or false, it may be important to remember when writing more complex examples.

Nested rules help you structure related logic without duplicating the outer conditions.

Metadata

The third element of a rule can also be a metadata object:

[ { status: "paid" }, (c) => c.generateInvoice(), { name: "paid" } ]
// -----------------------------------------------|------^-------|--
                                                      metadata

Metadata can assign a name (useful for debugging), adjust priority, or control advanced behavior such as eligibility and overlay semantics. We will return to these advanced topics later.

How StateEngine evaluates states

Whenever a field or flag changes, the engine reevaluates all rules from scratch. It computes which states are active, compares them to the previous set, and runs the appropriate transitions. States that became inactive run their "out" transitions, states that became active may run their "in" transitions depending on their eligibility and ordering.

You never manually track previous values. The engine handles activation, deactivation, ordering, and transition execution. In the next section we will look at DisplayStateEngine, which uses the same rule format but interprets transitions as visibility targets.