@supersoniks/concorde 4.6.0 → 4.7.3

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 (182) 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 +273 -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 +152 -152
  18. package/concorde-core.es.js +1853 -1689
  19. package/dist/altcha-widget.js +2662 -0
  20. package/dist/concorde-core.bundle.js +152 -152
  21. package/dist/concorde-core.es.js +1853 -1689
  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 +34 -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/api.spec.ts +8 -1
  108. package/src/core/decorators/api.ts +126 -15
  109. package/src/core/directives/DataProvider.sub.spec.ts +96 -0
  110. package/src/core/directives/DataProvider.ts +109 -40
  111. package/src/core/utils/HTML.ts +42 -10
  112. package/src/core/utils/PublisherProxy.ts +33 -18
  113. package/src/core/utils/dataProviderKey.ts +23 -0
  114. package/src/core/utils/publisherPathKey.spec.ts +58 -0
  115. package/src/docs/_core-concept/dataFlow.md +73 -0
  116. package/src/docs/_core-concept/subscriber.md +9 -10
  117. package/src/docs/_decorators/ancestor-attribute.md +4 -3
  118. package/src/docs/_decorators/auto-subscribe.md +19 -16
  119. package/src/docs/_decorators/bind.md +19 -16
  120. package/src/docs/_decorators/get.md +7 -4
  121. package/src/docs/_decorators/handle.md +15 -13
  122. package/src/docs/_decorators/on-assign.md +53 -53
  123. package/src/docs/_decorators/publish.md +2 -1
  124. package/src/docs/_decorators/subscribe.md +70 -9
  125. package/src/docs/_decorators/wait-for-ancestors.md +13 -10
  126. package/src/docs/_directives/sub.md +91 -0
  127. package/src/docs/_getting-started/ai-agents.md +56 -0
  128. package/src/docs/_getting-started/concorde-manual-install.md +133 -0
  129. package/src/docs/_getting-started/concorde-outside.md +13 -123
  130. package/src/docs/_getting-started/create-a-component.md +2 -0
  131. package/src/docs/_getting-started/my-first-component.md +236 -0
  132. package/src/docs/_getting-started/my-first-subscriber.md +29 -83
  133. package/src/docs/_getting-started/pubsub.md +21 -134
  134. package/src/docs/_getting-started/start.md +26 -18
  135. package/src/docs/_misc/api-configuration.md +79 -0
  136. package/src/docs/_misc/dataProviderKey.md +34 -1
  137. package/src/docs/_misc/docs-mock-api.md +60 -0
  138. package/src/docs/_misc/endpoint.md +2 -1
  139. package/src/docs/_misc/html-integration.md +13 -0
  140. package/src/docs/code.ts +58 -12
  141. package/src/docs/components/docs-demo-sources.ts +397 -0
  142. package/src/docs/components/docs-lit-demo-raw.ts +28 -0
  143. package/src/docs/components/docs-lit-demo.ts +166 -0
  144. package/src/docs/components/docs-source-link.ts +72 -0
  145. package/src/docs/docs-location.ts +54 -0
  146. package/src/docs/docs.ts +12 -0
  147. package/src/docs/example/decorators-demo-bind-demos.ts +41 -46
  148. package/src/docs/example/decorators-demo-geo.ts +16 -11
  149. package/src/docs/example/decorators-demo-init.ts +2 -228
  150. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +54 -14
  151. package/src/docs/example/decorators-demo.ts +71 -70
  152. package/src/docs/example/docs-api-config-demos.ts +234 -0
  153. package/src/docs/example/docs-joke-demos.ts +297 -0
  154. package/src/docs/example/docs-list-demos.ts +179 -0
  155. package/src/docs/example/docs-provider-keys.ts +315 -0
  156. package/src/docs/example/docs-queue-demos.ts +114 -0
  157. package/src/docs/example/docs-router-demos.ts +89 -0
  158. package/src/docs/example/docs-submit-demos.ts +455 -0
  159. package/src/docs/example/docs-toggle-demos.ts +73 -0
  160. package/src/docs/example/docs-user-two-scopes.ts +37 -0
  161. package/src/docs/example/docs-users-list.ts +71 -0
  162. package/src/docs/example/users.ts +41 -24
  163. package/src/docs/mock-api/api-config-mock.ts +152 -0
  164. package/src/docs/mock-api/fixtures.ts +377 -0
  165. package/src/docs/mock-api/register.ts +25 -0
  166. package/src/docs/mock-api/router.ts +234 -0
  167. package/src/docs/mock-api/service-worker.ts +23 -0
  168. package/src/docs/mock-api/urls.ts +11 -0
  169. package/src/docs/navigation/navigation.ts +39 -7
  170. package/src/docs/search/docs-search.json +4021 -936
  171. package/src/docs/search/markdown-renderer.ts +7 -3
  172. package/src/docs/search/page.ts +11 -14
  173. package/src/docs/search/sonic-code-markdown.spec.ts +29 -0
  174. package/src/docs/search/sonic-code-markdown.ts +28 -0
  175. package/src/docs.ts +4 -0
  176. package/src/tsconfig.json +87 -0
  177. package/src/tsconfig.tsbuildinfo +1 -1
  178. package/vite.config.mts +8 -0
  179. package/docs/assets/index-CaysOMFz.js +0 -5046
  180. package/docs/assets/index-D8mGoXzF.css +0 -1
  181. package/docs/src/docs/_misc/templates-demo.md +0 -19
  182. package/src/docs/_misc/templates-demo.md +0 -19
