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

DisplayStateEngine

DisplayStateEngine uses the exact same rule format as StateEngine, but interprets transitions differently: instead of running your functions, it treats the then part as a list of UI targets that should be visible when the state is active.

So, in other words while StateEngine controls behavior, DisplayStateEngine is a specialized engine focusing on controlling visibility of UI elements. The rule format stays the same, however:

[ when, then, nested_or_metadata ]

But in the DisplayStateEngine the then part is usually a list of target names. Targets is an array of strings with the same notation as we used for events, so you get #part for component parts, .field for component fields (the ones that have associated HTML-elements) and >role or role for child components with particular rules.

Basic example

Consider the order example again. We want to show different parts of the UI depending on the current status.

static display_states = [
    { visibility_mode: "whitelist" }, // whitelist is default, can be skipped

    [ { status: "draft" },     ["#draft", ">submit"] ],
    [ { status: "paid" },      ["#paid", "#shipping"] ],
    [ { status: "shipped" },   ["#shipped", "#tracking"] ],
    [ { status: "cancelled" }, ["#cancelled"] ],
];

Targets such as #draft or >submit map to component parts or roles in the template. When a state becomes active, its targets become visible.

visibility_mode

The first optional object inside display_states configures the engine. The most common option is:

{ visibility_mode: "whitelist" }

Whitelist is the default and is what you will use most of the time. In whitelist mode, the logic is:

  1. Hide everything.
  2. Show only the targets listed by the active state.

There is also a "blacklist" mode where the meaning flips:

  1. Show everything
  2. Hide only the targets listed by the active state.

When we say hide or show "everything" what we actually mean is that we show or hide every other target listed in any inactive state rule. If there's a target that isn't covered in any of the rules, it's never considered and, thus, its visibility is NOT managed by the DisplayStateEngine

active_mode

Another option you may want to use is:

{ active_mode: "winner" }

This controls how multiple active states are interpreted. By default, active_mode is "winner". That means only the winning state (plus overlays and eligible states) contribute targets. However, it may often happen that for your particular component you'd prefer to have a different setting:

{ active_mode: "all" }

Then the engine shows the union of targets from all active states. This is useful when multiple independent states can be active at the same time and should all affect visibility. Here's an extended example that demonstrates a component in which active_mode: "all" is preffered to express its UI logic:

export default class OrderPanel extends BaseComponent {

    ...

    static display_states = [
        { visibility_mode: "whitelist", active_mode: "all" },

        // Main phase panels (exactly one of these is typically active).
        [ { status: "draft" },     ["#draft", ">submit"] ],
        [ { status: "submitted" }, ["#submitted", ">cancel"] ],
        [ { status: "paid" },      ["#paid", "#shipping"] ],

        // Independent banner: can appear on top of any phase.
        [ { payment_failed: true }, ["#payment_failed_banner"] ],

        // Independent overlay: can appear on top of any phase.
        [ { flags: ["saving"] }, ["#saving_overlay"] ],
    ];
}

Nested rules

Nested rules work exactly the same way as in StateEngine

[
    { status: "submitted" }, ["#submitted", ">cancel"],
    [
        [ { flags: ["saving"] },  ["#processing"] ],
        [ { flags: ["!saving"] }, ["#queued"] ],
    ]
]

The parent targets are inherited by nested rules. So the effective targets for the first nested rule are:

["#submitted", ">cancel", "#processing"]

And for the second:

["#submitted", ">cancel", "#queued"]

As with StateEngine, nesting keeps visibility logic structured and concise, avoiding repeating outer conditions.

Rapid changes and in-flight transitions

One important difference from StateEngine is how transitions are applied. DisplayStateEngine always computes the full visibility result on every switch:

  1. Determine which targets should be visible.
  2. Determine which targets should be hidden.
  3. Apply hide transitions first.
  4. Apply show transitions.

In most cases, show and hide operations are fast. However, they may return promises, for example when animations are involved. In that case, transitions can take noticeable time to complete.

If the state changes again while visibility transitions are still in progress, the engine does not blindly apply each intermediate result. Instead, it queues the latest requested state and waits for the current transitions to finish. Once they complete, it applies only the most recent pending state.

Consider this sequence of states our component enters sequentially:

A -> B -> C

Suppose state A becomes active and its visibility transitions begin to run. For example, A shows and hides targets using CSS animations, so show() and hide() return promises that resolve only when those animations finish.

Now imagine that while A transitions are still in progress, the state changes again to B, and then again to C. In this situation, DisplayStateEngine will not start applying B while A is still in-flight. Instead, it remembers that an update is pending. If another switch happens (C), it overwrites the pending update with the latest one. And once A transitions settle, the engine applies only the most recent pending state.

This means B is skipped entirely and the engine goes straight to C. So effectively, the sequence becomes:

A -> C

This prevents UI thrashing during rapid changes and ensures that the interface always converges to the most recent state, not every intermediate one.

What you write, what Qite does

The important thing to remember is that state rules are declarations. You say:

[ { status: "paid" }, ["#paid", "#shipping"] ]

and Qite interprets it and performs the following under the hood:

  • Listens to field and flag changes.
  • Recomputs active states.
  • Determines which targets should be visible.
  • Runs hide() before show()
  • Skips intermediate states during rapid updates.

In the next section we will look more closely at field matchers, such as "isPresent()", "isBlank()", and membership matchers.