@supersoniks/concorde 4.5.2 → 4.7.0
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/.gitlab-ci.yml +23 -0
- package/README.md +106 -55
- package/ai/AGENTS.md +52 -0
- package/ai/README.md +30 -0
- package/ai/cursor/rules/concorde-menu.mdc +15 -0
- package/ai/cursor/rules/concorde-scope.mdc +14 -0
- package/ai/cursor/rules/concorde-theme.mdc +13 -0
- package/ai/cursor/rules/concorde.mdc +49 -0
- package/ai/jetbrains/rules/concorde.md +39 -0
- package/ai/skills/concorde/SKILL.md +220 -0
- package/ai/skills/concorde-get-set-dp/SKILL.md +194 -0
- package/ai/skills/concorde-imports/SKILL.md +78 -0
- package/ai/skills/concorde-menu/SKILL.md +74 -0
- package/ai/skills/concorde-scope/SKILL.md +70 -0
- package/ai/skills/concorde-theme/SKILL.md +46 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +159 -159
- package/concorde-core.es.js +1915 -1809
- package/dist/altcha-widget.js +2662 -0
- package/dist/concorde-core.bundle.js +159 -159
- package/dist/concorde-core.es.js +1915 -1809
- package/dist/docs-mock-api-sw.js +589 -0
- package/dist/docs-mock-api-sw.js.map +7 -0
- package/docs/altcha-widget.js +2662 -0
- package/docs/assets/index-D9pxaQYK.js +7508 -0
- package/docs/assets/index-t0-i22oI.css +1 -0
- package/docs/docs-mock-api-sw.js +589 -0
- package/docs/docs-mock-api-sw.js.map +7 -0
- package/docs/index.html +2 -2
- package/docs/src/core/components/functional/fetch/fetch.md +13 -11
- package/docs/src/core/components/functional/if/if.md +4 -11
- package/docs/src/core/components/functional/list/list.md +60 -194
- package/docs/src/core/components/functional/queue/queue.md +70 -85
- package/docs/src/core/components/functional/router/router.md +62 -97
- package/docs/src/core/components/functional/states/states.md +2 -2
- package/docs/src/core/components/functional/submit/submit.md +86 -55
- package/docs/src/core/components/ui/captcha/captcha.md +2 -2
- package/docs/src/core/components/ui/card/card.md +1 -1
- package/docs/src/core/components/ui/form/checkbox/checkbox.md +5 -32
- package/docs/src/core/components/ui/form/input/input.md +5 -30
- package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
- package/docs/src/core/components/ui/form/radio/radio.md +5 -32
- package/docs/src/core/components/ui/form/select/select.md +5 -31
- package/docs/src/core/components/ui/form/switch/switch.md +5 -32
- package/docs/src/core/components/ui/loader/loader.md +1 -13
- package/docs/src/core/components/ui/table/table.md +3 -3
- package/docs/src/docs/_core-concept/dataFlow.md +73 -0
- package/docs/src/docs/_core-concept/subscriber.md +9 -10
- package/docs/src/docs/_decorators/ancestor-attribute.md +4 -3
- package/docs/src/docs/_decorators/auto-subscribe.md +19 -16
- package/docs/src/docs/_decorators/bind.md +20 -17
- package/docs/src/docs/_decorators/get.md +7 -4
- package/docs/src/docs/_decorators/handle.md +171 -0
- package/docs/src/docs/_decorators/on-assign.md +99 -73
- package/docs/src/docs/_decorators/publish.md +2 -1
- package/docs/src/docs/_decorators/subscribe.md +70 -9
- package/docs/src/docs/_decorators/wait-for-ancestors.md +13 -10
- package/docs/src/docs/_directives/sub.md +91 -0
- package/docs/src/docs/_getting-started/ai-agents.md +56 -0
- package/docs/src/docs/_getting-started/concorde-manual-install.md +133 -0
- package/docs/src/docs/_getting-started/concorde-outside.md +13 -123
- package/docs/src/docs/_getting-started/create-a-component.md +2 -0
- package/docs/src/docs/_getting-started/my-first-component.md +236 -0
- package/docs/src/docs/_getting-started/my-first-subscriber.md +29 -83
- package/docs/src/docs/_getting-started/pubsub.md +21 -134
- package/docs/src/docs/_getting-started/start.md +26 -18
- package/docs/src/docs/_misc/api-configuration.md +79 -0
- package/docs/src/docs/_misc/dataProviderKey.md +38 -5
- package/docs/src/docs/_misc/docs-mock-api.md +60 -0
- package/docs/src/docs/_misc/endpoint.md +2 -1
- package/docs/src/docs/_misc/html-integration.md +13 -0
- package/docs/src/docs/search/docs-search.json +4163 -873
- package/docs/src/tsconfig.json +380 -317
- package/gitlab/job_tests.sh +55 -0
- package/package.json +37 -3
- package/public/altcha-widget.js +2662 -0
- package/public/docs-mock-api-sw.js +589 -0
- package/public/docs-mock-api-sw.js.map +7 -0
- package/scripts/ai-init.mjs +167 -0
- package/scripts/docs-mock-api-vite-plugin.ts +116 -0
- package/scripts/docs-open-in-editor-plugin.ts +130 -0
- package/scripts/pre-publish.mjs +2 -1
- package/src/core/components/functional/example/example.ts +1 -1
- package/src/core/components/functional/fetch/fetch.md +13 -11
- package/src/core/components/functional/if/if.md +4 -11
- package/src/core/components/functional/list/list.demo.ts +4 -4
- package/src/core/components/functional/list/list.md +60 -194
- package/src/core/components/functional/list/list.ts +8 -7
- package/src/core/components/functional/queue/queue.demo.ts +1 -1
- package/src/core/components/functional/queue/queue.md +70 -85
- package/src/core/components/functional/queue/queue.ts +4 -4
- package/src/core/components/functional/router/router.md +62 -97
- package/src/core/components/functional/router/router.ts +1 -1
- package/src/core/components/functional/states/states.md +2 -2
- package/src/core/components/functional/submit/submit.md +86 -55
- package/src/core/components/functional/submit/submit.ts +10 -3
- package/src/core/components/ui/captcha/captcha.md +2 -2
- package/src/core/components/ui/card/card.md +1 -1
- package/src/core/components/ui/form/checkbox/checkbox.md +5 -32
- package/src/core/components/ui/form/input/input.md +5 -30
- package/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
- package/src/core/components/ui/form/radio/radio.md +5 -32
- package/src/core/components/ui/form/select/select.md +5 -31
- package/src/core/components/ui/form/switch/switch.md +5 -32
- package/src/core/components/ui/loader/loader.md +1 -13
- package/src/core/components/ui/table/table.md +3 -3
- package/src/core/decorators/Subscriber.ts +2 -0
- package/src/core/decorators/subscriber/handle.disambig.spec.ts +20 -0
- package/src/core/decorators/subscriber/handle.skip.spec.ts +37 -0
- package/src/core/decorators/subscriber/handle.ts +128 -0
- package/src/core/decorators/subscriber/onAssign.ts +94 -4
- package/src/core/directives/DataProvider.sub.spec.ts +96 -0
- package/src/core/directives/DataProvider.ts +109 -40
- package/src/core/utils/PublisherProxy.ts +33 -18
- package/src/core/utils/dataProviderKey.ts +23 -0
- package/src/core/utils/publisherPathKey.spec.ts +58 -0
- package/src/decorators.ts +6 -0
- package/src/docs/_core-concept/dataFlow.md +73 -0
- package/src/docs/_core-concept/subscriber.md +9 -10
- package/src/docs/_decorators/ancestor-attribute.md +4 -3
- package/src/docs/_decorators/auto-subscribe.md +19 -16
- package/src/docs/_decorators/bind.md +20 -17
- package/src/docs/_decorators/get.md +7 -4
- package/src/docs/_decorators/handle.md +171 -0
- package/src/docs/_decorators/on-assign.md +99 -47
- package/src/docs/_decorators/publish.md +2 -1
- package/src/docs/_decorators/subscribe.md +70 -9
- package/src/docs/_decorators/wait-for-ancestors.md +13 -10
- package/src/docs/_directives/sub.md +91 -0
- package/src/docs/_getting-started/ai-agents.md +56 -0
- package/src/docs/_getting-started/concorde-manual-install.md +133 -0
- package/src/docs/_getting-started/concorde-outside.md +13 -123
- package/src/docs/_getting-started/create-a-component.md +2 -0
- package/src/docs/_getting-started/my-first-component.md +236 -0
- package/src/docs/_getting-started/my-first-subscriber.md +29 -83
- package/src/docs/_getting-started/pubsub.md +21 -134
- package/src/docs/_getting-started/start.md +26 -18
- package/src/docs/_misc/api-configuration.md +79 -0
- package/src/docs/_misc/dataProviderKey.md +38 -5
- package/src/docs/_misc/docs-mock-api.md +60 -0
- package/src/docs/_misc/endpoint.md +2 -1
- package/src/docs/_misc/html-integration.md +13 -0
- package/src/docs/code.ts +58 -12
- package/src/docs/components/docs-demo-sources.ts +397 -0
- package/src/docs/components/docs-lit-demo-raw.ts +28 -0
- package/src/docs/components/docs-lit-demo.ts +166 -0
- package/src/docs/components/docs-source-link.ts +72 -0
- package/src/docs/docs-location.ts +54 -0
- package/src/docs/docs.ts +12 -0
- package/src/docs/example/decorators-demo-bind-demos.ts +41 -46
- package/src/docs/example/decorators-demo-geo.ts +16 -11
- package/src/docs/example/decorators-demo-init.ts +2 -228
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +142 -12
- package/src/docs/example/decorators-demo.ts +71 -70
- package/src/docs/example/docs-api-config-demos.ts +234 -0
- package/src/docs/example/docs-joke-demos.ts +297 -0
- package/src/docs/example/docs-list-demos.ts +179 -0
- package/src/docs/example/docs-provider-keys.ts +315 -0
- package/src/docs/example/docs-queue-demos.ts +114 -0
- package/src/docs/example/docs-router-demos.ts +89 -0
- package/src/docs/example/docs-submit-demos.ts +455 -0
- package/src/docs/example/docs-toggle-demos.ts +73 -0
- package/src/docs/example/docs-user-two-scopes.ts +37 -0
- package/src/docs/example/docs-users-list.ts +71 -0
- package/src/docs/example/users.ts +41 -24
- package/src/docs/mock-api/api-config-mock.ts +152 -0
- package/src/docs/mock-api/fixtures.ts +377 -0
- package/src/docs/mock-api/register.ts +25 -0
- package/src/docs/mock-api/router.ts +234 -0
- package/src/docs/mock-api/service-worker.ts +23 -0
- package/src/docs/mock-api/urls.ts +11 -0
- package/src/docs/navigation/navigation.ts +43 -7
- package/src/docs/search/docs-search.json +4193 -858
- package/src/docs/search/markdown-renderer.ts +7 -3
- package/src/docs/search/page.ts +11 -14
- package/src/docs/search/sonic-code-markdown.spec.ts +29 -0
- package/src/docs/search/sonic-code-markdown.ts +28 -0
- package/src/docs.ts +4 -0
- package/src/tsconfig.json +96 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- package/vite.config.mts +8 -0
- package/docs/assets/index-CaysOMFz.js +0 -5046
- package/docs/assets/index-D8mGoXzF.css +0 -1
- package/docs/src/docs/_misc/templates-demo.md +0 -19
- package/src/docs/_misc/templates-demo.md +0 -19
|
@@ -4,11 +4,11 @@ Binds a class property to a path in a publisher. The property updates when publi
|
|
|
4
4
|
|
|
5
5
|
For Lit re-renders, also add `@state()` on the same property.
|
|
6
6
|
|
|
7
|
-
**See also:** [@subscribe](#docs/_decorators/subscribe.md/subscribe), [@publish](#docs/_decorators/publish.md/publish), [@get](#docs/_decorators/get.md/get).
|
|
7
|
+
**See also:** [@subscribe](#docs/_decorators/subscribe.md/subscribe), [@handle](#docs/_decorators/handle.md/handle), [@publish](#docs/_decorators/publish.md/publish), [@get](#docs/_decorators/get.md/get).
|
|
8
8
|
|
|
9
9
|
## Principle
|
|
10
10
|
|
|
11
|
-
The decorator subscribes
|
|
11
|
+
The decorator subscribes to the DataProvider store using dot notation or a `DataProviderKey`. Updates flow into the decorated property ([Data flow](#docs/_core-concept/dataFlow.md/dataFlow)).
|
|
12
12
|
|
|
13
13
|
## Import
|
|
14
14
|
|
|
@@ -25,26 +25,26 @@ import { bind } from "@supersoniks/concorde/decorators";
|
|
|
25
25
|
@customElement("demo-bind")
|
|
26
26
|
export class DemoBind extends LitElement {
|
|
27
27
|
static styles = [tailwind];
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
@bind("demoData.firstName")
|
|
30
30
|
@state()
|
|
31
31
|
firstName = "";
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
@bind("demoData.lastName")
|
|
34
34
|
@state()
|
|
35
35
|
lastName: string = "";
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
@bind("demoData.count")
|
|
38
38
|
@state()
|
|
39
39
|
count: number = 0;
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
render() {
|
|
42
42
|
return //......
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
updateData() {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
set(demoDataKey, { ...get(demoDataKey), count: get(demoDataKey).count + 1 });
|
|
47
|
+
// see demo-bind in src/docs/example/decorators-demo-bind-demos.ts
|
|
48
48
|
const randomIndex = Math.floor(Math.random() * demoUsers.get().length);
|
|
49
49
|
const randomUser = demoUsers.get()[randomIndex];
|
|
50
50
|
demoData.set({
|
|
@@ -54,12 +54,13 @@ export class DemoBind extends LitElement {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
</template>
|
|
59
59
|
</sonic-code>
|
|
60
60
|
|
|
61
61
|
<sonic-code>
|
|
62
62
|
<template>
|
|
63
|
+
<docs-demo-sources for="demo-bind"></docs-demo-sources>
|
|
63
64
|
<demo-bind></demo-bind>
|
|
64
65
|
</template>
|
|
65
66
|
</sonic-code>
|
|
@@ -72,10 +73,10 @@ export class DemoBind extends LitElement {
|
|
|
72
73
|
<template>
|
|
73
74
|
import { bind } from "@supersoniks/concorde/decorators";
|
|
74
75
|
import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
type Data = { count: number };
|
|
77
78
|
const dataKey = new DataProviderKey<Data>("data");
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
@bind(dataKey.count, { reflect: true })
|
|
80
81
|
@state()
|
|
81
82
|
count: number = 0;
|
|
@@ -99,15 +100,15 @@ avatar: string;
|
|
|
99
100
|
@customElement("demo-bind-reflect")
|
|
100
101
|
export class DemoBindReflect extends LitElement {
|
|
101
102
|
static styles = [tailwind];
|
|
102
|
-
|
|
103
|
+
|
|
103
104
|
@bind("bindReflectDemo.count", { reflect: true })
|
|
104
105
|
@state()
|
|
105
106
|
withReflect: number = 0;
|
|
106
|
-
|
|
107
|
+
|
|
107
108
|
@bind("bindReflectDemo.count")
|
|
108
109
|
@state()
|
|
109
110
|
withoutReflect: number = 0;
|
|
110
|
-
|
|
111
|
+
|
|
111
112
|
render() {
|
|
112
113
|
return html`
|
|
113
114
|
<div class="mb-3">
|
|
@@ -124,13 +125,14 @@ export class DemoBindReflect extends LitElement {
|
|
|
124
125
|
`;
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
+
|
|
128
129
|
</template>
|
|
129
130
|
</sonic-code>
|
|
130
131
|
|
|
131
132
|
<sonic-code toggleCode>
|
|
132
133
|
<template>
|
|
133
|
-
<demo-bind-reflect></demo-
|
|
134
|
+
<docs-demo-sources for="demo-bind-reflect"></docs-demo-sources>
|
|
135
|
+
<demo-bind-reflect></demo-bind-reflect>
|
|
134
136
|
</template>
|
|
135
137
|
</sonic-code>
|
|
136
138
|
|
|
@@ -148,6 +150,7 @@ Use `${prop}` or `${this.prop}` inside a **normal string literal** (not a JS tem
|
|
|
148
150
|
|
|
149
151
|
<sonic-code>
|
|
150
152
|
<template>
|
|
153
|
+
<docs-demo-sources for="demo-bind-dynamic"></docs-demo-sources>
|
|
151
154
|
<demo-bind-dynamic></demo-bind-dynamic>
|
|
152
155
|
</template>
|
|
153
156
|
</sonic-code>
|
|
@@ -7,7 +7,7 @@ Pass an [`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint) as the first argument.
|
|
|
7
7
|
## Configuration
|
|
8
8
|
|
|
9
9
|
- **Default:** `HTML.getApiConfiguration(host)` (ancestor `serviceURL`, etc.).
|
|
10
|
-
- **Second argument:** `DataProviderKey<APIConfiguration>` — config is read from the publisher at the resolved path; internal mutations trigger another GET.
|
|
10
|
+
- **Second argument:** `DataProviderKey<APIConfiguration>` — config is read from the publisher at the resolved path; internal mutations trigger another GET. See [API configuration](#docs/_misc/api-configuration.md/api-configuration) for mock demos.
|
|
11
11
|
|
|
12
12
|
## When the GET runs again
|
|
13
13
|
|
|
@@ -26,7 +26,7 @@ import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
|
|
|
26
26
|
|
|
27
27
|
## Minimal example
|
|
28
28
|
|
|
29
|
-
Same demo service as [`sonic-queue`](../../core/components/functional/queue/queue.demo.ts) (
|
|
29
|
+
Same demo service as [`sonic-queue`](../../core/components/functional/queue/queue.demo.ts) (`/docs-mock-api/geo/`). Publisher setup lives in `decorators-demo-geo.ts` and `decorators-demo-subscribe-publish-get-demos.ts`.
|
|
30
30
|
|
|
31
31
|
<sonic-code language="typescript">
|
|
32
32
|
<template>
|
|
@@ -40,6 +40,7 @@ payload: ApiGetResult<User> | null = null;
|
|
|
40
40
|
|
|
41
41
|
<sonic-code>
|
|
42
42
|
<template>
|
|
43
|
+
<docs-demo-sources for="demo-api-get"></docs-demo-sources>
|
|
43
44
|
<demo-api-get></demo-api-get>
|
|
44
45
|
</template>
|
|
45
46
|
</sonic-code>
|
|
@@ -48,15 +49,17 @@ Dynamic config and endpoint path (`demo-api-get-configuration-key` in doc source
|
|
|
48
49
|
|
|
49
50
|
<sonic-code>
|
|
50
51
|
<template>
|
|
52
|
+
<docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
|
|
51
53
|
<demo-api-get-configuration-key></demo-api-get-configuration-key>
|
|
52
54
|
</template>
|
|
53
55
|
</sonic-code>
|
|
54
56
|
|
|
55
|
-
Scoped `@get` with `@publish` / `@subscribe` on the payload (see [@publish](#docs/_decorators/publish.md/publish) and [@subscribe](#docs/_decorators/subscribe.md/subscribe)) — wrap under an ancestor with `serviceURL="
|
|
57
|
+
Scoped `@get` with `@publish` / `@subscribe` on the payload (see [@publish](#docs/_decorators/publish.md/publish) and [@subscribe](#docs/_decorators/subscribe.md/subscribe)) — wrap under an ancestor with `serviceURL="/docs-mock-api/geo/"`:
|
|
56
58
|
|
|
57
59
|
<sonic-code>
|
|
58
60
|
<template>
|
|
59
|
-
<div serviceURL="
|
|
61
|
+
<div serviceURL="/docs-mock-api/geo/">
|
|
62
|
+
<docs-demo-sources for="demo-api-get-publish-subscribe"></docs-demo-sources>
|
|
60
63
|
<demo-api-get-publish-subscribe></demo-api-get-publish-subscribe>
|
|
61
64
|
</div>
|
|
62
65
|
</template>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# @handle
|
|
2
|
+
|
|
3
|
+
Typed callback on one or more `DataProviderKey<T>` paths: invokes the decorated **method** when a publisher assigns a value (calculations, side effects, updating other `@state` properties, etc.).
|
|
4
|
+
|
|
5
|
+
Unlike [@subscribe](#docs/_decorators/subscribe.md/subscribe), nothing is bound to the decorated member — only your method runs. `@handle` is typed and accepts up to **3 keys** plus an optional trailing `HandleOptions` object. It supersedes the string-based [@onAssign](#docs/_decorators/on-assign.md/on-assign).
|
|
6
|
+
|
|
7
|
+
By default the method is called on **every** assignment, even when the value is `null` / `undefined`. Use the options below to restrict that.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
<sonic-code language="typescript">
|
|
12
|
+
<template>
|
|
13
|
+
import { handle, Skip } from "@supersoniks/concorde/decorators";
|
|
14
|
+
import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
|
|
15
|
+
import { get, set } from "@supersoniks/concorde/utils";
|
|
16
|
+
</template>
|
|
17
|
+
</sonic-code>
|
|
18
|
+
|
|
19
|
+
## Basic example
|
|
20
|
+
|
|
21
|
+
<sonic-code language="typescript">
|
|
22
|
+
<template>
|
|
23
|
+
type DemoCounterData = { count: number };
|
|
24
|
+
const demoDataKey = new DataProviderKey<DemoCounterData>("demoData");
|
|
25
|
+
|
|
26
|
+
@customElement("demo-handle")
|
|
27
|
+
export class DemoHandle extends LitElement {
|
|
28
|
+
@state() doubled = 0;
|
|
29
|
+
@state() lastUpdate = "";
|
|
30
|
+
|
|
31
|
+
@handle(demoDataKey.count)
|
|
32
|
+
onCountChange(count: number) {
|
|
33
|
+
this.doubled = count * 2;
|
|
34
|
+
this.lastUpdate = new Date().toLocaleTimeString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
incrementCount() {
|
|
38
|
+
const data = get(demoDataKey);
|
|
39
|
+
set(demoDataKey, { ...data, count: data.count + 1 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render() {
|
|
43
|
+
return html`
|
|
44
|
+
<p>Doubled count: ${this.doubled}</p>
|
|
45
|
+
<p><small>Last update: ${this.lastUpdate}</small></p>
|
|
46
|
+
<sonic-button @click=${this.incrementCount}>Increment</sonic-button>
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</template>
|
|
51
|
+
</sonic-code>
|
|
52
|
+
|
|
53
|
+
<sonic-code>
|
|
54
|
+
<template>
|
|
55
|
+
<docs-demo-sources for="demo-handle"></docs-demo-sources>
|
|
56
|
+
<demo-handle></demo-handle>
|
|
57
|
+
</template>
|
|
58
|
+
</sonic-code>
|
|
59
|
+
|
|
60
|
+
## Dynamic path
|
|
61
|
+
|
|
62
|
+
Placeholders in `DataProviderKey` resolve from the host component’s properties (same rules as `@bind` / `@subscribe`).
|
|
63
|
+
|
|
64
|
+
<sonic-code language="typescript">
|
|
65
|
+
<template>
|
|
66
|
+
type User = { firstName: string; lastName: string; email: string };
|
|
67
|
+
|
|
68
|
+
@customElement("demo-handle-dynamic")
|
|
69
|
+
export class DemoHandleDynamic extends LitElement {
|
|
70
|
+
@property({ type: Number })
|
|
71
|
+
userIndex = 0;
|
|
72
|
+
|
|
73
|
+
@state() displayName = "";
|
|
74
|
+
@state() lastUpdate = "";
|
|
75
|
+
|
|
76
|
+
@handle(new DataProviderKey<User, { userIndex: number }>("demoUsers.${userIndex}"))
|
|
77
|
+
onUserAssigned(user: User) {
|
|
78
|
+
this.displayName = `${user.firstName} ${user.lastName}`;
|
|
79
|
+
this.lastUpdate = new Date().toLocaleTimeString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render() {
|
|
83
|
+
return html`...`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</template>
|
|
87
|
+
</sonic-code>
|
|
88
|
+
|
|
89
|
+
<sonic-code>
|
|
90
|
+
<template>
|
|
91
|
+
<docs-demo-sources for="demo-handle-dynamic"></docs-demo-sources>
|
|
92
|
+
<demo-handle-dynamic></demo-handle-dynamic>
|
|
93
|
+
</template>
|
|
94
|
+
</sonic-code>
|
|
95
|
+
|
|
96
|
+
## Multiple paths
|
|
97
|
+
|
|
98
|
+
`@handle` accepts up to **3 keys**; the method receives one strongly-typed argument per key, in order. Each assignment triggers the method, so make your method safe against partial values (or use `waitForAllDefined`, see below).
|
|
99
|
+
|
|
100
|
+
<sonic-code language="typescript">
|
|
101
|
+
<template>
|
|
102
|
+
type QueueConfig = { onInactivity: { stillHere: { show: boolean } } };
|
|
103
|
+
const config = new DataProviderKey<QueueConfig>("sessionQueueConfig");
|
|
104
|
+
const idle = new DataProviderKey<{ isIdle: boolean }>("idleStatus");
|
|
105
|
+
|
|
106
|
+
@customElement("demo-handle-multi")
|
|
107
|
+
export class DemoHandleMulti extends LitElement {
|
|
108
|
+
|
|
109
|
+
// show: boolean, isIdle: boolean — fully typed from the keys
|
|
110
|
+
@handle(config.onInactivity.stillHere.show, idle.isIdle)
|
|
111
|
+
onInactivity(show: boolean, isIdle: boolean) {
|
|
112
|
+
if (show === true && isIdle === true) this.openModal();
|
|
113
|
+
else this.closeModal();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</template>
|
|
117
|
+
</sonic-code>
|
|
118
|
+
|
|
119
|
+
## Options (`HandleOptions`)
|
|
120
|
+
|
|
121
|
+
Pass an options object as the **last** argument. The names map to real situations seen in the apps.
|
|
122
|
+
|
|
123
|
+
### `waitForAllDefined`
|
|
124
|
+
|
|
125
|
+
Only call the method once **all** watched keys are defined (non `null` / `undefined`). This reproduces the historical `@onAssign` semantics — use it when the logic only makes sense with every source ready (e.g. building a date from `date` + `timeZone` + `direction`).
|
|
126
|
+
|
|
127
|
+
<sonic-code language="typescript">
|
|
128
|
+
<template>
|
|
129
|
+
@handle(trip.departureDate, trip.event.timeZone, form.direction, {
|
|
130
|
+
waitForAllDefined: true,
|
|
131
|
+
})
|
|
132
|
+
updateDepartureDate(date: number, timeZone: string, direction: string) {
|
|
133
|
+
// called only when the three values are all available
|
|
134
|
+
this.formDate = formatDate(date, timeZone, direction);
|
|
135
|
+
}
|
|
136
|
+
</template>
|
|
137
|
+
</sonic-code>
|
|
138
|
+
|
|
139
|
+
### `skip`
|
|
140
|
+
|
|
141
|
+
Do **not** call the method when a received value belongs to one of the listed **categories** (the `Skip` enum). Each entry is a named category — not a value — so there is no value/pattern ambiguity (e.g. `{}` is `Skip.EmptyObject`, an explicit "empty object" category, never a value comparison).
|
|
142
|
+
|
|
143
|
+
| Category | Matches |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `Skip.Nullish` | `null` or `undefined` (a publisher always emits `null`, never `undefined`) |
|
|
146
|
+
| `Skip.EmptyString` | `""` |
|
|
147
|
+
| `Skip.EmptyObject` | object with no keys (`{}`), arrays excluded |
|
|
148
|
+
| `Skip.EmptyArray` | empty array (`[]`) |
|
|
149
|
+
|
|
150
|
+
Useful when a not-yet-initialized publisher emits `{}` as a "loading" state. For a **specific value** (e.g. a particular string), guard inside the method instead.
|
|
151
|
+
|
|
152
|
+
<sonic-code language="typescript">
|
|
153
|
+
<template>
|
|
154
|
+
@handle(user.profile, { skip: [Skip.Nullish, Skip.EmptyObject] })
|
|
155
|
+
onProfile(profile: Profile) {
|
|
156
|
+
// not called while the publisher still holds {} (not loaded yet)
|
|
157
|
+
this.displayName = profile.firstName;
|
|
158
|
+
}
|
|
159
|
+
</template>
|
|
160
|
+
</sonic-code>
|
|
161
|
+
|
|
162
|
+
> Options can be combined, e.g. `@handle(a, b, { waitForAllDefined: true, skip: [Skip.Nullish] })`. For any **arbitrary** validation on a specific value, just guard inside the method (`if (!isValid(v)) return;`) — that is exactly what an `accept`-style predicate would do, since `@handle` only runs your method.
|
|
163
|
+
|
|
164
|
+
## Highlights
|
|
165
|
+
|
|
166
|
+
- Strict typing: the method receives one argument per key, in order.
|
|
167
|
+
- Up to 3 keys; for 4+ keys (rare), keep [@onAssign](#docs/_decorators/on-assign.md/on-assign) for now.
|
|
168
|
+
- By default the method runs on **every** assignment, even with `null` / `undefined` (unlike `@onAssign`, which waits for all values). Opt back into that behavior with `waitForAllDefined`.
|
|
169
|
+
- `skip` filters out values by **named category** (e.g. `[Skip.Nullish, Skip.EmptyObject]`); for arbitrary checks on a specific value, guard inside the method.
|
|
170
|
+
|
|
171
|
+
See also [DataProviderKey](#docs/_misc/dataProviderKey.md/dataProviderKey).
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# @onAssign
|
|
2
2
|
|
|
3
|
+
> **New apps:** use [@handle](#docs/_decorators/handle.md/handle) with `DataProviderKey` ([Data flow](#docs/_core-concept/dataFlow.md/dataFlow)). `@onAssign` uses untyped string paths; it remains documented for existing codebases — see **Migrating to @handle** below.
|
|
4
|
+
|
|
3
5
|
The `@onAssign` decorator allows you to execute a method when one or more publishers are updated. The method is called only when all specified publishers have been assigned values.
|
|
4
6
|
|
|
7
|
+
For a **typed** equivalent (recommended), use [@handle](#docs/_decorators/handle.md/handle).
|
|
8
|
+
|
|
5
9
|
## Principle
|
|
6
10
|
|
|
7
|
-
This decorator subscribes to one or more publishers
|
|
11
|
+
This decorator subscribes to one or more publishers by **string path** (legacy). When all specified publishers have been assigned values (via `set`), the decorated method is called with all the values as arguments. Prefer [@handle](#docs/_decorators/handle.md/handle) + `DataProviderKey` and `get` / `set` from [Data flow](#docs/_core-concept/dataFlow.md/dataFlow).
|
|
8
12
|
|
|
9
13
|
This is particularly useful when you need to wait for multiple data sources to be ready before executing logic.
|
|
10
14
|
|
|
@@ -27,11 +31,11 @@ import { onAssign } from "@supersoniks/concorde/decorators";
|
|
|
27
31
|
@customElement("demo-on-assign")
|
|
28
32
|
export class DemoOnAssign extends LitElement {
|
|
29
33
|
static styles = [tailwind];
|
|
30
|
-
|
|
34
|
+
|
|
31
35
|
@state() userWithSettings: any = null;
|
|
32
36
|
@state() isReady: boolean = false;
|
|
33
37
|
@state() lastUpdate: string = "";
|
|
34
|
-
|
|
38
|
+
|
|
35
39
|
@onAssign("demoUser", "demoUserSettings")
|
|
36
40
|
handleDataReady(user: any, settings: any) {
|
|
37
41
|
this.isReady = Object.keys(user).length > 0 && Object.keys(settings).length > 0;
|
|
@@ -39,12 +43,12 @@ export class DemoOnAssign extends LitElement {
|
|
|
39
43
|
this.lastUpdate = new Date().toLocaleTimeString();
|
|
40
44
|
this.requestUpdate();
|
|
41
45
|
}
|
|
42
|
-
|
|
46
|
+
|
|
43
47
|
render() {
|
|
44
48
|
const { name, email, theme, language } = this.userWithSettings;
|
|
45
49
|
return //...
|
|
46
50
|
}
|
|
47
|
-
|
|
51
|
+
|
|
48
52
|
updateData() {
|
|
49
53
|
const user = PublisherManager.get("demoUser");
|
|
50
54
|
const userSettings = PublisherManager.get("demoUserSettings");
|
|
@@ -53,7 +57,7 @@ export class DemoOnAssign extends LitElement {
|
|
|
53
57
|
name: `User n°${userNumber}`,
|
|
54
58
|
email: `user-${userNumber}@example.com`,
|
|
55
59
|
});
|
|
56
|
-
|
|
60
|
+
|
|
57
61
|
userSettings.set({
|
|
58
62
|
theme: ["light", "dark", "auto"][Math.floor(Math.random() * 3)],
|
|
59
63
|
language: ["en", "fr", "es"][Math.floor(Math.random() * 3)],
|
|
@@ -65,6 +69,7 @@ export class DemoOnAssign extends LitElement {
|
|
|
65
69
|
|
|
66
70
|
<sonic-code>
|
|
67
71
|
<template>
|
|
72
|
+
<docs-demo-sources for="demo-on-assign"></docs-demo-sources>
|
|
68
73
|
<demo-on-assign></demo-on-assign>
|
|
69
74
|
</template>
|
|
70
75
|
</sonic-code>
|
|
@@ -78,17 +83,17 @@ export class DemoOnAssign extends LitElement {
|
|
|
78
83
|
export class ProductView extends LitElement {
|
|
79
84
|
product: any = null;
|
|
80
85
|
inventory: any = null;
|
|
81
|
-
|
|
86
|
+
|
|
82
87
|
@onAssign("store.product", "store.inventory")
|
|
83
88
|
handleProductData(product: any, inventory: any) {
|
|
84
89
|
this.product = product;
|
|
85
90
|
this.inventory = inventory;
|
|
86
91
|
this.requestUpdate();
|
|
87
92
|
}
|
|
88
|
-
|
|
93
|
+
|
|
89
94
|
render() {
|
|
90
95
|
if (!this.product) return html`<div>Loading...</div>`;
|
|
91
|
-
|
|
96
|
+
|
|
92
97
|
const stock = this.inventory[this.product.id] || 0;
|
|
93
98
|
return html`
|
|
94
99
|
<div>
|
|
@@ -129,32 +134,32 @@ Each placeholder is replaced at runtime with the current value of the correspond
|
|
|
129
134
|
@customElement("demo-on-assign-dynamic")
|
|
130
135
|
export class DemoOnAssignDynamic extends LitElement {
|
|
131
136
|
static styles = [tailwind];
|
|
132
|
-
|
|
137
|
+
|
|
133
138
|
@property({ type: String })
|
|
134
139
|
dataProvider: "demoUsers" | "demoUsersAlt" = "demoUsers";
|
|
135
|
-
|
|
140
|
+
|
|
136
141
|
@property({ type: Number })
|
|
137
142
|
userIndex: number = 0;
|
|
138
|
-
|
|
143
|
+
|
|
139
144
|
@state() user: any = null;
|
|
140
145
|
@state() userSettings: any = null;
|
|
141
|
-
|
|
146
|
+
|
|
142
147
|
@onAssign("${dataProvider}.${userIndex}", "${dataProvider}Settings.${userIndex}")
|
|
143
148
|
handleUserDataReady(user: any, settings: any) {
|
|
144
149
|
this.user = user;
|
|
145
150
|
this.userSettings = settings;
|
|
146
151
|
}
|
|
147
|
-
|
|
152
|
+
|
|
148
153
|
updateUserIndex(e: Event) {
|
|
149
154
|
this.userIndex = parseInt((e.target as HTMLInputElement).value);
|
|
150
155
|
}
|
|
151
|
-
|
|
156
|
+
|
|
152
157
|
updateDataProvider(e: Event) {
|
|
153
158
|
this.dataProvider = (e.target as HTMLSelectElement).value as
|
|
154
159
|
| "demoUsers"
|
|
155
160
|
| "demoUsersAlt";
|
|
156
161
|
}
|
|
157
|
-
|
|
162
|
+
|
|
158
163
|
updateCurrentUserData() {
|
|
159
164
|
const usersPublisher = PublisherManager.get(this.dataProvider);
|
|
160
165
|
const settingsPublisher = PublisherManager.get(
|
|
@@ -168,7 +173,7 @@ export class DemoOnAssignDynamic extends LitElement {
|
|
|
168
173
|
settingsPublisher,
|
|
169
174
|
[String(this.userIndex)]
|
|
170
175
|
) as PublisherProxy;
|
|
171
|
-
|
|
176
|
+
|
|
172
177
|
if (userPublisher && settingPublisher) {
|
|
173
178
|
// Générer de nouvelles données aléatoires
|
|
174
179
|
const randomNames = [
|
|
@@ -178,7 +183,7 @@ export class DemoOnAssignDynamic extends LitElement {
|
|
|
178
183
|
];
|
|
179
184
|
const randomThemes = ["light", "dark", "auto"];
|
|
180
185
|
const randomLanguages = ["en", "fr", "es"];
|
|
181
|
-
|
|
186
|
+
|
|
182
187
|
const randomName =
|
|
183
188
|
randomNames[Math.floor(Math.random() * randomNames.length)];
|
|
184
189
|
const randomEmail = `${randomName.firstName.toLowerCase()}.${randomName.lastName.toLowerCase()}@example.com`;
|
|
@@ -186,7 +191,7 @@ export class DemoOnAssignDynamic extends LitElement {
|
|
|
186
191
|
randomThemes[Math.floor(Math.random() * randomThemes.length)];
|
|
187
192
|
const randomLanguage =
|
|
188
193
|
randomLanguages[Math.floor(Math.random() * randomLanguages.length)];
|
|
189
|
-
|
|
194
|
+
|
|
190
195
|
// Mettre à jour l'utilisateur directement
|
|
191
196
|
const currentUser = userPublisher.get() || {};
|
|
192
197
|
userPublisher.set({
|
|
@@ -195,7 +200,7 @@ export class DemoOnAssignDynamic extends LitElement {
|
|
|
195
200
|
lastName: randomName.lastName,
|
|
196
201
|
email: randomEmail,
|
|
197
202
|
});
|
|
198
|
-
|
|
203
|
+
|
|
199
204
|
// Mettre à jour les settings directement
|
|
200
205
|
settingPublisher.set({
|
|
201
206
|
theme: randomTheme,
|
|
@@ -203,15 +208,15 @@ export class DemoOnAssignDynamic extends LitElement {
|
|
|
203
208
|
});
|
|
204
209
|
}
|
|
205
210
|
}
|
|
206
|
-
|
|
211
|
+
|
|
207
212
|
render() {
|
|
208
213
|
return html`
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
<div class="flex flex-col gap-2">
|
|
215
|
+
<sonic-select label="Users set" @change=${this.updateDataProvider}>
|
|
216
|
+
<option value="demoUsers">First set of users</option>
|
|
217
|
+
<option value="demoUsersAlt">Second set of users</option>
|
|
218
|
+
</sonic-select>
|
|
219
|
+
<sonic-input
|
|
215
220
|
type="number"
|
|
216
221
|
.value=${this.userIndex}
|
|
217
222
|
@input=${this.updateUserIndex}
|
|
@@ -219,26 +224,25 @@ export class DemoOnAssignDynamic extends LitElement {
|
|
|
219
224
|
max="9"
|
|
220
225
|
label="Index"
|
|
221
226
|
class="block"
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<sonic-icon name="user" library="heroicons"></sonic-icon>
|
|
227
|
+
></sonic-input>
|
|
228
|
+
<sonic-button @click=${this.updateCurrentUserData}>
|
|
229
|
+
Update current user data
|
|
230
|
+
</sonic-button>
|
|
231
|
+
<div class="flex flex-col gap-2 border p-2">
|
|
232
|
+
<div>
|
|
233
|
+
<sonic-icon name="user" library="heroicons"></sonic-icon>
|
|
230
234
|
${this.user?.firstName} ${this.user?.lastName}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
</div>
|
|
236
|
+
<div>
|
|
237
|
+
<sonic-icon name="envelope" library="heroicons"></sonic-icon>
|
|
234
238
|
${this.user?.email}
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
237
241
|
Theme: ${this.userSettings?.theme} | Language:
|
|
238
242
|
${this.userSettings?.language}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
242
246
|
`;
|
|
243
247
|
}
|
|
244
248
|
}
|
|
@@ -247,6 +251,7 @@ export class DemoOnAssignDynamic extends LitElement {
|
|
|
247
251
|
|
|
248
252
|
<sonic-code>
|
|
249
253
|
<template>
|
|
254
|
+
<docs-demo-sources for="demo-on-assign-dynamic"></docs-demo-sources>
|
|
250
255
|
<demo-on-assign-dynamic></demo-on-assign-dynamic>
|
|
251
256
|
</template>
|
|
252
257
|
</sonic-code>
|
|
@@ -285,13 +290,13 @@ import { html, LitElement } from "lit";
|
|
|
285
290
|
import { customElement } from "lit/decorators.js";
|
|
286
291
|
import { onAssign } from "@supersoniks/concorde/decorators";
|
|
287
292
|
import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
|
|
288
|
-
|
|
293
|
+
|
|
289
294
|
@customElement("order-summary")
|
|
290
295
|
export class OrderSummary extends LitElement {
|
|
291
296
|
order: any = null;
|
|
292
297
|
customer: any = null;
|
|
293
298
|
shipping: any = null;
|
|
294
|
-
|
|
299
|
+
|
|
295
300
|
@onAssign("orderData", "customerData", "shippingData")
|
|
296
301
|
handleOrderReady(order: any, customer: any, shipping: any) {
|
|
297
302
|
this.order = order;
|
|
@@ -299,12 +304,12 @@ export class OrderSummary extends LitElement {
|
|
|
299
304
|
this.shipping = shipping;
|
|
300
305
|
this.requestUpdate();
|
|
301
306
|
}
|
|
302
|
-
|
|
307
|
+
|
|
303
308
|
render() {
|
|
304
309
|
if (!this.order || !this.customer || !this.shipping) {
|
|
305
310
|
return html`<div>Loading order details...</div>`;
|
|
306
311
|
}
|
|
307
|
-
|
|
312
|
+
|
|
308
313
|
return html`
|
|
309
314
|
<div class="order-summary">
|
|
310
315
|
<h2>Order #${this.order.id}</h2>
|
|
@@ -327,6 +332,53 @@ shippingPub.set({ address: "123 Main St" });
|
|
|
327
332
|
</template>
|
|
328
333
|
</sonic-code>
|
|
329
334
|
|
|
335
|
+
## Migrating to @handle
|
|
336
|
+
|
|
337
|
+
`@handle` is the typed successor of `@onAssign`. The key behavioral difference: `@onAssign` waits for **all** values to be defined before calling the method, whereas `@handle` calls it on **every** assignment by default. Use the `waitForAllDefined` option to keep the old semantics.
|
|
338
|
+
|
|
339
|
+
### Why migrate
|
|
340
|
+
|
|
341
|
+
- **Typed paths**: keys are `DataProviderKey<T>`, so the method arguments are strongly typed (no more `any`).
|
|
342
|
+
- **Explicit intent**: `waitForAllDefined` and `skip` replace implicit behavior.
|
|
343
|
+
- **Single API**: `@handle` covers the mono- and multi-path cases (up to 3 keys).
|
|
344
|
+
|
|
345
|
+
### Equivalent semantics (`waitForAllDefined`)
|
|
346
|
+
|
|
347
|
+
<sonic-code language="typescript">
|
|
348
|
+
<template>
|
|
349
|
+
// Before
|
|
350
|
+
@onAssign("demoUser", "demoUserSettings")
|
|
351
|
+
handleDataReady(user: any, settings: any) { /* ... */ }
|
|
352
|
+
|
|
353
|
+
// After — same "wait for everything" behavior, but typed
|
|
354
|
+
const user = new DataProviderKey<User>("demoUser");
|
|
355
|
+
const settings = new DataProviderKey<Settings>("demoUserSettings");
|
|
356
|
+
|
|
357
|
+
@handle(user, settings, { waitForAllDefined: true })
|
|
358
|
+
handleDataReady(user: User, settings: Settings) { /* ... */ }
|
|
359
|
+
</template>
|
|
360
|
+
</sonic-code>
|
|
361
|
+
|
|
362
|
+
### Single path
|
|
363
|
+
|
|
364
|
+
<sonic-code language="typescript">
|
|
365
|
+
<template>
|
|
366
|
+
// Before
|
|
367
|
+
@onAssign("settings.modules.logs_route.enabled")
|
|
368
|
+
onLogRoute(value: boolean) { /* ... */ }
|
|
369
|
+
|
|
370
|
+
// After
|
|
371
|
+
const settings = new DataProviderKey<AppSettings>("settings");
|
|
372
|
+
|
|
373
|
+
@handle(settings.modules.logs_route.enabled)
|
|
374
|
+
onLogRoute(value: boolean) { /* ... */ }
|
|
375
|
+
</template>
|
|
376
|
+
</sonic-code>
|
|
377
|
+
|
|
378
|
+
### 4+ paths
|
|
379
|
+
|
|
380
|
+
`@handle` is capped at 3 keys. For the rare case of 4 or more publishers, keep `@onAssign` for now, or split the logic into several `@handle` methods that each store their value and call a shared method (guarding against partial values).
|
|
381
|
+
|
|
330
382
|
## Notes
|
|
331
383
|
|
|
332
384
|
- This decorator works with any component that has `connectedCallback` and `disconnectedCallback` methods (such as `LitElement` or components extending `Subscriber`)
|
|
@@ -38,7 +38,7 @@ export class DemoPublish extends LitElement {
|
|
|
38
38
|
@input=${(e) => (this.email = (e.target as HTMLInputElement).value)}
|
|
39
39
|
label="Email"
|
|
40
40
|
></sonic-input>
|
|
41
|
-
<p>${sub(
|
|
41
|
+
<p>${sub(publishDemoKey.email)}</p>
|
|
42
42
|
`;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -47,6 +47,7 @@ export class DemoPublish extends LitElement {
|
|
|
47
47
|
|
|
48
48
|
<sonic-code>
|
|
49
49
|
<template>
|
|
50
|
+
<docs-demo-sources for="demo-publish"></docs-demo-sources>
|
|
50
51
|
<demo-publish></demo-publish>
|
|
51
52
|
</template>
|
|
52
53
|
</sonic-code>
|