Concorde decorator — @onAssign. > New apps: use @handle with DataProviderKey (Data flow). @onAssign uses untyped string paths; it remains documented for existing codebases — see Migrating to @handle below. Doc ID: docs/_decorators/on-assign. Keywords: Concorde, supersoniks, docs/_decorators/on-assign, on-assign, @onAssign, decorator, onAssign, DataProviderKey, set, get, User n°${userNumber}, user-${userNumber}@example.com. URL: https://concorde.supersoniks.org/crawl/docs/_decorators/on-assign.html.
@onAssign
New apps: use @handle with
DataProviderKey(Data flow).@onAssignuses untyped string paths; it remains documented for existing codebases — see Migrating to @handle below.
The @onAssign decorator allows you to execute a method when one or more publishers are updated. The method is called only when all specified publishers have been assigned values.
For a typed equivalent (recommended), use @handle.
Principle
This decorator subscribes to one or more publishers by string path (legacy). When all specified publishers have been assigned values (via set), the decorated method is called with all the values as arguments. Prefer @handle + DataProviderKey and get / set from Data flow.
This is particularly useful when you need to wait for multiple data sources to be ready before executing logic.
Usage
Import
import { onAssign } from "@supersoniks/concorde/decorators";
Basic example
//...
@customElement("demo-on-assign")
export class DemoOnAssign extends LitElement {
static styles = [tailwind];
@state() userWithSettings: any = null;
@state() isReady: boolean = false;
@state() lastUpdate: string = "";
@onAssign("demoUser", "demoUserSettings")
handleDataReady(user: any, settings: any) {
this.isReady = Object.keys(user).length > 0 && Object.keys(settings).length > 0;
this.userWithSettings = { ...user, ...settings };
this.lastUpdate = new Date().toLocaleTimeString();
this.requestUpdate();
}
render() {
const { name, email, theme, language } = this.userWithSettings;
return //...
}
updateData() {
const user = PublisherManager.get("demoUser");
const userSettings = PublisherManager.get("demoUserSettings");
const userNumber = Math.floor(Math.random() * 100);
user.set({
name: `User n°${userNumber}`,
email: `user-${userNumber}@example.com`,
});
userSettings.set({
theme: ["light", "dark", "auto"][Math.floor(Math.random() * 3)],
language: ["en", "fr", "es"][Math.floor(Math.random() * 3)],
});
}
}
<docs-demo-sources for="demo-on-assign"></docs-demo-sources>
<demo-on-assign></demo-on-assign>
Example with nested paths
@customElement("product-view")
export class ProductView extends LitElement {
product: any = null;
inventory: any = null;
@onAssign("store.product", "store.inventory")
handleProductData(product: any, inventory: any) {
this.product = product;
this.inventory = inventory;
this.requestUpdate();
}
render() {
if (!this.product) return html`<div>Loading...</div>`;
const stock = this.inventory[this.product.id] || 0;
return html`
<div>
<h2>${this.product.name}</h2>
<p>Price: ${this.product.price}€</p>
<p>Stock: ${stock}</p>
</div>
`;
}
}
Path syntax
The path uses dot notation to navigate through the publisher structure:
- First segment: dataProvider identifier (e.g.,
"userData") - Following segments: nested properties (e.g.,
"store.product") - Array access: use numeric index (e.g.,
"data.items.0")
Dynamic path driven by class properties
You can now build the paths dynamically by referencing the host class properties inside the strings passed to @onAssign. Two placeholder syntaxes are supported:
${myProperty}or${this.myProperty}{$myProperty}
Each placeholder is replaced at runtime with the current value of the corresponding property. @onAssign automatically watches those properties and:
- re-evaluates the final paths when one of them changes,
- removes the previous subscriptions before attaching the new ones,
- observe the changes inside
willUpdate(changedProperties)so nothing touches the getters/setters.
While a placeholder is null/undefined, subscriptions are detached. Optional skipEmptyPlaceholder on @handle (typed replacement). Details: Dynamic path placeholders.
@customElement("demo-on-assign-dynamic")
export class DemoOnAssignDynamic extends LitElement {
static styles = [tailwind];
@property({ type: String })
dataProvider: "demoUsers" | "demoUsersAlt" = "demoUsers";
@property({ type: Number })
userIndex: number = 0;
@state() user: any = null;
@state() userSettings: any = null;
@onAssign("${dataProvider}.${userIndex}", "${dataProvider}Settings.${userIndex}")
handleUserDataReady(user: any, settings: any) {
this.user = user;
this.userSettings = settings;
}
updateUserIndex(e: Event) {
this.userIndex = parseInt((e.target as HTMLInputElement).value);
}
updateDataProvider(e: Event) {
this.dataProvider = (e.target as HTMLSelectElement).value as
| "demoUsers"
| "demoUsersAlt";
}
updateCurrentUserData() {
const usersPublisher = PublisherManager.get(this.dataProvider);
const settingsPublisher = PublisherManager.get(
`${this.dataProvider}Settings`
);
const userPublisher = Objects.traverse(
usersPublisher,
[String(this.userIndex)]
) as PublisherProxy;
const settingPublisher = Objects.traverse(
settingsPublisher,
[String(this.userIndex)]
) as PublisherProxy;
if (userPublisher && settingPublisher) {
// Générer de nouvelles données aléatoires
const randomNames = [
{ firstName: "Alice", lastName: "Wonder" },
{ firstName: "Bob", lastName: "Builder" },
{ firstName: "Charlie", lastName: "Chaplin" },
];
const randomThemes = ["light", "dark", "auto"];
const randomLanguages = ["en", "fr", "es"];
const randomName =
randomNames[Math.floor(Math.random() * randomNames.length)];
const randomEmail = `${randomName.firstName.toLowerCase()}.${randomName.lastName.toLowerCase()}@example.com`;
const randomTheme =
randomThemes[Math.floor(Math.random() * randomThemes.length)];
const randomLanguage =
randomLanguages[Math.floor(Math.random() * randomLanguages.length)];
// Mettre à jour l'utilisateur directement
const currentUser = userPublisher.get() || {};
userPublisher.set({
...currentUser,
firstName: randomName.firstName,
lastName: randomName.lastName,
email: randomEmail,
});
// Mettre à jour les settings directement
settingPublisher.set({
theme: randomTheme,
language: randomLanguage,
});
}
}
render() {
return html`
<div class="flex flex-col gap-2">
<sonic-select label="Users set" @change=${this.updateDataProvider}>
<option value="demoUsers">First set of users</option>
<option value="demoUsersAlt">Second set of users</option>
</sonic-select>
<sonic-input
type="number"
.value=${this.userIndex}
@input=${this.updateUserIndex}
min="0"
max="9"
label="Index"
class="block"
></sonic-input>
<sonic-button @click=${this.updateCurrentUserData}>
Update current user data
</sonic-button>
<div class="flex flex-col gap-2 border p-2">
<div>
<sonic-icon name="user" library="heroicons"></sonic-icon>
${this.user?.firstName} ${this.user?.lastName}
</div>
<div>
<sonic-icon name="envelope" library="heroicons"></sonic-icon>
${this.user?.email}
</div>
<div>
Theme: ${this.userSettings?.theme} | Language:
${this.userSettings?.language}
</div>
</div>
</div>
`;
}
}
<docs-demo-sources for="demo-on-assign-dynamic"></docs-demo-sources>
<demo-on-assign-dynamic></demo-on-assign-dynamic>
⚠️ Use classic string literals:
@onAssign("${dataProvider}.${profileId}", "settings.${profileId}"). Do not use template literals (backticks), otherwise JavaScript would try to interpolate the value immediately.
Additional constraints:
- The hosting class must expose a
willUpdate(changedProperties?: Map<PropertyKey, unknown>)method (LitElement already provides it) so that@onAssigncan listen to dependency changes. - Dependencies need to be reactive (e.g.
@property()on LitElement) or you must callthis.requestUpdate("myProp")manually after changing them, otherwisewillUpdatewill never be notified. - If you use nested expressions like
${user.id}, the first segment (user) is the one being observed: you need to reassignthis.user(e.g. with a new object) so that the binding can detect the change.
Behavior
- The method is called only when all specified publishers have been assigned values
- The method receives the values of all publishers as arguments, in the same order as specified
- Subscription happens at the time of
connectedCallback - Unsubscription happens automatically at the time of
disconnectedCallback - If a publisher doesn't exist yet, it will be created with the value
null - The method is called with
thisbound to the component instance
Use cases
This decorator is particularly useful for:
- Waiting for multiple data sources before rendering or executing logic
- Synchronizing data from different publishers
- Initializing components that depend on multiple data providers
- Handling complex data dependencies where you need all data to be ready
Complete example
import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { onAssign } from "@supersoniks/concorde/decorators";
import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
@customElement("order-summary")
export class OrderSummary extends LitElement {
order: any = null;
customer: any = null;
shipping: any = null;
@onAssign("orderData", "customerData", "shippingData")
handleOrderReady(order: any, customer: any, shipping: any) {
this.order = order;
this.customer = customer;
this.shipping = shipping;
this.requestUpdate();
}
render() {
if (!this.order || !this.customer || !this.shipping) {
return html`<div>Loading order details...</div>`;
}
return html`
<div class="order-summary">
<h2>Order #${this.order.id}</h2>
<p>Customer: ${this.customer.name}</p>
<p>Shipping to: ${this.shipping.address}</p>
<p>Total: ${this.order.total}€</p>
</div>
`;
}
}
// Somewhere in your code, update the publishers:
const orderPub = PublisherManager.get("orderData");
const customerPub = PublisherManager.get("customerData");
const shippingPub = PublisherManager.get("shippingData");
// The method will be called only when all three are set:
orderPub.set({ id: "123", total: 99.99 });
customerPub.set({ name: "John Doe", email: "john@example.com" });
shippingPub.set({ address: "123 Main St" });
// handleOrderReady will be called with all three values
Migrating to @handle
@handle is the typed successor of @onAssign. The key behavioral difference: @onAssign waits for all values to be defined before calling the method, whereas @handle calls it on every assignment by default. Use the waitForAllDefined option to keep the old semantics.
Why migrate
- Typed paths: keys are
DataProviderKey<T>, so the method arguments are strongly typed (no moreany). - Explicit intent:
waitForAllDefinedandskipreplace implicit behavior. - Single API:
@handlecovers the mono- and multi-path cases (up to 3 keys).
Equivalent semantics (waitForAllDefined)
// Before
@onAssign("demoUser", "demoUserSettings")
handleDataReady(user: any, settings: any) { /* ... */ }
// After — same "wait for everything" behavior, but typed
const user = new DataProviderKey<User>("demoUser");
const settings = new DataProviderKey<Settings>("demoUserSettings");
@handle(user, settings, { waitForAllDefined: true })
handleDataReady(user: User, settings: Settings) { /* ... */ }
Single path
// Before
@onAssign("settings.modules.logs_route.enabled")
onLogRoute(value: boolean) { /* ... */ }
// After
const settings = new DataProviderKey<AppSettings>("settings");
@handle(settings.modules.logs_route.enabled)
onLogRoute(value: boolean) { /* ... */ }
4+ paths
@handle is capped at 3 keys. For the rare case of 4 or more publishers, keep @onAssign for now, or split the logic into several @handle methods that each store their value and call a shared method (guarding against partial values).
Notes
- This decorator works with any component that has
connectedCallbackanddisconnectedCallbackmethods (such asLitElementor components extendingSubscriber) - The method is called synchronously when all publishers are ready
- If you need to update the UI, remember to call
this.requestUpdate()inside the method - For more information about publishers, see the documentation on Sharing data