# My first component

Build a **Lit** user card with Tailwind and Concorde UI, then connect it to the **DataProvider** store: declare the **data configuration** (type, key, scope), and let **`@subscribe`** keep the card in sync.

## 1. Lit component + Tailwind

Export Tailwind once (e.g. `src/docs/tailwind.ts` in this repo):

<sonic-code language="typescript">
  <template>
import { css, unsafeCSS } from "lit";
import tailwindImport from "./css/tailwind.css?inline";

export const tailwind = css`${unsafeCSS(tailwindImport)}`;
  </template>
</sonic-code>

User card — plain Lit properties and UI components (no store yet):

<sonic-code language="typescript">
  <template>
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { tailwind } from "../tailwind";

@customElement("docs-user")
export class DocsUser extends LitElement {
  static styles = [tailwind];

  @property({ type: String }) first_name = "";
  @property({ type: String }) last_name = "";
  @property({ type: String }) email = "";
  @property({ type: String }) avatar = "";

  render() {
    return html`&lt;div class="flex items-center gap-3 rounded-md p-2"&gt;
      &lt;sonic-image src=${this.avatar} rounded="md" ratio="1/1" class="w-16"&gt;&lt;/sonic-image&gt;
      &lt;div&gt;
        &lt;div&gt;${this.first_name} &lt;span class="font-bold"&gt;${this.last_name}&lt;/span&gt;&lt;/div&gt;
        &lt;div class="text-sm text-neutral-400"&gt;${this.email}&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;`;
  }
}
  </template>
</sonic-code>

## 2. Data configuration

The card does not hard-code user fields anymore. You declare **what** is stored, **where** it lives, and **which ancestor scope** applies — then `@subscribe` mirrors that object on the component.

### Type — shape of the data

`DocsUserData` documents the fields you expect at this scope (first name, email, avatar, …). TypeScript checks that `user` matches that shape.

<sonic-code language="typescript">
  <template>
export type DocsUserData = {
  first_name: string;
  last_name: string;
  email: string;
  avatar: string;
};
  </template>
</sonic-code>

### Key — path in the DataProvider

`docsUserRowKey` points at the store segment to read. The path `"${dataProvider}"` is resolved at runtime from a property on the component (see scope below).

<sonic-code language="typescript">
  <template>
import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";

export const docsUserRowKey = new DataProviderKey&lt;
  DocsUserData,
  { dataProvider: string | null }
&gt;("${dataProvider}");
  </template>
</sonic-code>

The second generic (`{ dataProvider: string | null }`) lists what the host must expose so `"${dataProvider}"` can be resolved. See [DataProviderKey](/docs/misc/dataProviderKey) and [Dynamic path placeholders](/docs/misc/dynamic-path).

### Scope — which store segment applies

A parent sets `dataProvider=${docsUserScopeAKey.path}` (or a list row sets `…/list-item/0`). [`@ancestorAttribute`](/docs/decorators/ancestor-attribute) copies that attribute onto `this.dataProvider`, so the key resolves to the correct branch.

<sonic-code language="typescript">
  <template>
@ancestorAttribute("dataProvider")
dataProvider: string | null = null;
  </template>
</sonic-code>

### Subscribe — keep the card in sync

[`@subscribe`](/docs/decorators/subscribe) watches `docsUserRowKey` and updates `user` when the store changes. Use `@state()` so Lit re-renders. The template reads `this.user` like any other property.

<sonic-code language="typescript">
  <template>
