import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { bind, ancestorAttribute, onAssign, autoSubscribe, awaitConnectedAncestors, dispatchConnectedEvent, } from "@supersoniks/concorde/decorators"; import { sub } from "@supersoniks/concorde/directives"; import { PublisherManager, PublisherProxy, } from "@supersoniks/concorde/core/utils/PublisherProxy"; import { Objects } from "@supersoniks/concorde/utils"; import { tailwind } from "../tailwind"; const demoSectionClasses = "my-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-0 dark:bg-neutral-900/40 px-6 py-6 md:px-8 md:py-8 space-y-4 shadow-sm"; const ensurePublisherValue = ( publisherId: string, defaultValue: T ): void => { const publisher = PublisherManager.get(publisherId); const currentValue = typeof publisher.get === "function" ? publisher.get() : undefined; const isUnset = currentValue === undefined || currentValue === null || (typeof currentValue === "object" && !Array.isArray(currentValue) && Object.keys(currentValue).length === 0); if (isUnset) { publisher.set(defaultValue as T); } }; const initializeDecoratorsDemoData = () => { ensurePublisherValue("demoData", { title: "Initial Title", user: { name: "Initial User" }, count: 0, }); ensurePublisherValue("demoUser", { name: "Demo User", email: "demo@example.com", }); ensurePublisherValue("demoSettings", { theme: "light", language: "en", }); ensurePublisherValue("autoValue1", 10); ensurePublisherValue("autoValue2", 20); ensurePublisherValue("waitAncestorDemo", { message: "Context from ancestor" }); ensurePublisherValue("combinedData", { title: "Combined Title" }); ensurePublisherValue("combinedUser", { name: "Combined User" }); ensurePublisherValue("combinedSettings", { theme: "dark" }); ensurePublisherValue("reflectData", { title: "Initial Reflected Title", count: 0, }); ensurePublisherValue("dynamicProfiles", { alpha: { info: { title: "Profil Alpha" } }, beta: { info: { title: "Profil Beta" } }, }); ensurePublisherValue("dynamicProfilesAlt", { alpha: { info: { title: "Profil Alpha (Alt)" } }, beta: { info: { title: "Profil Beta (Alt)" } }, }); ensurePublisherValue("demoUsers", [ { id: 1, firstName: "Alice", lastName: "Smith", email: "alice.smith@example.com", }, { id: 2, firstName: "Bob", lastName: "Johnson", email: "bob.johnson@example.com", }, { id: 3, firstName: "Carol", lastName: "Williams", email: "carol.williams@example.com", }, { id: 4, firstName: "David", lastName: "Brown", email: "david.brown@example.com", }, { id: 5, firstName: "Eve", lastName: "Jones", email: "eve.jones@example.com", }, { id: 6, firstName: "Frank", lastName: "Garcia", email: "frank.garcia@example.com", }, { id: 7, firstName: "Grace", lastName: "Miller", email: "grace.miller@example.com", }, { id: 8, firstName: "Henry", lastName: "Davis", email: "henry.davis@example.com", }, { id: 9, firstName: "Ivy", lastName: "Martinez", email: "ivy.martinez@example.com", }, { id: 10, firstName: "Jack", lastName: "Taylor", email: "jack.taylor@example.com", }, ]); ensurePublisherValue("demoUsersAlt", [ { id: 11, firstName: "Sophie", lastName: "Lindquist", email: "sophie.lindquist@example.com", }, { id: 12, firstName: "Mateo", lastName: "Ortega", email: "mateo.ortega@example.com", }, { id: 13, firstName: "Jin", lastName: "Park", email: "jin.park@example.com", }, { id: 14, firstName: "Fatima", lastName: "El-Sayed", email: "fatima.el-sayed@example.com", }, { id: 15, firstName: "Lars", lastName: "Johansson", email: "lars.johansson@example.com", }, { id: 16, firstName: "Amara", lastName: "Singh", email: "amara.singh@example.com", }, { id: 17, firstName: "Zuri", lastName: "Okafor", email: "zuri.okafor@example.com", }, { id: 18, firstName: "Luca", lastName: "Rossi", email: "luca.rossi@example.com", }, { id: 19, firstName: "Ava", lastName: "Murphy", email: "ava.murphy@example.com", }, { id: 20, firstName: "Noah", lastName: "Keller", email: "noah.keller@example.com", }, ]); ensurePublisherValue("demoUsersSettings", [ { theme: "light", language: "en" }, { theme: "dark", language: "fr" }, { theme: "auto", language: "es" }, { theme: "light", language: "en" }, { theme: "dark", language: "fr" }, { theme: "auto", language: "es" }, { theme: "light", language: "en" }, { theme: "dark", language: "fr" }, { theme: "auto", language: "es" }, { theme: "light", language: "en" }, ]); ensurePublisherValue("demoUsersAltSettings", [ { theme: "dark", language: "de" }, { theme: "light", language: "it" }, { theme: "auto", language: "ja" }, { theme: "dark", language: "pt" }, { theme: "light", language: "ru" }, { theme: "auto", language: "zh" }, { theme: "dark", language: "ar" }, { theme: "light", language: "sv" }, { theme: "auto", language: "nl" }, { theme: "dark", language: "pl" }, ]); }; initializeDecoratorsDemoData(); /** * Demo component showcasing @ancestorAttribute decorator */ @customElement("demo-ancestor-attribute") export class DemoAncestorAttribute extends LitElement { @ancestorAttribute("dataProvider") dataProvider: string | null = null; @ancestorAttribute("testAttribute") testAttribute: string | null = null; render() { return html`

DataProvider property initialized from ancestor: ${this.dataProvider || "null"}

TestAttribute property initialized from ancestor: ${this.testAttribute || "null"}

`; } } /** * Demo component showcasing @bind decorator */ @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 html`
Update Data

Title: ${this.firstName || "Not set"}

User Name: ${this.lastName || "Not set"}

Number of updates: ${this.count}

`; } 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, }); } } /** * Demo component showcasing @bind decorator with reflect option */ @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; // initialize the publisher data connectedCallback() { super.connectedCallback(); this.resetData(); } resetData() { PublisherManager.get("bindReflectDemo").set({ count: 0 }); } render() { return html`
from publisher : ${sub("bindReflectDemo.count")}
from component with reflect : ${this.withReflect}
from component without reflect : ${this.withoutReflect}
this.withReflect++} >Increment with reflect this.withoutReflect++} >Increment without reflect Reset publisher data `; } } /** * Demo component showcasing @bind decorator with dynamic dataProvider/id */ @customElement("demo-bind-dynamic") export class DemoBindDynamic extends LitElement { static styles = [tailwind]; @property({ type: String }) dataProvider: "demoUsers" | "demoUsersAlt" = "demoUsers"; @property({ type: Number }) userIndex: number = 1; @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" }, { firstName: "Diana", lastName: "Prince" }, { firstName: "Eve", lastName: "Adams" }, ]; 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`
Update current user data
${this.user?.firstName} ${this.user?.lastName}
${this.user?.email}
`; } } /** * Demo component showcasing @onAssign decorator */ @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 html`
Update Data
${this.isReady === false ? html`

No settings yet...

` : html`

${name || "N/A"}

${email || "N/A"}

${theme || "N/A"}

${language || "N/A"}

Last updated: ${this.lastUpdate}

`}
`; } 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)], }); } } /** * Demo component showcasing @onAssign decorator with dynamic paths */ @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" }, { firstName: "Diana", lastName: "Prince" }, { firstName: "Eve", lastName: "Adams" }, ]; const randomThemes = ["light", "dark", "auto"]; const randomLanguages = ["en", "fr", "es", "de", "it"]; 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`
Update current user data
${this.user?.firstName} ${this.user?.lastName}
${this.user?.email}
Theme: ${this.userSettings?.theme} | Language: ${this.userSettings?.language}
`; } } /** * Demo component showcasing @autoSubscribe decorator */ @customElement("demo-auto-subscribe") export class DemoAutoSubscribe extends LitElement { static styles = [tailwind]; @state() displayText: string = ""; @state() computedValue: number = 0; @autoSubscribe() updateDisplay() { const value1 = PublisherManager.get("autoValue1").get() || 0; const value2 = PublisherManager.get("autoValue2").get() || 0; this.computedValue = value1 + value2; this.displayText = `${value1} + ${value2} = ${this.computedValue}`; } render() { return html`

