@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.
- package/.gitlab-ci.yml +23 -0
- package/README.md +106 -55
- package/ai/AGENTS.md +52 -0
- package/ai/README.md +30 -0
- package/ai/cursor/rules/concorde-menu.mdc +15 -0
- package/ai/cursor/rules/concorde-scope.mdc +14 -0
- package/ai/cursor/rules/concorde-theme.mdc +13 -0
- package/ai/cursor/rules/concorde.mdc +49 -0
- package/ai/jetbrains/rules/concorde.md +39 -0
- package/ai/skills/concorde/SKILL.md +273 -0
- package/ai/skills/concorde-get-set-dp/SKILL.md +194 -0
- package/ai/skills/concorde-imports/SKILL.md +78 -0
- package/ai/skills/concorde-menu/SKILL.md +74 -0
- package/ai/skills/concorde-scope/SKILL.md +70 -0
- package/ai/skills/concorde-theme/SKILL.md +46 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +152 -152
- package/concorde-core.es.js +1853 -1689
- package/dist/altcha-widget.js +2662 -0
- package/dist/concorde-core.bundle.js +152 -152
- package/dist/concorde-core.es.js +1853 -1689
- package/dist/docs-mock-api-sw.js +589 -0
- package/dist/docs-mock-api-sw.js.map +7 -0
- package/docs/altcha-widget.js +2662 -0
- package/docs/assets/index-D9pxaQYK.js +7508 -0
- package/docs/assets/index-t0-i22oI.css +1 -0
- package/docs/docs-mock-api-sw.js +589 -0
- package/docs/docs-mock-api-sw.js.map +7 -0
- package/docs/index.html +2 -2
- package/docs/src/core/components/functional/fetch/fetch.md +13 -11
- package/docs/src/core/components/functional/if/if.md +4 -11
- package/docs/src/core/components/functional/list/list.md +60 -194
- package/docs/src/core/components/functional/queue/queue.md +70 -85
- package/docs/src/core/components/functional/router/router.md +62 -97
- package/docs/src/core/components/functional/states/states.md +2 -2
- package/docs/src/core/components/functional/submit/submit.md +86 -55
- package/docs/src/core/components/ui/captcha/captcha.md +2 -2
- package/docs/src/core/components/ui/card/card.md +1 -1
- package/docs/src/core/components/ui/form/checkbox/checkbox.md +5 -32
- package/docs/src/core/components/ui/form/input/input.md +5 -30
- package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
- package/docs/src/core/components/ui/form/radio/radio.md +5 -32
- package/docs/src/core/components/ui/form/select/select.md +5 -31
- package/docs/src/core/components/ui/form/switch/switch.md +5 -32
- package/docs/src/core/components/ui/loader/loader.md +1 -13
- package/docs/src/core/components/ui/table/table.md +3 -3
- package/docs/src/docs/_core-concept/dataFlow.md +73 -0
- package/docs/src/docs/_core-concept/subscriber.md +9 -10
- package/docs/src/docs/_decorators/ancestor-attribute.md +4 -3
- package/docs/src/docs/_decorators/auto-subscribe.md +19 -16
- package/docs/src/docs/_decorators/bind.md +20 -17
- package/docs/src/docs/_decorators/get.md +7 -4
- package/docs/src/docs/_decorators/handle.md +171 -0
- package/docs/src/docs/_decorators/on-assign.md +99 -73
- package/docs/src/docs/_decorators/publish.md +2 -1
- package/docs/src/docs/_decorators/subscribe.md +70 -9
- package/docs/src/docs/_decorators/wait-for-ancestors.md +13 -10
- package/docs/src/docs/_directives/sub.md +91 -0
- package/docs/src/docs/_getting-started/ai-agents.md +56 -0
- package/docs/src/docs/_getting-started/concorde-manual-install.md +133 -0
- package/docs/src/docs/_getting-started/concorde-outside.md +13 -123
- package/docs/src/docs/_getting-started/create-a-component.md +2 -0
- package/docs/src/docs/_getting-started/my-first-component.md +236 -0
- package/docs/src/docs/_getting-started/my-first-subscriber.md +29 -83
- package/docs/src/docs/_getting-started/pubsub.md +21 -134
- package/docs/src/docs/_getting-started/start.md +26 -18
- package/docs/src/docs/_misc/api-configuration.md +79 -0
- package/docs/src/docs/_misc/dataProviderKey.md +38 -5
- package/docs/src/docs/_misc/docs-mock-api.md +60 -0
- package/docs/src/docs/_misc/endpoint.md +2 -1
- package/docs/src/docs/_misc/html-integration.md +13 -0
- package/docs/src/docs/search/docs-search.json +4163 -873
- package/docs/src/tsconfig.json +380 -317
- package/gitlab/job_tests.sh +55 -0
- package/package.json +34 -3
- package/public/altcha-widget.js +2662 -0
- package/public/docs-mock-api-sw.js +589 -0
- package/public/docs-mock-api-sw.js.map +7 -0
- package/scripts/ai-init.mjs +167 -0
- package/scripts/docs-mock-api-vite-plugin.ts +116 -0
- package/scripts/docs-open-in-editor-plugin.ts +130 -0
- package/scripts/pre-publish.mjs +2 -1
- package/src/core/components/functional/example/example.ts +1 -1
- package/src/core/components/functional/fetch/fetch.md +13 -11
- package/src/core/components/functional/if/if.md +4 -11
- package/src/core/components/functional/list/list.demo.ts +4 -4
- package/src/core/components/functional/list/list.md +60 -194
- package/src/core/components/functional/list/list.ts +8 -7
- package/src/core/components/functional/queue/queue.demo.ts +1 -1
- package/src/core/components/functional/queue/queue.md +70 -85
- package/src/core/components/functional/queue/queue.ts +4 -4
- package/src/core/components/functional/router/router.md +62 -97
- package/src/core/components/functional/router/router.ts +1 -1
- package/src/core/components/functional/states/states.md +2 -2
- package/src/core/components/functional/submit/submit.md +86 -55
- package/src/core/components/functional/submit/submit.ts +10 -3
- package/src/core/components/ui/captcha/captcha.md +2 -2
- package/src/core/components/ui/card/card.md +1 -1
- package/src/core/components/ui/form/checkbox/checkbox.md +5 -32
- package/src/core/components/ui/form/input/input.md +5 -30
- package/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
- package/src/core/components/ui/form/radio/radio.md +5 -32
- package/src/core/components/ui/form/select/select.md +5 -31
- package/src/core/components/ui/form/switch/switch.md +5 -32
- package/src/core/components/ui/loader/loader.md +1 -13
- package/src/core/components/ui/table/table.md +3 -3
- package/src/core/decorators/api.spec.ts +8 -1
- package/src/core/decorators/api.ts +126 -15
- package/src/core/directives/DataProvider.sub.spec.ts +96 -0
- package/src/core/directives/DataProvider.ts +109 -40
- package/src/core/utils/HTML.ts +42 -10
- package/src/core/utils/PublisherProxy.ts +33 -18
- package/src/core/utils/dataProviderKey.ts +23 -0
- package/src/core/utils/publisherPathKey.spec.ts +58 -0
- package/src/docs/_core-concept/dataFlow.md +73 -0
- package/src/docs/_core-concept/subscriber.md +9 -10
- package/src/docs/_decorators/ancestor-attribute.md +4 -3
- package/src/docs/_decorators/auto-subscribe.md +19 -16
- package/src/docs/_decorators/bind.md +19 -16
- package/src/docs/_decorators/get.md +7 -4
- package/src/docs/_decorators/handle.md +15 -13
- package/src/docs/_decorators/on-assign.md +53 -53
- package/src/docs/_decorators/publish.md +2 -1
- package/src/docs/_decorators/subscribe.md +70 -9
- package/src/docs/_decorators/wait-for-ancestors.md +13 -10
- package/src/docs/_directives/sub.md +91 -0
- package/src/docs/_getting-started/ai-agents.md +56 -0
- package/src/docs/_getting-started/concorde-manual-install.md +133 -0
- package/src/docs/_getting-started/concorde-outside.md +13 -123
- package/src/docs/_getting-started/create-a-component.md +2 -0
- package/src/docs/_getting-started/my-first-component.md +236 -0
- package/src/docs/_getting-started/my-first-subscriber.md +29 -83
- package/src/docs/_getting-started/pubsub.md +21 -134
- package/src/docs/_getting-started/start.md +26 -18
- package/src/docs/_misc/api-configuration.md +79 -0
- package/src/docs/_misc/dataProviderKey.md +34 -1
- package/src/docs/_misc/docs-mock-api.md +60 -0
- package/src/docs/_misc/endpoint.md +2 -1
- package/src/docs/_misc/html-integration.md +13 -0
- package/src/docs/code.ts +58 -12
- package/src/docs/components/docs-demo-sources.ts +397 -0
- package/src/docs/components/docs-lit-demo-raw.ts +28 -0
- package/src/docs/components/docs-lit-demo.ts +166 -0
- package/src/docs/components/docs-source-link.ts +72 -0
- package/src/docs/docs-location.ts +54 -0
- package/src/docs/docs.ts +12 -0
- package/src/docs/example/decorators-demo-bind-demos.ts +41 -46
- package/src/docs/example/decorators-demo-geo.ts +16 -11
- package/src/docs/example/decorators-demo-init.ts +2 -228
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +54 -14
- package/src/docs/example/decorators-demo.ts +71 -70
- package/src/docs/example/docs-api-config-demos.ts +234 -0
- package/src/docs/example/docs-joke-demos.ts +297 -0
- package/src/docs/example/docs-list-demos.ts +179 -0
- package/src/docs/example/docs-provider-keys.ts +315 -0
- package/src/docs/example/docs-queue-demos.ts +114 -0
- package/src/docs/example/docs-router-demos.ts +89 -0
- package/src/docs/example/docs-submit-demos.ts +455 -0
- package/src/docs/example/docs-toggle-demos.ts +73 -0
- package/src/docs/example/docs-user-two-scopes.ts +37 -0
- package/src/docs/example/docs-users-list.ts +71 -0
- package/src/docs/example/users.ts +41 -24
- package/src/docs/mock-api/api-config-mock.ts +152 -0
- package/src/docs/mock-api/fixtures.ts +377 -0
- package/src/docs/mock-api/register.ts +25 -0
- package/src/docs/mock-api/router.ts +234 -0
- package/src/docs/mock-api/service-worker.ts +23 -0
- package/src/docs/mock-api/urls.ts +11 -0
- package/src/docs/navigation/navigation.ts +39 -7
- package/src/docs/search/docs-search.json +4021 -936
- package/src/docs/search/markdown-renderer.ts +7 -3
- package/src/docs/search/page.ts +11 -14
- package/src/docs/search/sonic-code-markdown.spec.ts +29 -0
- package/src/docs/search/sonic-code-markdown.ts +28 -0
- package/src/docs.ts +4 -0
- package/src/tsconfig.json +87 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- package/vite.config.mts +8 -0
- package/docs/assets/index-CaysOMFz.js +0 -5046
- package/docs/assets/index-D8mGoXzF.css +0 -1
- package/docs/src/docs/_misc/templates-demo.md +0 -19
- package/src/docs/_misc/templates-demo.md +0 -19
|
@@ -64,34 +64,8 @@
|
|
|
64
64
|
</template>
|
|
65
65
|
</sonic-code>
|
|
66
66
|
|
|
67
|
-
## Example of use
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
<
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
|
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`).
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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,
|
|
8
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
// re-connected, re-subscribe to make the directive operable again
|
|
145
|
+
|
|
62
146
|
reconnected() {
|
|
63
|
-
if (!this.
|
|
64
|
-
this.
|
|
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;
|