import { html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import {
  ancestorAttribute,
  subscribe,
} from "@supersoniks/concorde/core/decorators/Subscriber";
import { docsUserRowKey, type DocsUserData } from "./users";
import { tailwind } from "../tailwind";

@customElement("docs-user")
export class DocsUser extends LitElement {
  static styles = [tailwind];

  @ancestorAttribute("dataProvider")
  dataProvider: string | null = null;

  @subscribe(docsUserRowKey)
  @state()
  user: DocsUserData | null = null;

  render() {
    const u = this.user;
    if (!u) return nothing;
    return html`&lt;div class="flex items-center gap-3 rounded-md p-2"&gt;
      &lt;sonic-image src=${u.avatar} rounded="md" ratio="1/1" class="w-16"&gt;&lt;/sonic-image&gt;
      &lt;div&gt;
        &lt;div&gt;${u.first_name} &lt;span class="font-bold"&gt;${u.last_name}&lt;/span&gt;&lt;/div&gt;
        &lt;div class="text-sm text-neutral-400"&gt;${u.email}&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;`;
  }
}
  </template>
</sonic-code>

Live code: `src/docs/example/users.ts`. In this repo, imports use **`core/…` paths** (short `@supersoniks/concorde/…` exports apply in **consumer apps** only).

### Two scopes, one component

The same `docs-user` is mounted twice. Each copy inherits a **different** nearest `dataProvider` — that is the row/list **scope**:

<sonic-code>
  <template>
    <docs-demo-sources for="docs-user-two-scopes"></docs-demo-sources>
    <docs-user-two-scopes></docs-user-two-scopes>
  </template>
</sonic-code>

Markup (two isolated stores; keys from `docs-provider-keys.ts`):

<sonic-code language="markup">
  <template>
    &lt;div class="grid md:grid-cols-2 gap-6"&gt;
      &lt;div dataProvider="${docsUserScopeAKey.path}"&gt;
        &lt;docs-user&gt;&lt;/docs-user&gt;
      &lt;/div&gt;
      &lt;div dataProvider="${docsUserScopeBKey.path}"&gt;
        &lt;docs-user&gt;&lt;/docs-user&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  </template>
</sonic-code>

Seeded in `docs-provider-keys.ts` (`set(docsUserScopeAKey, …)`): **Paul** / **Marie**. Without `@ancestorAttribute`, both cards would not know which store to read.

## 3. Fetch users — `sonic-list` with `.items`

Use a Lit parent and the **`items`** renderer (not HTML `<template>` children). The callback receives each row object from fetch — render fields directly (`${item.first_name}`, …), like porting a template that used `data-bind` / `<sonic-value>`.

<sonic-code>
  <template>
      <docs-lit-demo for="docs-users-list"></docs-lit-demo>
  </template>
</sonic-code>

Wrapper (`docs-users-list.ts`):

<sonic-code language="typescript">
  <template>
import { html, LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import "../../core/components/functional/list/list";
import "./users";

@customElement("docs-users-list")
export class DocsUsersList extends LitElement {
  private items = ({ first_name, last_name, email, avatar }) => html`
    &lt;div class="flex gap-3"&gt;
      &lt;sonic-image src=${avatar} ...&gt;&lt;/sonic-image&gt;
      &lt;div&gt;${first_name} &lt;b&gt;${last_name}&lt;/b&gt;&lt;/div&gt;
      &lt;div class="text-sm text-neutral-400"&gt;${email}&lt;/div&gt;
    &lt;/div&gt;
  `;

  render() {
    return html`
      &lt;sonic-list fetch dataProvider=${usersListEndpoint.path} key="data" .items=${this.items}&gt;&lt;/sonic-list&gt;
    `;
  }
}
  </template>
</sonic-code>

See [Local API demos](/docs/misc/docs-mock-api) for `serviceURL="/docs-mock-api"`.

## 4. Form preview with `formDataProvider`

Edit fields in a form; the card follows the same `docsUserRowKey` when the preview host sets `dataProvider="userPreview"`.

<sonic-code>
  <template>
    <div class="grid grid-cols-1 gap-4 max-w-xl">
      <form formDataProvider="${docsUserPreviewKey.path}" class="grid grid-cols-2 gap-3">
        <sonic-input label="First name" name="first_name" value="Paul" size="sm"></sonic-input>
        <sonic-input label="Last name" name="last_name" value="Metrand" size="sm"></sonic-input>
        <sonic-input class="col-span-2" label="Email" name="email" value="paul@example.com" size="sm"></sonic-input>
        <sonic-input class="col-span-2" label="Avatar URL" name="avatar" value="https://i.pravatar.cc/150?u=paul" size="sm"></sonic-input>
      </form>
      <sonic-divider align="left">Preview</sonic-divider>
      <div dataProvider="${docsUserPreviewKey.path}">
        <docs-user></docs-user>
      </div>
    </div>
  </template>
</sonic-code>

Use `formDataProvider` + `name` on inputs — no manual `@input` handlers ([Data flow](/docs/getting-started/dataFlow)).
