# @onAssign

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.

## Principle

This decorator subscribes to one or more publishers via the `PublisherManager`. When all specified publishers have been assigned values (via `set`), the decorated method is called with all the values as arguments.

This is particularly useful when you need to wait for multiple data sources to be ready before executing logic.

## Usage

### Import

<sonic-code language="typescript">
  <template>
import { onAssign } from "@supersoniks/concorde/decorators";
  </template>
</sonic-code>

### Basic example 


<sonic-code language="typescript">
  <template>
//...
@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)],
    });
  }
}
  </template>
</sonic-code>

<sonic-code>
  <template>
    <demo-on-assign></demo-on-assign>
  </template>
</sonic-code>


### Example with nested paths

<sonic-code language="typescript">
  <template>
@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>
    `;
  }
}
  </template>
</sonic-code>

## 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.


<sonic-code language="typescript">
  <template>
@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>
    `;
  }
}
  </template>
</sonic-code>

<sonic-code>
  <template>
    <demo-on-assign-dynamic></demo-on-assign-dynamic>
  </template>
</sonic-code>

> ⚠️ 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 `@onAssign` can listen to dependency changes.
- Dependencies need to be reactive (e.g. `@property()` on LitElement) or you must call `this.requestUpdate("myProp")` manually after changing them, otherwise `willUpdate` will never be notified.
- If you use nested expressions like `${user.id}`, the first segment (`user`) is the one being observed: you need to reassign `this.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 `this` bound 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

<sonic-code language="typescript">
  <template>
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
  </template>
</sonic-code>

## Notes

- This decorator works with any component that has `connectedCallback` and `disconnectedCallback` methods (such as `LitElement` or components extending `Subscriber`)
- 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](#docs/_getting-started/pubsub.md/pubsub)