${this.displayText}

this.randomizeValue("autoValue1")}> Randomize Value 1 this.randomizeValue("autoValue2")}> Randomize Value 2
`; } randomizeValue(publisherId: string) { const value = PublisherManager.get(publisherId); value.set(Math.floor(Math.random() * 100)); } } /** * Parent component for awaitConnectedAncestors demo. * Registered via customElements.define() on button click (not @customElement). */ @dispatchConnectedEvent() export class DemoWaitAncestorContainer extends LitElement { render() { return html``; } } /** * Demo component showcasing @awaitConnectedAncestors decorator (child). * Uses CSS selector: tag + attribute. */ @customElement("demo-wait-ancestor-value") @awaitConnectedAncestors("demo-wait-ancestor-container[dataProvider]") export class DemoWaitAncestorValue extends LitElement { @ancestorAttribute("dataProvider") dataProvider: string | null = null; @state() initializedAt = ""; connectedCallback() { super.connectedCallback(); this.initializedAt = new Date().toISOString(); } render() { return html`

DataProvider from ancestor: ${this.dataProvider || "—"}

Initialized at: ${this.initializedAt || "(waiting for parent…)"}

`; } } /** * Demo section with button to register parent component on demand. * Demonstrates that the child waits until the parent is defined. */ @customElement("demo-wait-ancestors-section") export class DemoWaitAncestorsSection extends LitElement { static styles = [tailwind]; @state() parentRegistered = false; registerParent() { if (customElements.get("demo-wait-ancestor-container")) { this.parentRegistered = true; return; } customElements.define( "demo-wait-ancestor-container", DemoWaitAncestorContainer ); this.parentRegistered = true; } render() { return html`
${this.parentRegistered ? "Parent already registered" : "Register parent component"}
`; } } // --- Multiple ancestors demo --- @dispatchConnectedEvent() export class DemoWaitAncestorOuter extends LitElement { render() { return html``; } } @dispatchConnectedEvent() export class DemoWaitAncestorInner extends LitElement { render() { return html``; } } @customElement("demo-wait-ancestor-value-multi") @awaitConnectedAncestors("demo-wait-ancestor-outer", "demo-wait-ancestor-inner") export class DemoWaitAncestorValueMulti extends LitElement { @ancestorAttribute("dataProvider") dataProvider: string | null = null; @state() initializedAt = ""; connectedCallback() { super.connectedCallback(); this.initializedAt = new Date().toISOString(); } render() { return html`

