Skip to content

UI State

Minified: 6.23 KBGzip: 2.63 KBBrotli: 2.32 KBZstd: 2.69 KB

wcp-ui-state brings reactive state to plain HTML. You declare named values called stores, bind them to elements in the DOM, and the page updates itself. No framework required.

Before using any state component, import wcp-expression-parser.
Bindings and derived stores depend on it to extract store references statically from expressions. Without it, nothing reactive will work.

Stores

A store is a named value that the rest of the system can observe. Any binding or derivation that references a store will re-evaluate automatically when that store changes.

Important

Stores are registered once, when the component first connects. Adding or removing
<wcp-store> elements after that point has no effect.

Stores are declared with <wcp-store> inside a <wcp-ui-state> container:

<wcp-ui-state global>
<wcp-store name="$count" type="number" value="0"></wcp-store>
<wcp-store name="$colorScheme" value="light" persistent></wcp-store>
<wcp-store name="$priceText" derive="$price + '$'"></wcp-store>
</wcp-ui-state>
AttributeDefaultDescription
nameRequired. The identifier used to reference this store in bindings and expressions.
value""Initial value of the store as a string. Parsed according to type.
type"string"How value is parsed at initialisation. One of string, number, boolean, json. Has no effect after that.
deriveAn expression whose result becomes the store’s value. Recomputes whenever any referenced store changes.
persistent"localStorage" when presentPersists the store across page loads. Accepts localStorage or sessionStorage. Restores the saved value on load if one exists.

Writing to a store

In any expression, you can update a store by calling it with a new value:

$count($count + 1);
$colorScheme("dark");

Important

Never write to a derived store. Its value is managed by its expression and will be
overwritten on the next dependency change.

Avoid cycles too: if store A triggers store B, and B derives from A, the system will loop indefinitely.

Bindings

Bindings are attributes you place on any element in case of a global <wcp-ui-state>, or that descends from it in an isolated <wcp-ui-state>.
They keep DOM properties, attributes, classes, styles, and text in sync with store values.

When a binding accepts multiple declarations, separate them with ;:

<div bind-attr="aria-label:$label; tabindex:$tabIndex"></div>

bind-prop

Sets a DOM property directly. Use this for value, checked, and similar properties where the HTML attribute and the live property diverge after initial render.

Syntax: bind-prop="[property]:[expression]; ..."

<input type="number" bind-prop="value:$count" />

bind-attr

Sets an HTML attribute to the result of an expression.

Syntax: bind-attr="[attribute]:[expression]; ..."

<input bind-attr="placeholder:$hint; aria-label:$label" />

bind-attr-toggle

Adds or removes a boolean attribute based on whether the expression is truthy.
Because boolean attributes work by presence rather than value, assigning "false" as a string would not remove them.
This binding handles the distinction correctly.

For attributes like aria-checked that take the string "true" or "false", use bind-attr instead.

Syntax: bind-attr-toggle="[attribute]:[expression]; ..."

<button bind-attr-toggle="disabled:$isSubmitting">Submit</button>

bind-class-toggle

Adds or removes a fixed class name based on whether the expression is truthy.

Syntax: bind-class-toggle="[class]:[expression]; ..."

<nav bind-class-toggle="is-open:$menuOpen; is-sticky:$scrolled"></nav>

bind-class

Sets a class name dynamically from an expression. Use this when the class name itself comes from state, not just whether it is applied.

Syntax: bind-class="[expression]"

<div bind-class="$currentTheme"></div>

bind-style

Sets a CSS property from an expression. Most useful for CSS custom properties.

Syntax: bind-style="[css-property]:[expression]; ..."

<ul bind-style="--indent:$depth"></ul>

bind-show

Shows or hides an element. When the expression is falsy, the element receives display: none. When it becomes truthy again, the previous display value is restored.

Syntax: bind-show="[expression]"

<div class="error" bind-show="$hasError"></div>

bind-text

Replaces the text content of an element with the result of an expression.

Syntax: bind-text="[expression]"

<span bind-text="$username"></span>

Events

bind-event runs an expression when a DOM event fires.
The special identifier EVENT refers to the native event object.

Syntax: bind-event="[event].[modifiers]:[expression]; ..."

<input bind-event="input:$query(EVENT.target.value)" />

Supported modifiers are prevent (preventDefault), stop (stopPropagation), once, capture, and passive.

Multiple event declarations on the same element are separated with ;:

<input bind-event="focus:$focused(true); blur:$focused(false)" />

Tip

Updating multiple stores in one handler by using sequence expression:

<button bind-event="click:$count($count + 1), $dirty(true)">Save</button>

JavaScript API

For anything that goes beyond what is practical to express in HTML, retrieve a store by name and read or write it directly:

const $count = uiState.getStore("$count");
$count.set($count.get() + 1);

If an expression in a binding starts feeling complex, that is a good signal to move the logic into a derived store registered from JavaScript rather than embedding it in the markup.

Scopes

Every <wcp-ui-state> defines a scope. Stores declared inside it are only visible to elements within that same component. Adding the global attribute makes stores available to the entire page instead.

When a binding looks up a store, it walks up the DOM from the bound element and checks each wcp-ui-state ancestor in order. The first scope that defines the store wins, with global state as the fallback.

Warning

Global components must appear higher in the DOM than any local component that depends on them.
Local state binds eagerly and resolves stores by walking up from its position — if a global
component hasn’t connected yet, its stores won’t be found.

A local scope can shadow a parent store by declaring one with the same name. Only descendants of that scope see the shadowed value.

This makes isolated scopes well-suited for repeating UI patterns that each need independent state:

<!-- Global stores available page-wide -->
<wcp-ui-state global>
<wcp-store name="$theme" value="light"></wcp-store>
</wcp-ui-state>
<!-- Outer scope: manages its own $open, inherits $theme from global -->
<wcp-ui-state>
<wcp-store name="$open" type="boolean" value="false"></wcp-store>
<button bind-event="click:$open(!$open)">Toggle</button>
<div bind-show="$open">
<span bind-text="$theme"></span>
<!-- Inner scope: shadows $open independently from the outer one -->
<wcp-ui-state>
<wcp-store name="$open" type="boolean" value="false"></wcp-store>
<button bind-event="click:$open(!$open)">Toggle inner</button>
<div bind-show="$open">Inner panel</div>
</wcp-ui-state>
</div>
</wcp-ui-state>

Note

The global attribute is read once when the element connects. Changing it later has no effect.

Not planned

innerHTML binding. Writing raw HTML through a binding is a cross-site scripting risk. Use JavaScript directly if you need to set innerHTML.

Rendering elements from state. Conditionally rendering or repeating elements is the job of a template engine or framework. This component is designed to react to state on elements that already exist in the DOM, not to generate markup. JavaScript handles the use case well enough for the cases where it is needed.

API References

Methods

bindElements()

Scans scoped elements for binding attributes and wires them to their stores. Binding attributes are removed from the DOM
after processing.

Safe to call incrementally when new elements are injected at runtime. Already-processed elements (whose attributes have been
stripped) are naturally skipped.

getStore()

Returns the store registered by name, or undefined if it hasn’t been added yet.

Type: <T>(storeName: string) => Store<T> | undefined

createStore()

Creates a new standalone store with the given initial value.

Does not register it with this component.

Type: <T>(initialValue: T) => Store<T>

Properties

global

Marks the state as global. Should be set only at component initialization.

Accessors: Get, Set
Type: boolean | null
Default: null

Attributes

"global"

This is a static attribute only can be set once to mark the state as global.

Type: boolean