# @bind

The `@bind` decorator automatically binds a class property to a path in a publisher. The property will be automatically 
updated when the publisher's data changes.

If you need to trigger the rendering lifecycle of a LitElement, please also add the `@state()` decorator to the property.

## Principle

This decorator subscribes to a publisher via the `PublisherManager` using a path (dot notation) to access a specific property. When this property is modified in the publisher, the decorated property is automatically updated.

## Usage

### Import

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

### Basic example

<sonic-code language="typescript">
<template>
@customElement("demo-bind")
export class DemoBind extends LitElement {
  static styles = [tailwind];
  //
  @bind("demoData.firstName")
  @state()
  firstName = "";
  //
  @bind("demoData.lastName")
  @state()
  lastName: string = "";
  //
  @bind("demoData.count")
  @state()
  count: number = 0;
  //
  render() {
    return //......
  }
  //
  updateData() {
    const demoData = PublisherManager.get("demoData");
    const demoUsers = PublisherManager.get("demoUsers");
    const randomIndex = Math.floor(Math.random() * demoUsers.get().length);
    const randomUser = demoUsers.get()[randomIndex];
    demoData.set({
      firstName: randomUser.firstName,
      lastName: randomUser.lastName,
      count: (demoData.count.get() || 0) + 1,
    });
  }
}
</template>
</sonic-code>

<sonic-code>
  <template>
    <demo-bind></demo-bind>
  </template>
</sonic-code>


## Reflect (liaison bidirectionnelle)

Lorsque vous avez besoin que les modifications locales se propagent également vers le publisher, activez l'option `reflect` :

<sonic-code language="typescript">
  <template>
@bind("userData.profile.avatarUrl", { reflect: true })
@state()
avatar: string;
  </template>
</sonic-code>

- `reflect: true` crée un getter/setter qui garde les descripteurs existants et synchronise la valeur avec le publisher.
- Les mises à jour provenant du publisher sont protégées par un flag interne afin d'éviter les boucles infinies.
- Toute écriture sur la propriété décorée (ex. saisie utilisateur, mise à jour durant `render`) déclenche un `publisher.set(...)`.
- Pratique pour implémenter des formulaires pilotés par les publishers sans écrire manuellement la logique de synchronisation.


<sonic-code language="typescript">
  <template>
@customElement("demo-bind-reflect")
export class DemoBindReflect extends LitElement {
  static styles = [tailwind];
//
  @bind("bindReflectDemo.count", { reflect: true })
  @state()
  withReflect: number = 0;
//
  @bind("bindReflectDemo.count")
  @state()
  withoutReflect: number = 0;
//
  render() {
    return html`
      <div class="mb-3">
        from publisher : ${sub("bindReflectDemo.count") || 0} <br />
        from component with reflect : ${this.withReflect || 0} <br />
        from component without reflect : ${this.withoutReflect || 0}
      </div>
      <sonic-button @click=${() => this.withReflect++}
        >Increment with reflect</sonic-button
      >
      <sonic-button @click=${() => this.withoutReflect++}
        >Increment without reflect</sonic-button
      >
    `;
  }
}
  </template>
</sonic-code>

<sonic-code toggleCode>
  <template>
<demo-bind-reflect></demo-bind-reflect>
  </template>
</sonic-code>


### Complex path

<sonic-code language="typescript">
  <template>
@customElement("user-profile")
export class UserProfile extends LitElement {
  // Access to a simple property
  @bind("cart")
  @state()
  cart: object;
  //
  // Access to a nested property
  @bind("userData.profile.email")
  @state()
  userEmail: string;
  //
  // Access to an array element
  @bind("userData.addresses.0.city")
  @state()
  primaryCity: string;
  //
  render() {
    return html`
      <div>
        <p>Number of items : ${this.cart.items.length}</p>
        <p>Email: ${this.userEmail}</p>
        <p>City: ${this.primaryCity}</p>
      </div>
    `;
  }
}
  </template>
</sonic-code>

## Path syntax

The path uses dot notation to navigate through the publisher structure:

- **First segment**: dataProvider identifier (e.g., `"myDataProvider"`)
- **Following segments**: nested properties (e.g., `"myDataProvider.user.profile"`)
- **Array access**: use numeric index (e.g., `"myDataProvider.items.0"`)



### Dynamic path driven by class properties

