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:
- Hide everything.
- Show only the targets listed by the active state.
There is also a
"blacklist"
mode where the meaning flips:
- Show everything
- 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:
- Determine which targets should be visible.
- Determine which targets should be hidden.
- Apply hide transitions first.
- 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()beforeshow() - 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.