DataProvider from ancestor: ${this.dataProvider || "—"}

Initialized at: ${this.initializedAt || "(waiting for both ancestors…)"}

`; } } /** * Demo: child waits for multiple ancestors. * Register outer first, then inner — child initializes only when both are ready. */ @customElement("demo-wait-ancestors-multi-section") export class DemoWaitAncestorsMultiSection extends LitElement { static styles = [tailwind]; @state() outerRegistered = false; @state() innerRegistered = false; registerOuter() { if (!customElements.get("demo-wait-ancestor-outer")) { customElements.define("demo-wait-ancestor-outer", DemoWaitAncestorOuter); } this.outerRegistered = true; } registerInner() { if (!customElements.get("demo-wait-ancestor-inner")) { customElements.define("demo-wait-ancestor-inner", DemoWaitAncestorInner); } this.innerRegistered = true; } render() { return html`
${this.outerRegistered ? "Outer registered" : "Register outer"} ${this.innerRegistered ? "Inner registered" : "Register inner"}
`; } } // --- Ancestors already connected demo --- @customElement("demo-wait-ancestor-ready") @dispatchConnectedEvent() export class DemoWaitAncestorReady extends LitElement { render() { return html``; } } @customElement("demo-wait-ancestor-value-ready") @awaitConnectedAncestors("demo-wait-ancestor-ready") export class DemoWaitAncestorValueReady extends LitElement { @ancestorAttribute("dataProvider") dataProvider: string | null = null; @state() initializedAt = ""; connectedCallback() { super.connectedCallback(); this.initializedAt = new Date().toISOString(); } render() { return html`

DataProvider: ${this.dataProvider || "—"}

Initialized at: ${this.initializedAt}

`; } } /** * Demo: ancestors already connected at load. * Parent is defined with @customElement. Child is added dynamically — it * initializes immediately because the ancestor is already ready. */ @customElement("demo-wait-ancestors-ready-section") export class DemoWaitAncestorsReadySection extends LitElement { static styles = [tailwind]; @state() childInDom = false; addChild() { this.childInDom = true; } render() { return html`

Parent is defined at load. Click to add child dynamically — it initializes immediately because the ancestor is already ready.

${this.childInDom ? "Child added" : "Add child dynamically"} ${this.childInDom ? html`` : html``}
`; } } /** * Demo: parent and child both in DOM from start, parent defined at load. * Child initializes immediately (no delay) because ancestor is already ready. */ @customElement("demo-wait-ancestors-static-section") export class DemoWaitAncestorsStaticSection extends LitElement { static styles = [tailwind]; render() { return html`

Parent and child both in DOM from load. Child initializes immediately because the ancestor is already defined and connected.

`; } }