You can now build the path dynamically by referencing the host class properties inside the string passed to `@bind`. Two placeholder syntaxes are supported:

- `` ${myProperty} `` or `` ${this.myProperty} ``
- `` {$myProperty} ``

Each placeholder is replaced at runtime with the current value of the corresponding property. `@bind` automatically watches those properties and:

- re-evaluates the final path when one of them changes,
- removes the previous subscription before attaching the new one,
- keeps working with `reflect: true`.
- observe the changes inside `willUpdate(changedProperties)` so nothing touche aux getters/setters.


<sonic-code language="typescript">
  <template>
@customElement("demo-bind-dynamic")
export class DemoBindDynamic extends LitElement {
  static styles = [tailwind];
  //
  @property({ type: String })
  dataProvider: "demoUsers" | "demoUsersAlt" = "demoUsers";
  //
  @property({ type: Number })
  userIndex: number = 0;
  //
  @bind("${dataProvider}.${userIndex}")
  @state()
  user: any = {};
  //
  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 userPublisher = Objects.traverse(
      usersPublisher,
      [String(this.userIndex)]
    ) as PublisherProxy;

    if (userPublisher) {
      // Générer de nouvelles données aléatoires
      const randomNames = [
        { firstName: "Alice", lastName: "Wonder" },
        { firstName: "Bob", lastName: "Builder" },
        { firstName: "Charlie", lastName: "Chaplin" },
      ];

      const randomName =
        randomNames[Math.floor(Math.random() * randomNames.length)];
      const randomEmail = `${randomName.firstName.toLowerCase()}.${randomName.lastName.toLowerCase()}@example.com`;

      // Mettre à jour l'utilisateur directement
      const currentUser = userPublisher.get() || {};
      userPublisher.set({
        ...currentUser,
        firstName: randomName.firstName,
        lastName: randomName.lastName,
        email: randomEmail,
      });
    }
  }
  //
  render() {
    return html`
      <div class="flex flex-col gap-2">
        <sonic-select
          .value=${this.dataProvider}
          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>
      </div>
    `;
  }

  </template>
</sonic-code>

<sonic-code>
  <template>
    <demo-bind-dynamic></demo-bind-dynamic>
  </template>
</sonic-code>

> ⚠️ Use a classic string literal: `@bind("${dataProvider}.${profileId}.info.title")`. Do **not** use a template literal (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 le fournit) so that `@bind` peut écouter les changements de dépendances.
- Dependencies need to be reactive (e.g. `@property()` on LitElement) or you must call `this.requestUpdate("myProp")` manually after changing them, otherwise `willUpdate` ne sera jamais notifié.
- 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 property is automatically updated when the publisher's data changes
- Subscription happens at the time of `connectedCallback`
- Unsubscription happens automatically at the time of `disconnectedCallback`
- If the path doesn't exist yet, a publisher is created with the value `null`
- The value will be updated as soon as the data becomes available


## Use cases

This decorator is particularly useful for:

- **Binding properties** to specific data in a publisher
- **Accessing sub-properties** without having to manually write subscription logic
- **Simplifying code** by avoiding repetitive calls to `PublisherManager.get()` and `onAssign()`

## Complete example

<sonic-code language="typescript">
  <template>
import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { bind } from "@supersoniks/concorde/decorators";
import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
//
@customElement("product-card")
export class ProductCard extends LitElement {
  @bind("productData.name")
  @state()
  productName: string;
  //
  @bind("productData.price")
  @state()
  price: number;
  //
  @bind("productData.description")
  @state()
  description: string;
  //
  render() {
    return html`
      <div class="product-card">
        <h2>${this.productName}</h2>
        <p class="price">${this.price}€</p>
        <p>${this.description}</p>
      </div>
    `;
  }
}
// Somewhere in your code, update the data:
const productPublisher = PublisherManager.get("productData");
productPublisher.set({
  name: "Example product",
  price: 29.99,
  description: "A product description"
});
// The component properties will be automatically updated
  </template>
</sonic-code>

## Notes

- This decorator works with any component that has `connectedCallback` and `disconnectedCallback` methods (such as `LitElement` or components extending `Subscriber`)
- Updates are reactive: any modification to the publisher triggers a property update
- For more information about publishers, see the documentation on [Sharing data](#docs/_getting-started/pubsub.md/pubsub)
