@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
@@ -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 via `PublisherManager` using dot notation. Publisher updates flow into the decorated property.
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
- const demoData = PublisherManager.get("demoData");
47
- const demoUsers = PublisherManager.get("demoUsers");
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-bind-reflect>
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) (`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
 
@@ -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
- <div class="flex flex-col gap-2">
210
- <sonic-select label="Users set" @change=${this.updateDataProvider}>
211
- <option value="demoUsers">First set of users</option>
212
- <option value="demoUsersAlt">Second set of users</option>
213
- </sonic-select>
214
- <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
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
- </sonic-input>
224
- <sonic-button @click=${this.updateCurrentUserData}
225
- >Update current user data</sonic-button
226
- >
227
- <div class="flex flex-col gap-2 border p-2">
228
- <div>
229
- <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;
230
234
  ${this.user?.firstName} ${this.user?.lastName}
231
- </div>
232
- <div>
233
- <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;
234
238
  ${this.user?.email}
235
- </div>
236
- <div>
239
+ &lt;/div&gt;
240
+ &lt;div&gt;
237
241
  Theme: ${this.userSettings?.theme} | Language:
238
242
  ${this.userSettings?.language}
239
- </div>
240
- </div>
241
- </div>
243
+ &lt;/div&gt;
244
+ &lt;/div&gt;
245
+ &lt;/div&gt;
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&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
+
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("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>