@supersoniks/concorde 3.2.8 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +229 -229
  3. package/concorde-core.es.js +2166 -1831
  4. package/dist/concorde-core.bundle.js +229 -229
  5. package/dist/concorde-core.es.js +2166 -1831
  6. package/docs/assets/{index-C0K6xugr.css → index-B669R8JF.css} +1 -1
  7. package/docs/assets/index-BTo6ly4d.js +4820 -0
  8. package/docs/index.html +2 -2
  9. package/docs/src/core/components/functional/fetch/fetch.md +6 -0
  10. package/docs/src/core/components/ui/menu/menu.md +46 -5
  11. package/docs/src/core/components/ui/modal/modal.md +0 -4
  12. package/docs/src/core/components/ui/toast/toast.md +166 -0
  13. package/docs/src/docs/_misc/ancestor-attribute.md +94 -0
  14. package/docs/src/docs/_misc/auto-subscribe.md +199 -0
  15. package/docs/src/docs/_misc/bind.md +362 -0
  16. package/docs/src/docs/_misc/on-assign.md +336 -0
  17. package/docs/src/docs/_misc/templates-demo.md +19 -0
  18. package/docs/src/docs/search/docs-search.json +550 -0
  19. package/docs/src/tsconfig-model.json +1 -1
  20. package/docs/src/tsconfig.json +28 -8
  21. package/package.json +8 -1
  22. package/src/core/components/functional/queue/queue.demo.ts +8 -11
  23. package/src/core/components/functional/sdui/sdui.ts +0 -0
  24. package/src/core/decorators/Subscriber.ts +5 -187
  25. package/src/core/decorators/subscriber/ancestorAttribute.ts +17 -0
  26. package/src/core/decorators/subscriber/autoFill.ts +28 -0
  27. package/src/core/decorators/subscriber/autoSubscribe.ts +54 -0
  28. package/src/core/decorators/subscriber/bind.ts +305 -0
  29. package/src/core/decorators/subscriber/common.ts +50 -0
  30. package/src/core/decorators/subscriber/onAssign.ts +318 -0
  31. package/src/core/mixins/Fetcher.ts +0 -0
  32. package/src/core/utils/HTML.ts +0 -0
  33. package/src/core/utils/PublisherProxy.ts +1 -1
  34. package/src/core/utils/api.ts +0 -0
  35. package/src/decorators.ts +9 -2
  36. package/src/docs/_misc/ancestor-attribute.md +94 -0
  37. package/src/docs/_misc/auto-subscribe.md +199 -0
  38. package/src/docs/_misc/bind.md +362 -0
  39. package/src/docs/_misc/on-assign.md +336 -0
  40. package/src/docs/_misc/templates-demo.md +19 -0
  41. package/src/docs/example/decorators-demo.ts +658 -0
  42. package/src/docs/navigation/navigation.ts +22 -3
  43. package/src/docs/search/docs-search.json +415 -0
  44. package/src/docs.ts +4 -0
  45. package/src/tsconfig-model.json +1 -1
  46. package/src/tsconfig.json +22 -2
  47. package/src/tsconfig.tsbuildinfo +1 -1
  48. package/vite.config.mts +0 -2
  49. package/docs/assets/index-Dgl1lJQo.js +0 -4861
  50. package/templates-test.html +0 -32
