@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.
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +229 -229
- package/concorde-core.es.js +2166 -1831
- package/dist/concorde-core.bundle.js +229 -229
- package/dist/concorde-core.es.js +2166 -1831
- package/docs/assets/{index-C0K6xugr.css → index-B669R8JF.css} +1 -1
- package/docs/assets/index-BTo6ly4d.js +4820 -0
- package/docs/index.html +2 -2
- package/docs/src/core/components/functional/fetch/fetch.md +6 -0
- package/docs/src/core/components/ui/menu/menu.md +46 -5
- package/docs/src/core/components/ui/modal/modal.md +0 -4
- package/docs/src/core/components/ui/toast/toast.md +166 -0
- package/docs/src/docs/_misc/ancestor-attribute.md +94 -0
- package/docs/src/docs/_misc/auto-subscribe.md +199 -0
- package/docs/src/docs/_misc/bind.md +362 -0
- package/docs/src/docs/_misc/on-assign.md +336 -0
- package/docs/src/docs/_misc/templates-demo.md +19 -0
- package/docs/src/docs/search/docs-search.json +550 -0
- package/docs/src/tsconfig-model.json +1 -1
- package/docs/src/tsconfig.json +28 -8
- package/package.json +8 -1
- package/src/core/components/functional/queue/queue.demo.ts +8 -11
- package/src/core/components/functional/sdui/sdui.ts +0 -0
- package/src/core/decorators/Subscriber.ts +5 -187
- package/src/core/decorators/subscriber/ancestorAttribute.ts +17 -0
- package/src/core/decorators/subscriber/autoFill.ts +28 -0
- package/src/core/decorators/subscriber/autoSubscribe.ts +54 -0
- package/src/core/decorators/subscriber/bind.ts +305 -0
- package/src/core/decorators/subscriber/common.ts +50 -0
- package/src/core/decorators/subscriber/onAssign.ts +318 -0
- package/src/core/mixins/Fetcher.ts +0 -0
- package/src/core/utils/HTML.ts +0 -0
- package/src/core/utils/PublisherProxy.ts +1 -1
- package/src/core/utils/api.ts +0 -0
- package/src/decorators.ts +9 -2
- package/src/docs/_misc/ancestor-attribute.md +94 -0
- package/src/docs/_misc/auto-subscribe.md +199 -0
- package/src/docs/_misc/bind.md +362 -0
- package/src/docs/_misc/on-assign.md +336 -0
- package/src/docs/_misc/templates-demo.md +19 -0
- package/src/docs/example/decorators-demo.ts +658 -0
- package/src/docs/navigation/navigation.ts +22 -3
- package/src/docs/search/docs-search.json +415 -0
- package/src/docs.ts +4 -0
- package/src/tsconfig-model.json +1 -1
- package/src/tsconfig.json +22 -2
- package/src/tsconfig.tsbuildinfo +1 -1
- package/vite.config.mts +0 -2
- package/docs/assets/index-Dgl1lJQo.js +0 -4861
- 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
|
package/src/core/utils/HTML.ts
CHANGED
|
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);
|
package/src/core/utils/api.ts
CHANGED
|
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"] =
|
|
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
|
+
|