@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.
Files changed (185) hide show
  1. package/.gitlab-ci.yml +23 -0
  2. package/README.md +106 -55
  3. package/ai/AGENTS.md +52 -0
  4. package/ai/README.md +30 -0
  5. package/ai/cursor/rules/concorde-menu.mdc +15 -0
  6. package/ai/cursor/rules/concorde-scope.mdc +14 -0
  7. package/ai/cursor/rules/concorde-theme.mdc +13 -0
  8. package/ai/cursor/rules/concorde.mdc +49 -0
  9. package/ai/jetbrains/rules/concorde.md +39 -0
  10. package/ai/skills/concorde/SKILL.md +220 -0
  11. package/ai/skills/concorde-get-set-dp/SKILL.md +194 -0
  12. package/ai/skills/concorde-imports/SKILL.md +78 -0
  13. package/ai/skills/concorde-menu/SKILL.md +74 -0
  14. package/ai/skills/concorde-scope/SKILL.md +70 -0
  15. package/ai/skills/concorde-theme/SKILL.md +46 -0
  16. package/build-infos.json +1 -1
  17. package/concorde-core.bundle.js +159 -159
  18. package/concorde-core.es.js +1915 -1809
  19. package/dist/altcha-widget.js +2662 -0
  20. package/dist/concorde-core.bundle.js +159 -159
  21. package/dist/concorde-core.es.js +1915 -1809
  22. package/dist/docs-mock-api-sw.js +589 -0
  23. package/dist/docs-mock-api-sw.js.map +7 -0
  24. package/docs/altcha-widget.js +2662 -0
  25. package/docs/assets/index-D9pxaQYK.js +7508 -0
  26. package/docs/assets/index-t0-i22oI.css +1 -0
  27. package/docs/docs-mock-api-sw.js +589 -0
  28. package/docs/docs-mock-api-sw.js.map +7 -0
  29. package/docs/index.html +2 -2
  30. package/docs/src/core/components/functional/fetch/fetch.md +13 -11
  31. package/docs/src/core/components/functional/if/if.md +4 -11
  32. package/docs/src/core/components/functional/list/list.md +60 -194
  33. package/docs/src/core/components/functional/queue/queue.md +70 -85
  34. package/docs/src/core/components/functional/router/router.md +62 -97
  35. package/docs/src/core/components/functional/states/states.md +2 -2
  36. package/docs/src/core/components/functional/submit/submit.md +86 -55
  37. package/docs/src/core/components/ui/captcha/captcha.md +2 -2
  38. package/docs/src/core/components/ui/card/card.md +1 -1
  39. package/docs/src/core/components/ui/form/checkbox/checkbox.md +5 -32
  40. package/docs/src/core/components/ui/form/input/input.md +5 -30
  41. package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
  42. package/docs/src/core/components/ui/form/radio/radio.md +5 -32
  43. package/docs/src/core/components/ui/form/select/select.md +5 -31
  44. package/docs/src/core/components/ui/form/switch/switch.md +5 -32
  45. package/docs/src/core/components/ui/loader/loader.md +1 -13
  46. package/docs/src/core/components/ui/table/table.md +3 -3
  47. package/docs/src/docs/_core-concept/dataFlow.md +73 -0
  48. package/docs/src/docs/_core-concept/subscriber.md +9 -10
  49. package/docs/src/docs/_decorators/ancestor-attribute.md +4 -3
  50. package/docs/src/docs/_decorators/auto-subscribe.md +19 -16
  51. package/docs/src/docs/_decorators/bind.md +20 -17
  52. package/docs/src/docs/_decorators/get.md +7 -4
  53. package/docs/src/docs/_decorators/handle.md +171 -0
  54. package/docs/src/docs/_decorators/on-assign.md +99 -73
  55. package/docs/src/docs/_decorators/publish.md +2 -1
  56. package/docs/src/docs/_decorators/subscribe.md +70 -9
  57. package/docs/src/docs/_decorators/wait-for-ancestors.md +13 -10
  58. package/docs/src/docs/_directives/sub.md +91 -0
  59. package/docs/src/docs/_getting-started/ai-agents.md +56 -0
  60. package/docs/src/docs/_getting-started/concorde-manual-install.md +133 -0
  61. package/docs/src/docs/_getting-started/concorde-outside.md +13 -123
  62. package/docs/src/docs/_getting-started/create-a-component.md +2 -0
  63. package/docs/src/docs/_getting-started/my-first-component.md +236 -0
  64. package/docs/src/docs/_getting-started/my-first-subscriber.md +29 -83
  65. package/docs/src/docs/_getting-started/pubsub.md +21 -134
  66. package/docs/src/docs/_getting-started/start.md +26 -18
  67. package/docs/src/docs/_misc/api-configuration.md +79 -0
  68. package/docs/src/docs/_misc/dataProviderKey.md +38 -5
  69. package/docs/src/docs/_misc/docs-mock-api.md +60 -0
  70. package/docs/src/docs/_misc/endpoint.md +2 -1
  71. package/docs/src/docs/_misc/html-integration.md +13 -0
  72. package/docs/src/docs/search/docs-search.json +4163 -873
  73. package/docs/src/tsconfig.json +380 -317
  74. package/gitlab/job_tests.sh +55 -0
  75. package/package.json +37 -3
  76. package/public/altcha-widget.js +2662 -0
  77. package/public/docs-mock-api-sw.js +589 -0
  78. package/public/docs-mock-api-sw.js.map +7 -0
  79. package/scripts/ai-init.mjs +167 -0
  80. package/scripts/docs-mock-api-vite-plugin.ts +116 -0
  81. package/scripts/docs-open-in-editor-plugin.ts +130 -0
  82. package/scripts/pre-publish.mjs +2 -1
  83. package/src/core/components/functional/example/example.ts +1 -1
  84. package/src/core/components/functional/fetch/fetch.md +13 -11
  85. package/src/core/components/functional/if/if.md +4 -11
  86. package/src/core/components/functional/list/list.demo.ts +4 -4
  87. package/src/core/components/functional/list/list.md +60 -194
  88. package/src/core/components/functional/list/list.ts +8 -7
  89. package/src/core/components/functional/queue/queue.demo.ts +1 -1
  90. package/src/core/components/functional/queue/queue.md +70 -85
  91. package/src/core/components/functional/queue/queue.ts +4 -4
  92. package/src/core/components/functional/router/router.md +62 -97
  93. package/src/core/components/functional/router/router.ts +1 -1
  94. package/src/core/components/functional/states/states.md +2 -2
  95. package/src/core/components/functional/submit/submit.md +86 -55
  96. package/src/core/components/functional/submit/submit.ts +10 -3
  97. package/src/core/components/ui/captcha/captcha.md +2 -2
  98. package/src/core/components/ui/card/card.md +1 -1
  99. package/src/core/components/ui/form/checkbox/checkbox.md +5 -32
  100. package/src/core/components/ui/form/input/input.md +5 -30
  101. package/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
  102. package/src/core/components/ui/form/radio/radio.md +5 -32
  103. package/src/core/components/ui/form/select/select.md +5 -31
  104. package/src/core/components/ui/form/switch/switch.md +5 -32
  105. package/src/core/components/ui/loader/loader.md +1 -13
  106. package/src/core/components/ui/table/table.md +3 -3
  107. package/src/core/decorators/Subscriber.ts +2 -0
  108. package/src/core/decorators/subscriber/handle.disambig.spec.ts +20 -0
  109. package/src/core/decorators/subscriber/handle.skip.spec.ts +37 -0
  110. package/src/core/decorators/subscriber/handle.ts +128 -0
  111. package/src/core/decorators/subscriber/onAssign.ts +94 -4
  112. package/src/core/directives/DataProvider.sub.spec.ts +96 -0
  113. package/src/core/directives/DataProvider.ts +109 -40
  114. package/src/core/utils/PublisherProxy.ts +33 -18
  115. package/src/core/utils/dataProviderKey.ts +23 -0
  116. package/src/core/utils/publisherPathKey.spec.ts +58 -0
  117. package/src/decorators.ts +6 -0
  118. package/src/docs/_core-concept/dataFlow.md +73 -0
  119. package/src/docs/_core-concept/subscriber.md +9 -10
  120. package/src/docs/_decorators/ancestor-attribute.md +4 -3
  121. package/src/docs/_decorators/auto-subscribe.md +19 -16
  122. package/src/docs/_decorators/bind.md +20 -17
  123. package/src/docs/_decorators/get.md +7 -4
  124. package/src/docs/_decorators/handle.md +171 -0
  125. package/src/docs/_decorators/on-assign.md +99 -47
  126. package/src/docs/_decorators/publish.md +2 -1
  127. package/src/docs/_decorators/subscribe.md +70 -9
  128. package/src/docs/_decorators/wait-for-ancestors.md +13 -10
  129. package/src/docs/_directives/sub.md +91 -0
  130. package/src/docs/_getting-started/ai-agents.md +56 -0
  131. package/src/docs/_getting-started/concorde-manual-install.md +133 -0
  132. package/src/docs/_getting-started/concorde-outside.md +13 -123
  133. package/src/docs/_getting-started/create-a-component.md +2 -0
  134. package/src/docs/_getting-started/my-first-component.md +236 -0
  135. package/src/docs/_getting-started/my-first-subscriber.md +29 -83
  136. package/src/docs/_getting-started/pubsub.md +21 -134
  137. package/src/docs/_getting-started/start.md +26 -18
  138. package/src/docs/_misc/api-configuration.md +79 -0
  139. package/src/docs/_misc/dataProviderKey.md +38 -5
  140. package/src/docs/_misc/docs-mock-api.md +60 -0
  141. package/src/docs/_misc/endpoint.md +2 -1
  142. package/src/docs/_misc/html-integration.md +13 -0
  143. package/src/docs/code.ts +58 -12
  144. package/src/docs/components/docs-demo-sources.ts +397 -0
  145. package/src/docs/components/docs-lit-demo-raw.ts +28 -0
  146. package/src/docs/components/docs-lit-demo.ts +166 -0
  147. package/src/docs/components/docs-source-link.ts +72 -0
  148. package/src/docs/docs-location.ts +54 -0
  149. package/src/docs/docs.ts +12 -0
  150. package/src/docs/example/decorators-demo-bind-demos.ts +41 -46
  151. package/src/docs/example/decorators-demo-geo.ts +16 -11
  152. package/src/docs/example/decorators-demo-init.ts +2 -228
  153. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +142 -12
  154. package/src/docs/example/decorators-demo.ts +71 -70
  155. package/src/docs/example/docs-api-config-demos.ts +234 -0
  156. package/src/docs/example/docs-joke-demos.ts +297 -0
  157. package/src/docs/example/docs-list-demos.ts +179 -0
  158. package/src/docs/example/docs-provider-keys.ts +315 -0
  159. package/src/docs/example/docs-queue-demos.ts +114 -0
  160. package/src/docs/example/docs-router-demos.ts +89 -0
  161. package/src/docs/example/docs-submit-demos.ts +455 -0
  162. package/src/docs/example/docs-toggle-demos.ts +73 -0
  163. package/src/docs/example/docs-user-two-scopes.ts +37 -0
  164. package/src/docs/example/docs-users-list.ts +71 -0
  165. package/src/docs/example/users.ts +41 -24
  166. package/src/docs/mock-api/api-config-mock.ts +152 -0
  167. package/src/docs/mock-api/fixtures.ts +377 -0
  168. package/src/docs/mock-api/register.ts +25 -0
  169. package/src/docs/mock-api/router.ts +234 -0
  170. package/src/docs/mock-api/service-worker.ts +23 -0
  171. package/src/docs/mock-api/urls.ts +11 -0
  172. package/src/docs/navigation/navigation.ts +43 -7
  173. package/src/docs/search/docs-search.json +4193 -858
  174. package/src/docs/search/markdown-renderer.ts +7 -3
  175. package/src/docs/search/page.ts +11 -14
  176. package/src/docs/search/sonic-code-markdown.spec.ts +29 -0
  177. package/src/docs/search/sonic-code-markdown.ts +28 -0
  178. package/src/docs.ts +4 -0
  179. package/src/tsconfig.json +96 -0
  180. package/src/tsconfig.tsbuildinfo +1 -1
  181. package/vite.config.mts +8 -0
  182. package/docs/assets/index-CaysOMFz.js +0 -5046
  183. package/docs/assets/index-D8mGoXzF.css +0 -1
  184. package/docs/src/docs/_misc/templates-demo.md +0 -19
  185. package/src/docs/_misc/templates-demo.md +0 -19