@@ -0,0 +1,318 @@
1
+ import { Objects } from "@supersoniks/concorde/utils";
2
+
3
+ import { PublisherProxy, PublisherManager } from "../../utils/PublisherProxy";
4
+ import { ConnectedComponent, setSubscribable } from "./common";
5
+
6
+ const dynamicWatcherStore = Symbol("__onAssignDynamicWatcherStore__");
7
+ const dynamicWillUpdateHookedStore = Symbol(
8
+ "__onAssignDynamicWillUpdateHooked__"
9
+ );
10
+
11
+ function registerDynamicWatcher(
12
+ instance: any,
13
+ propertyName: string,
14
+ onChange: () => void
15
+ ) {
16
+ const key = String(propertyName);
17
+ ensureWillUpdateHook(instance);
18
+ if (!instance[dynamicWatcherStore]) {
19
+ Object.defineProperty(instance, dynamicWatcherStore, {
20
+ value: new Map<string, Set<() => void>>(),
21
+ enumerable: false,
22
+ configurable: false,
23
+ writable: false,
24
+ });
25
+ }
26
+ const watcherMap = instance[dynamicWatcherStore] as Map<
27
+ string,
28
+ Set<() => void>
29
+ >;
30
+ if (!watcherMap.has(key)) {
31
+ watcherMap.set(key, new Set());
32
+ }
33
+ const watchers = watcherMap.get(key)!;
34
+ watchers.add(onChange);
35
+ return () => {
36
+ watchers.delete(onChange);
37
+ if (watchers.size === 0) {
38
+ watcherMap.delete(key);
39
+ }
40
+ };
41
+ }
42
+
43
+ function ensureWillUpdateHook(instance: any) {
44
+ const proto = Object.getPrototypeOf(instance);
45
+ if (!proto || proto[dynamicWillUpdateHookedStore]) return;
46
+ const originalWillUpdate = Object.prototype.hasOwnProperty.call(
47
+ proto,
48
+ "willUpdate"
49
+ )
50
+ ? proto.willUpdate
51
+ : Object.getPrototypeOf(proto)?.willUpdate;
52
+ proto.willUpdate = function (changedProperties?: Map<unknown, unknown>) {
53
+ const handlers = this[dynamicWatcherStore] as
54
+ | Map<string, Set<() => void>>
55
+ | undefined;
56
+ if (handlers && handlers.size > 0) {
57
+ if (changedProperties && changedProperties.size > 0) {
58
+ changedProperties.forEach((_value, dependency) => {
59
+ const callbacks = handlers.get(String(dependency));
60
+ if (callbacks) {
61
+ callbacks.forEach((cb) => cb());
62
+ }
63
+ });
64
+ } else {
65
+ handlers.forEach((callbacks) => callbacks.forEach((cb) => cb()));
66
+ }
67
+ }
68
+ originalWillUpdate?.call(this, changedProperties);
69
+ };
70
+ proto[dynamicWillUpdateHookedStore] = true;
71
+ }
72
+
73
+ function extractDynamicDependencies(path: string) {
74
+ const patterns = [/\$\{([^}]+)\}/g, /\{\$([^}]+)\}/g];
75
+ const deps = new Set<string>();
76
+ for (const pattern of patterns) {
77
+ let match;
78
+ while ((match = pattern.exec(path)) !== null) {
79
+ const cleaned = cleanPlaceholder(match[1]);
80
+ if (!cleaned) continue;
81
+ const [root] = cleaned.split(".");
82
+ if (root) deps.add(root);
83
+ }
84
+ }
85
+ return Array.from(deps);
86
+ }
87
+
88
+ function cleanPlaceholder(value: string) {
89
+ return value.trim().replace(/^this\./, "");
90
+ }
91
+
92
+ function resolveDynamicPath(component: any, template: string) {
93
+ let missing = false;
94
+ const replaceValue = (_match: string, expression: string) => {
95
+ const cleaned = cleanPlaceholder(expression);
96
+ const resolved = getValueFromExpression(component, cleaned);
97
+ if (resolved === undefined || resolved === null) {
98
+ missing = true;
99
+ return "";
100
+ }
101
+ return `${resolved}`;
102
+ };
103
+ const resolvedPath = template
104
+ .replace(/\$\{([^}]+)\}/g, replaceValue)
105
+ .replace(/\{\$([^}]+)\}/g, replaceValue)
106
+ .trim();
107
+ if (missing || !resolvedPath.length) {
108
+ return { ready: false, path: null };
109
+ }
110
+ const segments = resolvedPath.split(".").filter(Boolean);
111
+ if (segments.length === 0 || !segments[0]) {
112
+ return { ready: false, path: null };
113
+ }
114
+ return { ready: true, path: resolvedPath };
115
+ }
116
+
117
+ function getValueFromExpression(component: any, expression: string) {
118
+ if (!expression) return undefined;
119
+ const segments = expression.split(".").filter(Boolean);
120
+ if (segments.length === 0) return undefined;
121
+ let current: unknown = component;
122
+ for (const segment of segments) {
123
+ if (
124
+ current === undefined ||
125
+ current === null ||
126
+ typeof current !== "object"
127
+ ) {
128
+ return undefined;
129
+ }
130
+ current = (current as Record<string, unknown>)[segment];
131
+ }
132
+ return current;
133
+ }
134
+
135
+ function getPublisherFromPath(path: string) {
136
+ const segments = path.split(".").filter((segment) => segment.length > 0);
137
+ if (segments.length === 0) return null;
138
+ const dataProvider = segments.shift() || "";
139
+ if (!dataProvider) return null;
140
+ let publisher = PublisherManager.get(dataProvider);
141
+ if (!publisher) return null;
142
+ publisher = Objects.traverse(publisher, segments);
143
+ return publisher as PublisherProxy | null;
144
+ }
145
+
146
+ type Callback = (...values: unknown[]) => void;
147
+ type PathConfiguration = {
148
+ originalPath: string;
149
+ dynamicDependencies: string[];
150
+ isDynamic: boolean;
151
+ };
152
+
153
+ type Configuration = {
154
+ callbacks: Set<Callback>;
155
+ publisher: PublisherProxy | null;
156
+ onAssign: (value: unknown) => void;
157
+ unsubscribePublisher: (() => void) | null;
158
+ pathConfig: PathConfiguration;
159
+ index: number;
160
+ };
161
+
162
+ export function onAssign(...values: Array<string>) {
163
+ const pathConfigs: PathConfiguration[] = values.map((path) => {
164
+ const dynamicDependencies = extractDynamicDependencies(path);
165
+ return {
166
+ originalPath: path,
167
+ dynamicDependencies,
168
+ isDynamic: dynamicDependencies.length > 0,
169
+ };
170
+ });
171
+
172
+ return function (
173
+ target: unknown,
174
+ _propertyKey: string,
175
+ descriptor: PropertyDescriptor
176
+ ) {
177
+ setSubscribable(target);
178
+ const stateKey = `__onAssign_state__`;
179
+ let callback: Callback;
180
+
181
+ (target as ConnectedComponent).__onConnected__((component) => {
182
+ const state =
183
+ (component as any)[stateKey] ||
184
+ ((component as any)[stateKey] = {
185
+ cleanupWatchers: [] as Array<() => void>,
186
+ configurations: [] as Configuration[],
187
+ });
188
+
189
+ // Nettoyage des watchers et configurations précédentes
190
+ state.cleanupWatchers.forEach((cleanup: () => void) => cleanup());
191
+ state.cleanupWatchers = [];
192
+ state.configurations.forEach((conf: Configuration) => {
193
+ if (conf.unsubscribePublisher) {
194
+ conf.unsubscribePublisher();
195
+ }
196
+ });
197
+ state.configurations = [];
198
+
199
+ const onAssignValues: unknown[] = [];
200
+ const confs: Configuration[] = [];
201
+
202
+ // Initialisation des configurations
203
+ for (let i = 0; i < values.length; i++) {
204
+ const pathConfig = pathConfigs[i];
205
+ const callbacks: Set<Callback> = new Set();
206
+ const onAssign = (assignedValue: unknown) => {
207
+ onAssignValues[i] = assignedValue;
208
+ if (
209
+ onAssignValues.filter((v) => v !== null && v !== undefined)
210
+ .length === values.length
211
+ ) {
212
+ callbacks.forEach((callback) => callback(...onAssignValues));
213
+ }
214
+ };
215
+ confs.push({
216
+ publisher: null,
217
+ onAssign,
218
+ callbacks,
219
+ unsubscribePublisher: null,
220
+ pathConfig,
221
+ index: i,
222
+ });
223
+ }
224
+
225
+ const subscribeToPath = (
226
+ conf: Configuration,
227
+ resolvedPath: string | null
228
+ ) => {
229
+ // Désabonnement de l'ancien publisher
230
+ if (conf.unsubscribePublisher) {
231
+ conf.unsubscribePublisher();
232
+ conf.unsubscribePublisher = null;
233
+ }
234
+
235
+ // Réinitialiser la valeur pour ce chemin lors du changement
236
+ onAssignValues[conf.index] = null;
237
+ conf.publisher = null;
238
+
239
+ if (!resolvedPath) {
240
+ return;
241
+ }
242
+
243
+ const publisher = getPublisherFromPath(resolvedPath);
244
+ if (!publisher) {
245
+ return;
246
+ }
247
+
248
+ publisher.onAssign(conf.onAssign);
249
+ conf.unsubscribePublisher = () => {
250
+ publisher.offAssign(conf.onAssign);
251
+ if (conf.publisher === publisher) {
252
+ conf.publisher = null;
253
+ }
254
+ };
255
+ conf.publisher = publisher;
256
+ };
257
+
258
+ const refreshSubscriptions = () => {
259
+ for (const conf of confs) {
260
+ if (conf.pathConfig.isDynamic) {
261
+ const resolution = resolveDynamicPath(
262
+ component,
263
+ conf.pathConfig.originalPath
264
+ );
265
+ if (!resolution.ready) {
266
+ subscribeToPath(conf, null);
267
+ continue;
268
+ }
269
+ subscribeToPath(conf, resolution.path);
270
+ } else {
271
+ subscribeToPath(conf, conf.pathConfig.originalPath);
272
+ }
273
+ }
274
+ };
275
+
276
+ // Enregistrement des watchers pour les chemins dynamiques
277
+ for (const conf of confs) {
278
+ if (conf.pathConfig.isDynamic) {
279
+ for (const dependency of conf.pathConfig.dynamicDependencies) {
280
+ const unsubscribe = registerDynamicWatcher(
281
+ component as Record<string, unknown>,
282
+ dependency,
283
+ () => refreshSubscriptions()
284
+ );
285
+ state.cleanupWatchers.push(unsubscribe);
286
+ }
287
+ }
288
+ }
289
+
290
+ // Initialisation du callback
291
+ callback = descriptor.value.bind(component);
292
+ for (const conf of confs) {
293
+ conf.callbacks.add(callback);
294
+ }
295
+
296
+ // Initialisation des abonnements
297
+ refreshSubscriptions();
298
+
299
+ state.configurations = confs;
300
+ });
301
+
302
+ (target as ConnectedComponent).__onDisconnected__((component) => {
303
+ const state = (component as any)[stateKey];
304
+ if (!state) return;
305
+
306
+ state.cleanupWatchers.forEach((cleanup: () => void) => cleanup());
307
+ state.cleanupWatchers = [];
308
+
309
+ state.configurations.forEach((conf: Configuration) => {
310
+ if (conf.unsubscribePublisher) {
311
+ conf.unsubscribePublisher();
312
+ }
313
+ conf.callbacks.delete(callback);
314
+ });
315
+ state.configurations = [];
316
+ });
317
+ };
318
+ }
File without changes
File without changes
@@ -455,9 +455,9 @@ export class PublisherProxy<T = any> {
455
455
  */
456
456
  _cachedGet_?: T;
457
457
  get(): T {
458
- if (this._cachedGet_ !== undefined) return this._cachedGet_;
459
458
  if (PublisherManager.modifiedCollectore.length > 0)
460
459
  PublisherManager.modifiedCollectore[0].add(this);
460
+ if (this._cachedGet_ !== undefined) return this._cachedGet_;
461
461
  if (Object.prototype.hasOwnProperty.call(this._value_, "__value")) {
462
462
  const v = (this._value_ as any).__value;
463
463
  return (this._cachedGet_ = (v != undefined ? v : null) as T);
File without changes
package/src/decorators.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import * as mySubscriber from "@supersoniks/concorde/core/decorators/Subscriber";
2
2
  export const bind = mySubscriber.bind;
3
3
  export const onAssign = mySubscriber.onAssign;
4
+ export const ancestorAttribute = mySubscriber.ancestorAttribute;
5
+ export const autoSubscribe = mySubscriber.autoSubscribe;
6
+ export const autoFill = mySubscriber.autoFill;
4
7
 
5
- import {ConcordeWindow} from "./core/_types/types";
8
+ import { ConcordeWindow } from "./core/_types/types";
6
9
  declare const window: ConcordeWindow;
7
10
 
8
- window["concorde-decorator-subscriber"] = window["concorde-decorator-subscriber"] || {};
11
+ window["concorde-decorator-subscriber"] =
12
+ window["concorde-decorator-subscriber"] || {};
9
13
  window["concorde-decorator-subscriber"] = {
10
14
  bind: mySubscriber.bind,
11
15
  onAssing: mySubscriber.onAssign,
16
+ ancestorAttribute: mySubscriber.ancestorAttribute,
17
+ autoSubscribe: mySubscriber.autoSubscribe,
18
+ autoFill: mySubscriber.autoFill,
12
19
  };
@@ -0,0 +1,94 @@
1
+ # @ancestorAttribute
2
+
3
+ The `@ancestorAttribute` decorator automatically injects the value of an ancestor's attribute into a class property at the time of `connectedCallback`.
4
+
5
+ ## Principle
6
+
7
+ This decorator uses `HTML.getAncestorAttributeValue` to traverse up the DOM tree from the current element and find the first ancestor that has the specified attribute. The value of this attribute is then assigned to the decorated property.
8
+
9
+ ## Usage
10
+
11
+ ### Import
12
+
13
+ <sonic-code language="typescript">
14
+ <template>
15
+ import { ancestorAttribute } from "@supersoniks/concorde/decorators";
16
+ </template>
17
+ </sonic-code>
18
+
19
+ ### Basic example
20
+ Le composant lit les attributs `dataProvider` et `testAttribute` exposés par son conteneur ancêtre.
21
+
22
+ <sonic-code language="typescript">
23
+ <template>
24
+ //...
25
+ @customElement("demo-bind-reflect")
26
+ export class DemoBindReflect extends LitElement {
27
+ static styles = [tailwind];
28
+ //
29
+ @bind("bindReflectDemo.count", { reflect: true })
30
+ @state()
31
+ withReflect: number = 0;
32
+ //
33
+ @bind("bindReflectDemo.count")
34
+ @state()
35
+ withoutReflect: number = 0;
36
+ // initialize the publisher data
37
+ connectedCallback() {
38
+ super.connectedCallback();
39
+ this.resetData();
40
+ }
41
+ //
42
+ resetData() {
43
+ PublisherManager.get("bindReflectDemo").set({ count: 0 });
44
+ }
45
+ render() {
46
+ return html`
47
+ <div class="mb-3">
48
+ from publisher : ${sub("bindReflectDemo.count")} <br />
49
+ from component with reflect : ${this.withReflect} <br />
50
+ from component without reflect : ${this.withoutReflect}
51
+ </div>
52
+ <sonic-button @click=${() => this.withReflect++}
53
+ >Increment with reflect</sonic-button
54
+ >
55
+ <sonic-button @click=${() => this.withoutReflect++}
56
+ >Increment without reflect</sonic-button
57
+ >
58
+ <sonic-button @click=${this.resetData}>Reset publisher data</sonic-button>
59
+ `;
60
+ }
61
+ }
62
+ </template>
63
+ </sonic-code>
64
+
65
+ <sonic-code>
66
+ <template>
67
+ <div dataProvider="demoDataProvider" testAttribute="test-value-123">
68
+ <demo-ancestor-attribute></demo-ancestor-attribute>
69
+ </div>
70
+ </template>
71
+ </sonic-code>
72
+
73
+
74
+ ## Use cases
75
+
76
+ This decorator is particularly useful for:
77
+
78
+ - **Retrieving the `dataProvider`** from an ancestor without having to pass it explicitly
79
+ - **Retrieving the `formDataProvider`** in form components
80
+ - **Retrieving the `wordingProvider`** for translation
81
+ - **Retrieving any other attribute** defined on an ancestor
82
+
83
+ ## Behavior
84
+
85
+ - The search starts from the current element and traverses up the DOM tree
86
+ - If the attribute is not found, the property will be assigned `null`
87
+ - The injection happens automatically at the time of `connectedCallback`
88
+ - The value is not reactive: it is only updated once when the element is connected to the DOM
89
+
90
+ ## Notes
91
+
92
+ - This decorator works with any component that has a `connectedCallback` method (such as `LitElement` or components extending `Subscriber`)
93
+ - The search also traverses Shadow DOM if necessary
94
+ - If multiple ancestors have the attribute, the closest one will be used
@@ -0,0 +1,199 @@
1
+ # @autoSubscribe
2
+
3
+ The `@autoSubscribe` decorator automatically detects which publishers are accessed within a method and subscribes to them. When any of these publishers change, the method is automatically re-executed.
4
+
5
+ ## Principle
6
+
7
+ This decorator wraps a method to track which publishers are accessed during its execution. It then subscribes to all accessed publishers, and when any of them change, the method is re-executed. This provides automatic reactivity without manually managing subscriptions.
8
+
9
+ ## Usage
10
+
11
+ ### Import
12
+
13
+ <sonic-code language="typescript">
14
+ <template>
15
+ import { autoSubscribe } from "@supersoniks/concorde/decorators";
16
+ </template>
17
+ </sonic-code>
18
+
19
+ ### Basic example
20
+
21
+
22
+ <sonic-code language="typescript">
23
+ <template>
24
+ @customElement("demo-auto-subscribe")
25
+ export class DemoAutoSubscribe extends LitElement {
26
+ static styles = [tailwind];
27
+ //
28
+ @state() displayText: string = "";
29
+ @state() computedValue: number = 0;
30
+ //
31
+ @autoSubscribe()
32
+ updateDisplay() {
33
+ const value1 = PublisherManager.get("autoValue1").get() || 0;
34
+ const value2 = PublisherManager.get("autoValue2").get() || 0;
35
+ this.computedValue = value1 + value2;
36
+ this.displayText = `${value1} + ${value2} = ${this.computedValue}`;
37
+ }
38
+ //
39
+ render() {
40
+ return html`
41
+ <p><strong>${this.displayText}</strong></p>
42
+ <div>
43
+ <sonic-button @click=${() => this.randomizeValue("autoValue1")}>
44
+ Randomize Value 1
45
+ </sonic-button>
46
+ <sonic-button @click=${() => this.randomizeValue("autoValue2")}>
47
+ Randomize Value 2
48
+ </sonic-button>
49
+ </div>
50
+ `;
51
+ }
52
+ //
53
+ randomizeValue(publisherId: string) {
54
+ const value = PublisherManager.get(publisherId);
55
+ value.set(Math.floor(Math.random() * 100));
56
+ }
57
+ }
58
+ </template>
59
+ </sonic-code>
60
+
61
+
62
+ <sonic-code >
63
+ <template>
64
+ <demo-auto-subscribe></demo-auto-subscribe>
65
+ </template>
66
+ </sonic-code>
67
+
68
+ ### Example with render method
69
+
70
+ <sonic-code language="typescript">
71
+ <template>
72
+ @customElement("reactive-view")
73
+ export class ReactiveView extends LitElement {
74
+ @autoSubscribe()
75
+ render() {
76
+ const data = PublisherManager.get("myData");
77
+ const config = PublisherManager.get("config");
78
+ //
79
+ // This render method will be automatically re-executed
80
+ // when myData or config change
81
+ const value = data.get()?.value || 0;
82
+ const multiplier = config.get()?.multiplier || 1;
83
+ //
84
+ return html`
85
+ <div>
86
+ <h1>Result: ${value * multiplier}</h1>
87
+ <p>Value: ${value}</p>
88
+ <p>Multiplier: ${multiplier}</p>
89
+ </div>
90
+ `;
91
+ }
92
+ }
93
+ </template>
94
+ </sonic-code>
95
+
96
+ ## How it works
97
+
98
+ 1. **First execution**: When the method is called, `PublisherManager.collectModifiedPublisher()` is used to track which publishers are accessed
99
+ 2. **Subscription**: After execution, the decorator subscribes to all detected publishers
100
+ 3. **Re-execution**: When any subscribed publisher changes, the method is automatically called again
101
+ 4. **Cleanup**: On `disconnectedCallback`, all subscriptions are automatically removed
102
+
103
+ ## Behavior
104
+
105
+ - The method is automatically called on `connectedCallback`
106
+ - The method is re-executed whenever any accessed publisher changes
107
+ - Subscriptions are managed automatically (no manual cleanup needed)
108
+ - Only publishers accessed during method execution are subscribed to
109
+ - The decorator uses `queueMicrotask` to batch multiple updates and avoid unnecessary re-renders
110
+
111
+ ## Use cases
112
+
113
+ This decorator is particularly useful for:
114
+
115
+ - **Reactive rendering** where the render method depends on multiple publishers
116
+ - **Data transformation** that needs to update when source data changes
117
+ - **Computed properties** that depend on multiple data sources
118
+ - **Automatic synchronization** between publishers and component state
119
+
120
+ ## Complete example
121
+
122
+ <sonic-code language="typescript">
123
+ <template>
124
+ import { html, LitElement } from "lit";
125
+ import { customElement } from "lit/decorators.js";
126
+ import { autoSubscribe } from "@supersoniks/concorde/decorators";
127
+ import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
128
+ //
129
+ @customElement("shopping-cart")
130
+ export class ShoppingCart extends LitElement {
131
+ items: any[] = [];
132
+ total: number = 0;
133
+ discount: number = 0;
134
+ //
135
+ @autoSubscribe()
136
+ calculateTotal() {
137
+ const cart = PublisherManager.get("cart");
138
+ const promo = PublisherManager.get("promo");
139
+ //
140
+ // Access cart items
141
+ this.items = cart.items.get() || [];
142
+ //
143
+ // Access promo code
144
+ const promoCode = promo.code.get() || "";
145
+ const discountPercent = promoCode === "SAVE10" ? 0.1 : 0;
146
+ //
147
+ // Calculate totals
148
+ const subtotal = this.items.reduce((sum, item) =>
149
+ sum + (item.price * item.quantity), 0
150
+ );
151
+ this.discount = subtotal * discountPercent;
152
+ this.total = subtotal - this.discount;
153
+ //
154
+ this.requestUpdate();
155
+ }
156
+ //
157
+ connectedCallback() {
158
+ super.connectedCallback();
159
+ this.calculateTotal();
160
+ }
161
+ //
162
+ render() {
163
+ return html`
164
+ <div class="cart">
165
+ <h2>Shopping Cart</h2>
166
+ ${this.items.map(item => html`
167
+ <div>${item.name} x${item.quantity} - ${item.price}€</div>
168
+ `)}
169
+ <div class="total">
170
+ <p>Subtotal: ${this.total + this.discount}€</p>
171
+ <p>Discount: -${this.discount}€</p>
172
+ <p><strong>Total: ${this.total}€</strong></p>
173
+ </div>
174
+ </div>
175
+ `;
176
+ }
177
+ }
178
+ //
179
+ // When you update the publishers, calculateTotal is automatically called:
180
+ const cart = PublisherManager.get("cart");
181
+ cart.items.set([
182
+ { name: "Product 1", price: 10, quantity: 2 },
183
+ { name: "Product 2", price: 15, quantity: 1 }
184
+ ]);
185
+ //
186
+ const promo = PublisherManager.get("promo");
187
+ promo.code.set("SAVE10");
188
+ // calculateTotal will be automatically called and the UI will update
189
+ </template>
190
+ </sonic-code>
191
+
192
+ ## Notes
193
+
194
+ - This decorator works with any component that has `connectedCallback` and `disconnectedCallback` methods (such as `LitElement` or components extending `Subscriber`)
195
+ - The method is called automatically on `connectedCallback`
196
+ - Remember to call `this.requestUpdate()` if you're updating component properties
197
+ - The decorator uses debouncing via `queueMicrotask` to prevent excessive re-executions
198
+ - For more information about publishers, see the documentation on [Sharing data](#docs/_getting-started/pubsub.md/pubsub)
199
+