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

States

States are the most powerful way to build complex UI logic. It's Qite's way to express "when X is true, do Y or show/hide Z" as plain rules, declaratively. Instead of wiring a bunch of if statements across event handlers, you declare a small set of rules and let the engine keep things consistent.

There are two engines for states:

  • StateEngine runs behavior transitions (functions) when a state becomes active or inactive.
  • DisplayStateEngine shows or hides UI targets based on which state or states are currently active.

Active states are determined by a combination of fields and flags values. Whenever a field or a flag changes, state engines reevaluate active states automatically, and then run transitions. Transitions are functions that you write or, in the case of DisplayStateEngine, calls that hide or show particular parts of your UI.

Example: online-store order lifecycle

Let us look at a single, realistic example and use both engines in it.

We will build an OrderPanel component that reacts to an order lifecycle, but also to a couple of extra facts that often exist in real systems:

  • payment_method affects which payment instructions we show
  • saving affects whether we show a spinner / disable actions

The order has:

  • field status: "draft", "submitted", "paid", "shipped", "cancelled"
  • field payment_method: "card" or "bank_transfer"
  • field tracking_number
  • flag saving

When fields and flags change, we want two things:

  1. Run certain business logic (generate invoice, notify customer, etc).
  2. Show or hide certain sections of the UI.
export default class OrderPanel extends BaseComponent {

    static flags = [
        ["saving"],
    ];

    static field_defaults = {
        status: "draft",
        payment_method: "card",
        tracking_number: null,
    };

    // Behavior rules
    static states = [

        // When submitted, mark as saving and log.
        [
            { status: "submitted" },
            {
                in:  (c) => { c.flags.set("saving", true);  c.logSubmit(); },
                out: (c) => { c.flags.set("saving", false); },
            },
            { name: "submitted" }
        ],

        // When paid, generate invoice once on enter.
        [ { status: "paid" }, (c) => c.generateInvoice(), { name: "paid" } ],

        // When shipped and tracking exists, notify.
        [
            { fields: { status: "shipped", tracking_number: "isPresent()" } },
            (c) => c.notifyShipment(),
            { name: "shipped" }
        ],
    ];

    // Visibility rules
    static display_states = [
        { visibility_mode: "whitelist" }, // means default is UI targets are hidden

        // Draft: choose which payment instructions to show.
        [ { fields: { status: "draft", payment_method: "card" }},          ["#draft", "#pay_card", ">submit"]],
        [ { fields: { status: "draft", payment_method: "bank_transfer" }}, ["#draft", "#pay_bank", ">submit"]],

        // Submitted: nested rules decide whether we show "processing" or "queued".
        [
            { status: "submitted" }, ["#submitted", ">cancel"],
            [
                [ { flags: ["saving"] },  ["#processing"]],
                [ { flags: ["!saving"] }, ["#queued"]],
            ]
        ],

        // Paid and shipped.
        [ { status: "paid" },      ["#paid", "#shipping"]],
        [ { status: "shipped" },   ["#shipped", "#tracking"]],
        [ { status: "cancelled" }, ["#cancelled"]],
    ];

    logSubmit() {
        // business logic
    }

    generateInvoice() {
        // business logic
    }

    notifyShipment() {
        // business logic
    }
}

Now let's break down what's happening in the code above without going into detail for now:

  1. When status changes, BaseComponent publishes a field_change event.
  2. Both state engines receive it and request a reevaluation.
  3. StateEngine runs behavior transitions for states that became active or inactive.
  4. DisplayStateEngine computes which UI targets should be visible and hides the rest.

Notice what we did not write in our code: there were no manual event listeners, no if status changed from X to Y and, importantly, there were no scattered show() and hide() calls. We described conditions and effects, then Qite applied it when state (fields and flags) changed.

Scalability

As more fields and flags are added, you extend the rules instead of growing conditional logic inside event handlers. Each rule answers the following questions:

  • When is this true?
  • What should happen when it becomes true?
  • What should happen when it stops being true?

In the next section we will break down the rule structure [ when, then, nested_or_metadata ] and show how to write clean, expressive state rules.