@@ -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) (`geo.api.gouv.fr`). Publisher setup lives in `decorators-demo-geo.ts` and `decorators-demo-subscribe-publish-get-demos.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="https://geo.api.gouv.fr/"`:
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="https://geo.api.gouv.fr/">
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&lt;DemoCounterData&gt;("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
+ &lt;p&gt;Doubled count: ${this.doubled}&lt;/p&gt;
45
+ &lt;p&gt;&lt;small&gt;Last update: ${this.lastUpdate}&lt;/small&gt;&lt;/p&gt;
46
+ &lt;sonic-button @click=${this.incrementCount}&gt;Increment&lt;/sonic-button&gt;
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&lt;User, { userIndex: number }&gt;("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&lt;QueueConfig&gt;("sessionQueueConfig");
104
+ const idle = new DataProviderKey&lt;{ isIdle: boolean }&gt;("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 via the `PublisherManager`. When all specified publishers have been assigned values (via `set`), the decorated method is called with all the values as arguments.
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
 
@@ -15,7 +19,6 @@ This is particularly useful when you need to wait for multiple data sources to b
15
19
  <sonic-code language="typescript">
16
20
  <template>
17
21
  import { onAssign } from "@supersoniks/concorde/decorators";
18
- import { DataProviderKey } from "@supersoniks/concorde/decorators";
19
22
  </template>
20
23
  </sonic-code>
21
24
 
@@ -28,11 +31,11 @@ import { DataProviderKey } from "@supersoniks/concorde/decorators";
28
31
  @customElement("demo-on-assign")
29
32
  export class DemoOnAssign extends LitElement {
30
33
  static styles = [tailwind];
31
- //
34
+
32
35
  @state() userWithSettings: any = null;
33
36
  @state() isReady: boolean = false;
34
37
  @state() lastUpdate: string = "";
35
- //
38
+
36
39
  @onAssign("demoUser", "demoUserSettings")
37
40
  handleDataReady(user: any, settings: any) {
38
41
  this.isReady = Object.keys(user).length > 0 && Object.keys(settings).length > 0;
@@ -40,12 +43,12 @@ export class DemoOnAssign extends LitElement {
40
43
  this.lastUpdate = new Date().toLocaleTimeString();
41
44
  this.requestUpdate();
42
45
  }
43
- //
46
+
44
47
  render() {
45
48
  const { name, email, theme, language } = this.userWithSettings;
46
49
  return //...
47
50
  }
48
- //
51
+
49
52
  updateData() {
50
53
  const user = PublisherManager.get("demoUser");
51
54
  const userSettings = PublisherManager.get("demoUserSettings");
@@ -54,7 +57,7 @@ export class DemoOnAssign extends LitElement {
54
57
  name: `User n°${userNumber}`,
55
58
  email: `user-${userNumber}@example.com`,
56
59
  });
57
- //
60
+
58
61
  userSettings.set({
59
62
  theme: ["light", "dark", "auto"][Math.floor(Math.random() * 3)],
60
63
  language: ["en", "fr", "es"][Math.floor(Math.random() * 3)],
@@ -66,6 +69,7 @@ export class DemoOnAssign extends LitElement {
66
69
 
67
70
  <sonic-code>
68
71
  <template>
72
+ <docs-demo-sources for="demo-on-assign"></docs-demo-sources>
69
73
  <demo-on-assign></demo-on-assign>
70
74
  </template>
71
75
  </sonic-code>
@@ -79,17 +83,17 @@ export class DemoOnAssign extends LitElement {
79
83
  export class ProductView extends LitElement {
80
84
  product: any = null;
81
85
  inventory: any = null;
82
- //
86
+
83
87
  @onAssign("store.product", "store.inventory")
84
88
  handleProductData(product: any, inventory: any) {
85
89
  this.product = product;
86
90
  this.inventory = inventory;
87
91
  this.requestUpdate();
88
92
  }
89
- //
93
+
90
94
  render() {
91
95
  if (!this.product) return html`<div>Loading...</div>`;
92
- //
96
+
93
97
  const stock = this.inventory[this.product.id] || 0;
94
98
  return html`
95
99
  <div>
@@ -103,31 +107,6 @@ export class ProductView extends LitElement {
103
107
  </template>
104
108
  </sonic-code>
105
109
 
106
- ### Example with `DataProviderKey` (type-safe)
107
-
108
- <sonic-code language="typescript">
109
- <template>
110
- type User = { id: string; name: string };
111
- type Settings = { theme: "light" | "dark" };
112
-
113
- const userKey = new DataProviderKey<User>("demoUser");
114
- const settingsKey = new DataProviderKey<Settings>("demoUserSettings");
115
-
116
- @customElement("demo-on-assign-typed")
117
- export class DemoOnAssignTyped extends LitElement {
118
- @state() user: User | null = null;
119
- @state() settings: Settings | null = null;
120
-
121
- @onAssign(userKey, settingsKey)
122
- handleReady(user: User, settings: Settings) {
123
- this.user = user;
124
- this.settings = settings;
125
- this.requestUpdate();
126
- }
127
- }
128
- </template>
129
- </sonic-code>
130
-
131
110
  ## Path syntax
132
111
 
133
112
  The path uses dot notation to navigate through the publisher structure:
@@ -155,32 +134,32 @@ Each placeholder is replaced at runtime with the current value of the correspond
155
134
  @customElement("demo-on-assign-dynamic")
156
135
  export class DemoOnAssignDynamic extends LitElement {
157
136
  static styles = [tailwind];
158
- //
137
+
159
138
  @property({ type: String })
160
139
  dataProvider: "demoUsers" | "demoUsersAlt" = "demoUsers";
161
- //
140
+
162
141
  @property({ type: Number })
163
142
  userIndex: number = 0;
164
- //
143
+
165
144
  @state() user: any = null;
166
145
  @state() userSettings: any = null;
167
- //
146
+
168
147
  @onAssign("${dataProvider}.${userIndex}", "${dataProvider}Settings.${userIndex}")
169
148
  handleUserDataReady(user: any, settings: any) {
170
149
  this.user = user;
171
150
  this.userSettings = settings;
172
151
  }
173
- //
152
+
174
153
  updateUserIndex(e: Event) {
175
154
  this.userIndex = parseInt((e.target as HTMLInputElement).value);
176
155
  }
177
- //
156
+
178
157
  updateDataProvider(e: Event) {
179
158
  this.dataProvider = (e.target as HTMLSelectElement).value as
180
159
  | "demoUsers"
181
160
  | "demoUsersAlt";
182
161
  }
183
- //
162
+
184
163
  updateCurrentUserData() {
185
164
  const usersPublisher = PublisherManager.get(this.dataProvider);
186
165
  const settingsPublisher = PublisherManager.get(
@@ -194,7 +173,7 @@ export class DemoOnAssignDynamic extends LitElement {
194
173
  settingsPublisher,
195
174
  [String(this.userIndex)]
196
175
  ) as PublisherProxy;
197
- //
176
+
198
177
  if (userPublisher && settingPublisher) {
199
178
  // Générer de nouvelles données aléatoires
200
179
  const randomNames = [
@@ -204,7 +183,7 @@ export class DemoOnAssignDynamic extends LitElement {
204
183
  ];
205
184
  const randomThemes = ["light", "dark", "auto"];
206
185
  const randomLanguages = ["en", "fr", "es"];
207
- //
186
+
208
187
  const randomName =
209
188
  randomNames[Math.floor(Math.random() * randomNames.length)];
210
189
  const randomEmail = `${randomName.firstName.toLowerCase()}.${randomName.lastName.toLowerCase()}@example.com`;
@@ -212,7 +191,7 @@ export class DemoOnAssignDynamic extends LitElement {
212
191
  randomThemes[Math.floor(Math.random() * randomThemes.length)];
213
192
  const randomLanguage =
214
193
  randomLanguages[Math.floor(Math.random() * randomLanguages.length)];
215
- //
194
+
216
195
  // Mettre à jour l'utilisateur directement
217
196
  const currentUser = userPublisher.get() || {};
218
197
  userPublisher.set({
@@ -221,7 +200,7 @@ export class DemoOnAssignDynamic extends LitElement {
221
200
  lastName: randomName.lastName,
222
201
  email: randomEmail,
223
202
  });
224
- //
203
+
225
204
  // Mettre à jour les settings directement
226
205
  settingPublisher.set({
227
206
  theme: randomTheme,
@@ -229,15 +208,15 @@ export class DemoOnAssignDynamic extends LitElement {
229
208
  });
230
209
  }
231
210
  }
232
- //
211
+
233
212
  render() {
234
213
  return html`
235
- <div class="flex flex-col gap-2">
236
- <sonic-select label="Users set" @change=${this.updateDataProvider}>
237
- <option value="demoUsers">First set of users</option>
238
- <option value="demoUsersAlt">Second set of users</option>
239
- </sonic-select>
240
- <sonic-input
214
+ &lt;div class="flex flex-col gap-2"&gt;
215
+ &lt;sonic-select label="Users set" @change=${this.updateDataProvider}&gt;
216
+ &lt;option value="demoUsers"&gt;First set of users&lt;/option&gt;
217
+ &lt;option value="demoUsersAlt"&gt;Second set of users&lt;/option&gt;
218
+ &lt;/sonic-select&gt;
219
+ &lt;sonic-input
241
220
  type="number"
242
221
  .value=${this.userIndex}
243
222
  @input=${this.updateUserIndex}
@@ -245,26 +224,25 @@ export class DemoOnAssignDynamic extends LitElement {
245
224
  max="9"
246
225
  label="Index"
247
226
  class="block"
248
- >
249
- </sonic-input>
250
- <sonic-button @click=${this.updateCurrentUserData}
251
- >Update current user data</sonic-button
252
- >
253
- <div class="flex flex-col gap-2 border p-2">
254
- <div>
255
- <sonic-icon name="user" library="heroicons"></sonic-icon>
227
+ &gt;&lt;/sonic-input&gt;
228
+ &lt;sonic-button @click=${this.updateCurrentUserData}&gt;
229
+ Update current user data
230
+ &lt;/sonic-button&gt;
231
+ &lt;div class="flex flex-col gap-2 border p-2"&gt;
232
+ &lt;div&gt;
233
+ &lt;sonic-icon name="user" library="heroicons"&gt;&lt;/sonic-icon&gt;
256
234
  ${this.user?.firstName} ${this.user?.lastName}
257
- </div>
258
- <div>
259
- <sonic-icon name="envelope" library="heroicons"></sonic-icon>
235
+ &lt;/div&gt;
236
+ &lt;div&gt;
237
+ &lt;sonic-icon name="envelope" library="heroicons"&gt;&lt;/sonic-icon&gt;
260
238
  ${this.user?.email}
261
- </div>
262
- <div>
239
+ &lt;/div&gt;
240
+ &lt;div&gt;
263
241
  Theme: ${this.userSettings?.theme} | Language:
264
242
  ${this.userSettings?.language}
265
- </div>
266
- </div>
267
- </div>
243
+ &lt;/div&gt;
244
+ &lt;/div&gt;
245
+ &lt;/div&gt;
268
246
  `;
269
247
  }
270
248
  }
@@ -273,6 +251,7 @@ export class DemoOnAssignDynamic extends LitElement {
273
251
 
274
252
  <sonic-code>
275
253
  <template>
254
+ <docs-demo-sources for="demo-on-assign-dynamic"></docs-demo-sources>
276
255
  <demo-on-assign-dynamic></demo-on-assign-dynamic>
277
256
  </template>
278
257
  </sonic-code>
@@ -311,13 +290,13 @@ import { html, LitElement } from "lit";
311
290
  import { customElement } from "lit/decorators.js";
312
291
  import { onAssign } from "@supersoniks/concorde/decorators";
313
292
  import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
314
- //
293
+
315
294
  @customElement("order-summary")
316
295
  export class OrderSummary extends LitElement {
317
296
  order: any = null;
318
297
  customer: any = null;
319
298
  shipping: any = null;
320
- //
299
+
321
300
  @onAssign("orderData", "customerData", "shippingData")
322
301
  handleOrderReady(order: any, customer: any, shipping: any) {
323
302
  this.order = order;
@@ -325,12 +304,12 @@ export class OrderSummary extends LitElement {
325
304
  this.shipping = shipping;
326
305
  this.requestUpdate();
327
306
  }
328
- //
307
+
329
308
  render() {
330
309
  if (!this.order || !this.customer || !this.shipping) {
331
310
  return html`<div>Loading order details...</div>`;
332
311
  }
333
- //
312
+
334
313
  return html`
335
314
  <div class="order-summary">
336
315
  <h2>Order #${this.order.id}</h2>
@@ -353,6 +332,53 @@ shippingPub.set({ address: "123 Main St" });
353
332
  </template>
354
333
  </sonic-code>
355
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&lt;User&gt;("demoUser");
355
+ const settings = new DataProviderKey&lt;Settings&gt;("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&lt;AppSettings&gt;("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
+
356
382
  ## Notes
357
383
 
358
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("publishDemo.email")}</p>
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>