Concorde decorator — @handle. Typed callback on one or more DataProviderKey<T> paths: invokes the decorated method when a publisher assigns a value (calculations, side effects, updating other @state properties, etc.). Doc ID: docs/_decorators/handle. Keywords: Concorde, supersoniks, docs/_decorators/handle, handle, @handle, decorator, DataProviderKey, @state, HandleOptions, null, undefined, @bind, @subscribe, ${user.firstName} ${user.lastName}. URL: https://concorde.supersoniks.org/crawl/docs/_decorators/handle.html.
@handle
Typed callback on one or more DataProviderKey<T> paths: invokes the decorated method when a publisher assigns a value (calculations, side effects, updating other @state properties, etc.).
Unlike @subscribe, nothing is bound to the decorated member — only your method runs. @handle is typed and accepts up to 3 keys plus an optional trailing HandleOptions object. It supersedes the string-based @onAssign.
By default the method is called on every assignment, even when the value is null / undefined. Use the options below to restrict that.
Import
import { handle, Skip } from "@supersoniks/concorde/decorators";
import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
import { get, set } from "@supersoniks/concorde/utils";
Basic example
type DemoCounterData = { count: number };
const demoDataKey = new DataProviderKey<DemoCounterData>("demoData");
@customElement("demo-handle")
export class DemoHandle extends LitElement {
@state() doubled = 0;
@state() lastUpdate = "";
@handle(demoDataKey.count)
onCountChange(count: number) {
this.doubled = count * 2;
this.lastUpdate = new Date().toLocaleTimeString();
}
incrementCount() {
const data = get(demoDataKey);
set(demoDataKey, { ...data, count: data.count + 1 });
}
render() {
return html`
<p>Doubled count: ${this.doubled}</p>
<p><small>Last update: ${this.lastUpdate}</small></p>
<sonic-button @click=${this.incrementCount}>Increment</sonic-button>
`;
}
}
<docs-demo-sources for="demo-handle"></docs-demo-sources>
<demo-handle></demo-handle>
Dynamic path
Placeholders in DataProviderKey resolve from the host component’s properties (same rules as @bind / @subscribe).
type User = { firstName: string; lastName: string; email: string };
@customElement("demo-handle-dynamic")
export class DemoHandleDynamic extends LitElement {
@property({ type: Number })
userIndex = 0;
@state() displayName = "";
@state() lastUpdate = "";
@handle(new DataProviderKey<User, { userIndex: number }>("demoUsers.${userIndex}"))
onUserAssigned(user: User) {
this.displayName = `${user.firstName} ${user.lastName}`;
this.lastUpdate = new Date().toLocaleTimeString();
}
render() {
return html`...`;
}
}
<docs-demo-sources for="demo-handle-dynamic"></docs-demo-sources>
<demo-handle-dynamic></demo-handle-dynamic>
Multiple paths
@handle accepts up to 3 keys; the method receives one strongly-typed argument per key, in order. Each assignment triggers the method, so make your method safe against partial values (or use waitForAllDefined, see below).
type QueueConfig = { onInactivity: { stillHere: { show: boolean } } };
const config = new DataProviderKey<QueueConfig>("sessionQueueConfig");
const idle = new DataProviderKey<{ isIdle: boolean }>("idleStatus");
@customElement("demo-handle-multi")
export class DemoHandleMulti extends LitElement {
// show: boolean, isIdle: boolean — fully typed from the keys
@handle(config.onInactivity.stillHere.show, idle.isIdle)
onInactivity(show: boolean, isIdle: boolean) {
if (show === true && isIdle === true) this.openModal();
else this.closeModal();
}
}
Options (HandleOptions)
Pass an options object as the last argument. The names map to real situations seen in the apps.
waitForAllDefined
Only call the method once all watched keys are defined (non null / undefined). This reproduces the historical @onAssign semantics — use it when the logic only makes sense with every source ready (e.g. building a date from date + timeZone + direction).
@handle(trip.departureDate, trip.event.timeZone, form.direction, {
waitForAllDefined: true,
})
updateDepartureDate(date: number, timeZone: string, direction: string) {
// called only when the three values are all available
this.formDate = formatDate(date, timeZone, direction);
}
skip
Do not call the method when a received value belongs to one of the listed categories (the Skip enum). Each entry is a named category — not a value — so there is no value/pattern ambiguity (e.g. {} is Skip.EmptyObject, an explicit "empty object" category, never a value comparison).
| Category | Matches |
|---|---|
Skip.Nullish |
null or undefined (a publisher always emits null, never undefined) |
Skip.EmptyString |
"" |
Skip.EmptyObject |
object with no keys ({}), arrays excluded |
Skip.EmptyArray |
empty array ([]) |
Useful when a not-yet-initialized publisher emits {} as a "loading" state. For a specific value (e.g. a particular string), guard inside the method instead.
@handle(user.profile, { skip: [Skip.Nullish, Skip.EmptyObject] })
onProfile(profile: Profile) {
// not called while the publisher still holds {} (not loaded yet)
this.displayName = profile.firstName;
}
Options can be combined, e.g.
@handle(a, b, { waitForAllDefined: true, skip: [Skip.Nullish] }). For any arbitrary validation on a specific value, just guard inside the method (if (!isValid(v)) return;) — that is exactly what anaccept-style predicate would do, since@handleonly runs your method.
skipEmptyPlaceholder
On a dynamic key path ("items.${itemId}"), if true, a placeholder resolved to '' is treated as not ready (no subscription) — empty string only, not 0 or false. See Dynamic path placeholders.
Highlights
- Strict typing: the method receives one argument per key, in order.
- Up to 3 keys; for 4+ keys (rare), keep @onAssign for now.
- By default the method runs on every assignment, even with
null/undefined(unlike@onAssign, which waits for all values). Opt back into that behavior withwaitForAllDefined. skipfilters out values by named category (e.g.[Skip.Nullish, Skip.EmptyObject]); for arbitrary checks on a specific value, guard inside the method.
See also DataProviderKey and Dynamic path placeholders.