@@ -64,34 +64,8 @@
64
64
  </template>
65
65
  </sonic-code>
66
66
 
67
- ## Example of use
68
- <sonic-code>
69
- <template>
70
- <sonic-select
71
- formDataProvider="select-filter"
72
- name="lang"
73
- value="fr"
74
- >
75
- <option value="fr">fr<option>
76
- <optionn value="en">en<option>
77
- </sonic-select>
78
- <sonic-subscriber dataProvider="select-filter" class="text-xl my-4 block font-bold">
79
- Blagues trouvées pour le code de langue"<span data-bind ::inner-html="$lang"></span>" :
80
- </sonic-subscriber>
81
- <sonic-queue
82
- lazyload
83
- dataProviderExpression="joke/Any?amount=10"
84
- dataFilterProvider="select-filter"
85
- serviceURL="https://v2.jokeapi.dev"
86
- key="jokes"
87
- >
88
- <template>
89
- <div class="border-0 border-b-[1px] border-b-neutral-300 border-dotted py-3">
90
- <div data-bind ::inner-html="$joke"></div>
91
- <div data-bind ::inner-html="$setup"></div>
92
- <div data-bind ::inner-html="$delivery"></div>
93
- </div>
94
- </template>
95
- </sonic-queue>
96
- </template>
97
- </sonic-code>
67
+ ## Example of use — language filter + queue
68
+
69
+ **`name="lang"`** is sent as `?lang=fr|en` to the [doc mock API](#docs/_misc/docs-mock-api.md/docs-mock-api) (`filterDocsJokes`). Same pattern as [Input](#core/components/ui/form/input/input.md/input) (`contains`):
70
+
71
+ <docs-lit-demo for="docs-joke-lang-demo"></docs-lit-demo>
@@ -50,35 +50,8 @@
50
50
  </sonic-code>
51
51
 
52
52
 
53
- ## Example of use
54
- <sonic-code>
55
- <template>
56
- <sonic-subscriber dataProvider="jokeFilterswitch" class="text-xl my-4 block font-bold">
57
- Remove following jokes :
58
- <sonic-value key="blacklistFlags" class="block text-sm"></sonic-value>
59
- </sonic-subscriber>
60
- <div formDataProvider="jokeFilterswitch" class="grid grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2 mt-2 mb-3">
61
- <sonic-switch name="blacklistFlags" value="nsfw">nsfw</sonic-switch>
62
- <sonic-switch name="blacklistFlags" value="religious">religious</sonic-switch>
63
- <sonic-switch name="blacklistFlags" value="political">political</sonic-switch>
64
- <sonic-switch name="blacklistFlags" value="racist" checked >racist</sonic-switch>
65
- <sonic-switch name="blacklistFlags" value="sexist" >sexist</sonic-switch>
66
- <sonic-switch name="blacklistFlags" value="explicit">explicit</sonic-switch>
67
- </div>
68
- <sonic-queue
69
- lazyload
70
- dataProviderExpression="joke/Any?amount=10&lang=en"
71
- dataFilterProvider="jokeFilterswitch"
72
- serviceURL="https://v2.jokeapi.dev"
73
- key="jokes"
74
- >
75
- <template>
76
- <div class="border-0 border-b-[1px] border-b-neutral-300 py-3 leading-tight">
77
- <sonic-value key="joke"></sonic-value>
78
- <sonic-value key="setup" class="font-bold"></sonic-value><br>
79
- <sonic-value key="delivery"></sonic-value>
80
- </div>
81
- </template>
82
- </sonic-queue>
83
- </template>
84
- </sonic-code>
53
+ ## Example of use — blacklist + queue
54
+
55
+ Same mock filter as [Checkbox](#core/components/ui/form/checkbox/checkbox.md/checkbox); switches can enable several flags (comma-separated in the query).
56
+
57
+ <docs-lit-demo for="docs-joke-blacklist-switch-demo"></docs-lit-demo>
@@ -29,19 +29,7 @@
29
29
 
30
30
  ## Fixed mode
31
31
 
32
- <sonic-code >
33
- <template>
34
- <div dataProvider="toggleLoaderForm" formDataProvider="toggleLoaderForm">
35
- <sonic-checkbox label="Show fixed loader"
36
- name="toggleLoader"
37
- unique value="true">
38
- </sonic-checkbox>
39
- <sonic-if data-bind ::condition="$toggleLoader" >
40
- <sonic-loader></sonic-loader>
41
- </sonic-if>
42
- </div>
43
- </template>
44
- </sonic-code>
32
+ <docs-lit-demo for="docs-toggle-loader-demo"></docs-lit-demo>
45
33
 
46
34
 
47
35
  ## Loading button
@@ -42,7 +42,7 @@
42
42
  </sonic-tr>
43
43
  </sonic-thead>
44
44
  <sonic-tbody>
45
- <sonic-list debug fetch serviceURL="https://reqres.in" dataProvider="api/users" key="data" displayContents>
45
+ <sonic-list debug fetch serviceURL="/docs-mock-api" dataProvider="api/users" key="data" displayContents>
46
46
  <template>
47
47
  <sonic-tr>
48
48
  <sonic-td data-bind ::inner-html="$id"></sonic-td>
@@ -135,7 +135,7 @@
135
135
  </sonic-tr>
136
136
  </sonic-thead>
137
137
  <sonic-tbody>
138
- <sonic-list fetch serviceURL="https://reqres.in" dataProvider="api/users" key="data" displayContents>
138
+ <sonic-list fetch serviceURL="/docs-mock-api" dataProvider="api/users" key="data" displayContents>
139
139
  <template>
140
140
  <sonic-tr>
141
141
  <sonic-td data-bind ::inner-html="$id"></sonic-td>
@@ -256,7 +256,7 @@ Every table is responsive by default
256
256
  </sonic-tr>
257
257
  </sonic-thead>
258
258
  <sonic-tbody>
259
- <sonic-list fetch serviceURL="https://reqres.in" dataProvider="api/users" key="data" displayContents>
259
+ <sonic-list fetch serviceURL="/docs-mock-api" dataProvider="api/users" key="data" displayContents>
260
260
  <template>
261
261
  <sonic-tr>
262
262
  <sonic-td data-bind ::inner-html="$id"></sonic-td>
@@ -22,6 +22,13 @@ async function flushGetMicrotasks() {
22
22
  await Promise.resolve();
23
23
  }
24
24
 
25
+ /** @get scoped : scheduleAfterHostReady diffère le fetch (rAF ou updateComplete Lit). */
26
+ async function flushScopedGet() {
27
+ await flushGetMicrotasks();
28
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
29
+ await flushGetMicrotasks();
30
+ }
31
+
25
32
  function mockPayload<T>(result: T) {
26
33
  return {
27
34
  request: new Request("https://api.example.test/items/1"),
@@ -113,7 +120,7 @@ describe("get", () => {
113
120
  wrap.appendChild(el);
114
121
  document.body.appendChild(wrap);
115
122
 
116
- await flushGetMicrotasks();
123
+ await flushScopedGet();
117
124
 
118
125
  expect(API.prototype.getDetailed).toHaveBeenCalledWith("scoped-endpoint");
119
126
  expect(el.payload?.result).toEqual({ id: "loaded" });
@@ -47,11 +47,108 @@ function resolveScopedConfiguration(
47
47
  return HTML.getApiConfiguration(host);
48
48
  }
49
49
 
50
+ function isScopedConfigurationReady(
51
+ config: APIConfiguration | null,
52
+ ): config is APIConfiguration {
53
+ return typeof config?.serviceURL === "string" && config.serviceURL.length > 0;
54
+ }
55
+
56
+ /** Attributs scope dont la présence peut déclencher un GET différé (Lit → minuscules). */
57
+ const SCOPE_WATCH_ATTRIBUTES = [
58
+ "serviceURL",
59
+ "serviceurl",
60
+ "token",
61
+ "credentials",
62
+ "tokenProvider",
63
+ "tokenprovider",
64
+ "userName",
65
+ "username",
66
+ "password",
67
+ "eventsApiToken",
68
+ "eventsapitoken",
69
+ ] as const;
70
+
71
+ type LitHost = HTMLElement & { updateComplete?: Promise<unknown> };
72
+
73
+ function isLitHost(component: unknown): component is LitHost {
74
+ return (
75
+ component instanceof HTMLElement &&
76
+ typeof (component as LitHost).updateComplete !== "undefined"
77
+ );
78
+ }
79
+
80
+ function scheduleAfterHostReady(
81
+ component: unknown,
82
+ callback: () => void,
83
+ ): () => void {
84
+ if (isLitHost(component)) {
85
+ let cancelled = false;
86
+ void component.updateComplete!.then(() => {
87
+ if (!cancelled) callback();
88
+ });
89
+ return () => {
90
+ cancelled = true;
91
+ };
92
+ }
93
+ const rafId = requestAnimationFrame(() => callback());
94
+ return () => cancelAnimationFrame(rafId);
95
+ }
96
+
97
+ /**
98
+ * Attend que la config scope soit lisible (attributs DOM ou propriétés Lit reflect).
99
+ */
100
+ function watchScopedConfiguration(
101
+ component: unknown,
102
+ onReady: () => void,
103
+ ): () => void {
104
+ const host = asSearchableHost(component);
105
+ if (!host) return () => {};
106
+
107
+ let settled = false;
108
+ const tryNotify = () => {
109
+ if (settled) return;
110
+ if (!isScopedConfigurationReady(resolveScopedConfiguration(component))) {
111
+ return;
112
+ }
113
+ settled = true;
114
+ onReady();
115
+ };
116
+
117
+ tryNotify();
118
+ if (settled) return () => {};
119
+
120
+ const cleanups: Array<() => void> = [];
121
+
122
+ const rafId = requestAnimationFrame(() => tryNotify());
123
+ cleanups.push(() => cancelAnimationFrame(rafId));
124
+
125
+ queueMicrotask(() => tryNotify());
126
+
127
+ const observer = new MutationObserver(() => tryNotify());
128
+ let node: SearchableDomElement | null = host;
129
+ while (node) {
130
+ if (node instanceof Element) {
131
+ observer.observe(node, {
132
+ attributes: true,
133
+ attributeFilter: [...SCOPE_WATCH_ATTRIBUTES],
134
+ });
135
+ }
136
+ node = (node.parentNode || (node as ShadowRoot).host) as SearchableDomElement;
137
+ }
138
+ cleanups.push(() => observer.disconnect());
139
+
140
+ return () => {
141
+ settled = true;
142
+ cleanups.forEach((cleanup) => cleanup());
143
+ };
144
+ }
145
+
50
146
  type ApiGetState = {
51
147
  cleanupWatchers: Array<() => void>;
52
148
  requestGeneration: number;
53
149
  configPublisher: DataProvider | null;
54
150
  configMutationHandler: (() => void) | null;
151
+ scopeWatchCleanup: (() => void) | null;
55
152
  };
56
153
 
57
154
  function detachConfigPublisher(state: ApiGetState): void {
@@ -68,7 +165,8 @@ function detachConfigPublisher(state: ApiGetState): void {
68
165
  * Le path est un `Endpoint<T, Ue>` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte).
69
166
  *
70
167
  * **Scoped (défaut)** : `HTML.getApiConfiguration(host)` avec `host` = l’élément connecté
71
- * (`HTMLElement` / `ShadowRoot`). Sans hôte DOM valide, la propriété reste inchangée jusqu’à connexion.
168
+ * (`HTMLElement` / `ShadowRoot`). Si `serviceURL` n’est pas encore disponible (reflect Lit, scope
169
+ * ancêtre), le GET est différé jusqu’à ce que la config soit prête.
72
170
  *
73
171
  * **Deuxième paramètre** : `DataProviderKey<APIConfiguration>` — la config est lue via
74
172
  * `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
@@ -138,6 +236,7 @@ export function get<T, Ue = any, Uk = any>(
138
236
  requestGeneration: 0,
139
237
  configPublisher: null,
140
238
  configMutationHandler: null,
239
+ scopeWatchCleanup: null,
141
240
  };
142
241
  comp[stateKey] = state;
143
242
  }
@@ -166,8 +265,15 @@ export function get<T, Ue = any, Uk = any>(
166
265
  } else {
167
266
  config = resolveScopedConfiguration(component);
168
267
  }
169
- if (!config) {
170
- comp[propertyKey] = undefined;
268
+ if (!isScopedConfigurationReady(config)) {
269
+ if (!usesPublisherConfig && !state.scopeWatchCleanup) {
270
+ const scopeWatch = watchScopedConfiguration(component, runFetch);
271
+ state.scopeWatchCleanup = scopeWatch;
272
+ state.cleanupWatchers.push(() => {
273
+ scopeWatch();
274
+ state.scopeWatchCleanup = null;
275
+ });
276
+ }
171
277
  return;
172
278
  }
173
279
  const generation = ++state.requestGeneration;
@@ -214,19 +320,24 @@ export function get<T, Ue = any, Uk = any>(
214
320
  }
215
321
  rebindPublisherConfig();
216
322
  } else {
217
- if (isDynamicPath) {
218
- for (const dependency of endpointDynamicDependencies) {
219
- const unsubscribe = observeDynamicProperty(
220
- getDynamicWatchKeys.watcherStore,
221
- getDynamicWatchKeys.hooked,
222
- component,
223
- dependency,
224
- () => runFetch(),
225
- );
226
- state.cleanupWatchers.push(unsubscribe);
323
+ const startScopedFetch = () => {
324
+ if (isDynamicPath) {
325
+ for (const dependency of endpointDynamicDependencies) {
326
+ const unsubscribe = observeDynamicProperty(
327
+ getDynamicWatchKeys.watcherStore,
328
+ getDynamicWatchKeys.hooked,
329
+ component,
330
+ dependency,
331
+ () => runFetch(),
332
+ );
333
+ state.cleanupWatchers.push(unsubscribe);
334
+ }
227
335
  }
228
- }
229
- runFetch();
336
+ runFetch();
337
+ };
338
+ state.cleanupWatchers.push(
339
+ scheduleAfterHostReady(component, startScopedFetch),
340
+ );
230
341
  }
231
342
  });
232
343
 
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { html, LitElement } from "lit";
3
+ import { customElement, property } from "lit/decorators.js";
4
+ import { DataProviderKey } from "../utils/dataProviderKey";
5
+ import { dp, set } from "../utils/PublisherProxy";
6
+ import { sub } from "./DataProvider";
7
+
8
+ type User = { name: string };
9
+
10
+ const dynamicUserKey = new DataProviderKey<User, { userIndex: number }>(
11
+ "subSpec.users.${userIndex}",
12
+ );
13
+
14
+ const staticCountKey = new DataProviderKey<{ count: number }>(
15
+ "subSpec.counter",
16
+ ).count;
17
+
18
+ @customElement("sub-spec-host")
19
+ class SubSpecHost extends LitElement {
20
+ @property({ type: Number }) userIndex = 0;
21
+
22
+ render() {
23
+ return html`
24
+ <span id="dynamic">${sub(dynamicUserKey.name)}</span>
25
+ <span id="static">${sub(staticCountKey)}</span>
26
+ <span id="string">${sub("subSpec.label")}</span>
27
+ `;
28
+ }
29
+ }
30
+
31
+ describe("sub() with DataProviderKey", () => {
32
+ let host: SubSpecHost;
33
+
34
+ beforeEach(async () => {
35
+ set("subSpec.users.0", { name: "Alice" });
36
+ set("subSpec.users.1", { name: "Bob" });
37
+ set("subSpec.counter", { count: 0 });
38
+ dpSetCount(7);
39
+ set("subSpec.label", "hello");
40
+
41
+ host = document.createElement("sub-spec-host") as SubSpecHost;
42
+ document.body.appendChild(host);
43
+ await host.updateComplete;
44
+ await flushMicrotasks();
45
+ });
46
+
47
+ afterEach(() => {
48
+ document.body.replaceChildren();
49
+ });
50
+
51
+ it("suit une clé dynamique quand userIndex change", async () => {
52
+ expect(text(host, "#dynamic")).toBe("Alice");
53
+
54
+ host.userIndex = 1;
55
+ await host.updateComplete;
56
+ await flushFrames();
57
+
58
+ expect(text(host, "#dynamic")).toBe("Bob");
59
+ });
60
+
61
+ it("suit une sous-clé statique", async () => {
62
+ expect(text(host, "#static")).toBe("7");
63
+
64
+ dpSetCount(42);
65
+ await host.updateComplete;
66
+
67
+ expect(text(host, "#static")).toBe("42");
68
+ });
69
+
70
+ it("accepte encore un chemin string", async () => {
71
+ expect(text(host, "#string")).toBe("hello");
72
+ set("subSpec.label", "world");
73
+ await host.updateComplete;
74
+ expect(text(host, "#string")).toBe("world");
75
+ });
76
+ });
77
+
78
+ function dpSetCount(count: number) {
79
+ dp(staticCountKey).set(count);
80
+ }
81
+
82
+ function text(el: SubSpecHost, selector: string): string {
83
+ return el.shadowRoot?.querySelector(selector)?.textContent ?? "";
84
+ }
85
+
86
+ async function flushMicrotasks() {
87
+ await Promise.resolve();
88
+ await Promise.resolve();
89
+ await Promise.resolve();
90
+ }
91
+
92
+ async function flushFrames() {
93
+ await flushMicrotasks();
94
+ await new Promise((r) => requestAnimationFrame(r));
95
+ await new Promise((r) => requestAnimationFrame(r));
96
+ }
@@ -2,21 +2,32 @@ import { AsyncDirective, PartInfo } from "lit/async-directive.js";
2
2
  import { directive } from "lit/directive.js";
3
3
  import { noChange } from "lit";
4
4
  import { SearchableDomElement } from "../utils/HTML";
5
- import {
5
+ import {
6
+ extractDynamicDependencies,
7
+ hasPath,
8
+ resolveDynamicPath,
9
+ } from "../decorators/subscriber/dynamicPath";
10
+ import {
11
+ bindDynamicWatchKeys,
12
+ observeDynamicProperty,
13
+ } from "../decorators/subscriber/dynamicPropertyWatch";
14
+ import {
6
15
  getObservables,
7
- get as pubGet, set as pubSet, dataProvider as pubDataProvider ,
8
- dp as pubDp, DataProvider
16
+ get as pubGet,
17
+ set as pubSet,
18
+ dataProvider as pubDataProvider,
19
+ dp as pubDp,
20
+ DataProvider,
9
21
  } from "../utils/PublisherProxy";
10
22
 
23
+ export type SubPathInput = string | {path: string};
11
24
 
12
25
  class ObserveDirective extends AsyncDirective {
13
26
  observables: Set<DataProvider<any>> = new Set();
14
- unsubscribe(): void {
15
- this.observables.forEach((publisher: DataProvider<any>) =>
16
- publisher.offAssign(this.onAssign)
17
- );
18
- }
19
- observable?: string;
27
+ pathTemplate?: string;
28
+ /** Chemin résolu actuellement abonné */
29
+ resolvedPath?: string;
30
+ cleanupWatchers: Array<() => void> = [];
20
31
  node?: SearchableDomElement;
21
32
 
22
33
  /* eslint-disable @typescript-eslint/no-explicit-any*/
@@ -26,21 +37,87 @@ class ObserveDirective extends AsyncDirective {
26
37
  }
27
38
  /* eslint-enable @typescript-eslint/no-explicit-any*/
28
39
 
29
- render(observable: string) {
30
- if (this.observable !== observable) {
31
- this.observable = observable;
32
- if (this.isConnected) {
33
- this.subscribe(observable);
34
- }
40
+ private teardownWatchers() {
41
+ this.cleanupWatchers.forEach((cleanup) => cleanup());
42
+ this.cleanupWatchers = [];
43
+ }
44
+
45
+ private normalizeInput(input: SubPathInput): string {
46
+ return hasPath(input) ? input.path : input;
47
+ }
48
+
49
+ render(input: SubPathInput) {
50
+ const path = this.normalizeInput(input);
51
+ const pathChanged = this.pathTemplate !== path;
52
+ if (pathChanged) {
53
+ this.pathTemplate = path;
54
+ this.teardownWatchers();
55
+ }
56
+ if (pathChanged || !this.resolvedPath) {
57
+ queueMicrotask(() => {
58
+ if (!this.isConnected || !this.pathTemplate) return;
59
+ this.setupSubscription();
60
+ });
35
61
  }
36
62
  return noChange;
37
63
  }
38
64
 
65
+ private setupSubscription() {
66
+ const host = this.node;
67
+ const path = this.pathTemplate;
68
+ if (!host || !path) return;
69
+
70
+ this.teardownWatchers();
71
+ const dependencies = extractDynamicDependencies(path);
72
+ if (dependencies.length > 0) {
73
+ for (const dependency of dependencies) {
74
+ this.cleanupWatchers.push(
75
+ observeDynamicProperty(
76
+ bindDynamicWatchKeys.watcherStore,
77
+ bindDynamicWatchKeys.hooked,
78
+ host,
79
+ dependency,
80
+ () => this.refreshSubscription(),
81
+ ),
82
+ );
83
+ }
84
+ }
85
+ this.refreshSubscription();
86
+ }
87
+
88
+ private refreshSubscription() {
89
+ const host = this.node;
90
+ const path = this.pathTemplate;
91
+ if (!path) return;
92
+
93
+ const isDynamic = extractDynamicDependencies(path).length > 0;
94
+ let resolved: string | null = path;
95
+ if (isDynamic) {
96
+ if (!host) {
97
+ resolved = null;
98
+ } else {
99
+ const resolution = resolveDynamicPath(host, path);
100
+ resolved = resolution.ready ? resolution.path : null;
101
+ }
102
+ }
103
+
104
+ if (resolved === this.resolvedPath) return;
105
+
106
+ if (!resolved) {
107
+ this.unsubscribe();
108
+ this.resolvedPath = undefined;
109
+ this.setValue(undefined);
110
+ return;
111
+ }
112
+
113
+ this.resolvedPath = resolved;
114
+ this.subscribe(resolved);
115
+ }
116
+
39
117
  onAssign = (v: unknown) => {
40
118
  this.setValue(v);
41
119
  };
42
- // Subscribes to the observable, calling the directive's asynchronous
43
- // setValue API each time the value changes
120
+
44
121
  subscribe<T>(observable: string) {
45
122
  this.unsubscribe();
46
123
  this.onAssign = (v: unknown) => {
@@ -52,55 +129,47 @@ class ObserveDirective extends AsyncDirective {
52
129
  publisher.onAssign(this.onAssign);
53
130
  });
54
131
  }
55
- // When the directive is disconnected from the DOM, unsubscribe to ensure
56
- // the directive instance can be garbage collected
132
+
133
+ unsubscribe(): void {
134
+ this.observables.forEach((publisher: DataProvider<any>) =>
135
+ publisher.offAssign(this.onAssign),
136
+ );
137
+ this.observables.clear();
138
+ }
139
+
57
140
  disconnected() {
141
+ this.teardownWatchers();
58
142
  this.unsubscribe();
143
+ this.resolvedPath = undefined;
59
144
  }
60
- // If the subtree the directive is in was disconnected and subsequently
61
- // re-connected, re-subscribe to make the directive operable again
145
+
62
146
  reconnected() {
63
- if (!this.observable) return;
64
- this.subscribe(this.observable);
147
+ if (!this.pathTemplate) return;
148
+ this.setupSubscription();
65
149
  }
66
150
  }
151
+
67
152
  const dir = directive(ObserveDirective);
68
- //
69
- //autoUpdate directive
153
+
70
154
  export const subscribe = dir;
71
155
  export const sub = dir;
72
156
 
73
-
74
157
  /**
75
158
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#get"}
76
- * @param id Observable
77
- * @returns value of the observable
78
- *
79
159
  */
80
160
  export const get: <T = any>(id: string) => T = pubGet;
81
161
 
82
-
83
-
84
162
  /**
85
163
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#dataProvider"}
86
- * @param id Observable
87
- * @param defaultValue Optional default value
88
- * @returns Observable
89
164
  */
90
165
  export const dataProvider = pubDataProvider;
91
166
 
92
167
  /**
93
168
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#dp"}
94
- * @param id Observable
95
- * @param defaultValue Optional default value
96
- * @returns Observable
97
169
  */
98
170
  export const dp = pubDp;
99
171
 
100
172
  /**
101
173
  * @deprecated @see {@link "/src/core/utils/PublisherProxy.ts#set"}
102
- * @param id Observable
103
- * @param value value to set
104
- * @returns void
105
174
  */
106
175
  export const set: <T = any>(id: string, value: T) => void = pubSet;