(
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`
First set of users
Second set of users
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`
First set of users
Second set of users
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.
`;
}
}