@supersoniks/concorde 4.5.2 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +23 -0
- package/README.md +106 -55
- package/ai/AGENTS.md +52 -0
- package/ai/README.md +30 -0
- package/ai/cursor/rules/concorde-menu.mdc +15 -0
- package/ai/cursor/rules/concorde-scope.mdc +14 -0
- package/ai/cursor/rules/concorde-theme.mdc +13 -0
- package/ai/cursor/rules/concorde.mdc +49 -0
- package/ai/jetbrains/rules/concorde.md +39 -0
- package/ai/skills/concorde/SKILL.md +220 -0
- package/ai/skills/concorde-get-set-dp/SKILL.md +194 -0
- package/ai/skills/concorde-imports/SKILL.md +78 -0
- package/ai/skills/concorde-menu/SKILL.md +74 -0
- package/ai/skills/concorde-scope/SKILL.md +70 -0
- package/ai/skills/concorde-theme/SKILL.md +46 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +159 -159
- package/concorde-core.es.js +1915 -1809
- package/dist/altcha-widget.js +2662 -0
- package/dist/concorde-core.bundle.js +159 -159
- package/dist/concorde-core.es.js +1915 -1809
- package/dist/docs-mock-api-sw.js +589 -0
- package/dist/docs-mock-api-sw.js.map +7 -0
- package/docs/altcha-widget.js +2662 -0
- package/docs/assets/index-D9pxaQYK.js +7508 -0
- package/docs/assets/index-t0-i22oI.css +1 -0
- package/docs/docs-mock-api-sw.js +589 -0
- package/docs/docs-mock-api-sw.js.map +7 -0
- package/docs/index.html +2 -2
- package/docs/src/core/components/functional/fetch/fetch.md +13 -11
- package/docs/src/core/components/functional/if/if.md +4 -11
- package/docs/src/core/components/functional/list/list.md +60 -194
- package/docs/src/core/components/functional/queue/queue.md +70 -85
- package/docs/src/core/components/functional/router/router.md +62 -97
- package/docs/src/core/components/functional/states/states.md +2 -2
- package/docs/src/core/components/functional/submit/submit.md +86 -55
- package/docs/src/core/components/ui/captcha/captcha.md +2 -2
- package/docs/src/core/components/ui/card/card.md +1 -1
- package/docs/src/core/components/ui/form/checkbox/checkbox.md +5 -32
- package/docs/src/core/components/ui/form/input/input.md +5 -30
- package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
- package/docs/src/core/components/ui/form/radio/radio.md +5 -32
- package/docs/src/core/components/ui/form/select/select.md +5 -31
- package/docs/src/core/components/ui/form/switch/switch.md +5 -32
- package/docs/src/core/components/ui/loader/loader.md +1 -13
- package/docs/src/core/components/ui/table/table.md +3 -3
- package/docs/src/docs/_core-concept/dataFlow.md +73 -0
- package/docs/src/docs/_core-concept/subscriber.md +9 -10
- package/docs/src/docs/_decorators/ancestor-attribute.md +4 -3
- package/docs/src/docs/_decorators/auto-subscribe.md +19 -16
- package/docs/src/docs/_decorators/bind.md +20 -17
- package/docs/src/docs/_decorators/get.md +7 -4
- package/docs/src/docs/_decorators/handle.md +171 -0
- package/docs/src/docs/_decorators/on-assign.md +99 -73
- package/docs/src/docs/_decorators/publish.md +2 -1
- package/docs/src/docs/_decorators/subscribe.md +70 -9
- package/docs/src/docs/_decorators/wait-for-ancestors.md +13 -10
- package/docs/src/docs/_directives/sub.md +91 -0
- package/docs/src/docs/_getting-started/ai-agents.md +56 -0
- package/docs/src/docs/_getting-started/concorde-manual-install.md +133 -0
- package/docs/src/docs/_getting-started/concorde-outside.md +13 -123
- package/docs/src/docs/_getting-started/create-a-component.md +2 -0
- package/docs/src/docs/_getting-started/my-first-component.md +236 -0
- package/docs/src/docs/_getting-started/my-first-subscriber.md +29 -83
- package/docs/src/docs/_getting-started/pubsub.md +21 -134
- package/docs/src/docs/_getting-started/start.md +26 -18
- package/docs/src/docs/_misc/api-configuration.md +79 -0
- package/docs/src/docs/_misc/dataProviderKey.md +38 -5
- package/docs/src/docs/_misc/docs-mock-api.md +60 -0
- package/docs/src/docs/_misc/endpoint.md +2 -1
- package/docs/src/docs/_misc/html-integration.md +13 -0
- package/docs/src/docs/search/docs-search.json +4163 -873
- package/docs/src/tsconfig.json +380 -317
- package/gitlab/job_tests.sh +55 -0
- package/package.json +37 -3
- package/public/altcha-widget.js +2662 -0
- package/public/docs-mock-api-sw.js +589 -0
- package/public/docs-mock-api-sw.js.map +7 -0
- package/scripts/ai-init.mjs +167 -0
- package/scripts/docs-mock-api-vite-plugin.ts +116 -0
- package/scripts/docs-open-in-editor-plugin.ts +130 -0
- package/scripts/pre-publish.mjs +2 -1
- package/src/core/components/functional/example/example.ts +1 -1
- package/src/core/components/functional/fetch/fetch.md +13 -11
- package/src/core/components/functional/if/if.md +4 -11
- package/src/core/components/functional/list/list.demo.ts +4 -4
- package/src/core/components/functional/list/list.md +60 -194
- package/src/core/components/functional/list/list.ts +8 -7
- package/src/core/components/functional/queue/queue.demo.ts +1 -1
- package/src/core/components/functional/queue/queue.md +70 -85
- package/src/core/components/functional/queue/queue.ts +4 -4
- package/src/core/components/functional/router/router.md +62 -97
- package/src/core/components/functional/router/router.ts +1 -1
- package/src/core/components/functional/states/states.md +2 -2
- package/src/core/components/functional/submit/submit.md +86 -55
- package/src/core/components/functional/submit/submit.ts +10 -3
- package/src/core/components/ui/captcha/captcha.md +2 -2
- package/src/core/components/ui/card/card.md +1 -1
- package/src/core/components/ui/form/checkbox/checkbox.md +5 -32
- package/src/core/components/ui/form/input/input.md +5 -30
- package/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +6 -4
- package/src/core/components/ui/form/radio/radio.md +5 -32
- package/src/core/components/ui/form/select/select.md +5 -31
- package/src/core/components/ui/form/switch/switch.md +5 -32
- package/src/core/components/ui/loader/loader.md +1 -13
- package/src/core/components/ui/table/table.md +3 -3
- package/src/core/decorators/Subscriber.ts +2 -0
- package/src/core/decorators/subscriber/handle.disambig.spec.ts +20 -0
- package/src/core/decorators/subscriber/handle.skip.spec.ts +37 -0
- package/src/core/decorators/subscriber/handle.ts +128 -0
- package/src/core/decorators/subscriber/onAssign.ts +94 -4
- package/src/core/directives/DataProvider.sub.spec.ts +96 -0
- package/src/core/directives/DataProvider.ts +109 -40
- package/src/core/utils/PublisherProxy.ts +33 -18
- package/src/core/utils/dataProviderKey.ts +23 -0
- package/src/core/utils/publisherPathKey.spec.ts +58 -0
- package/src/decorators.ts +6 -0
- package/src/docs/_core-concept/dataFlow.md +73 -0
- package/src/docs/_core-concept/subscriber.md +9 -10
- package/src/docs/_decorators/ancestor-attribute.md +4 -3
- package/src/docs/_decorators/auto-subscribe.md +19 -16
- package/src/docs/_decorators/bind.md +20 -17
- package/src/docs/_decorators/get.md +7 -4
- package/src/docs/_decorators/handle.md +171 -0
- package/src/docs/_decorators/on-assign.md +99 -47
- package/src/docs/_decorators/publish.md +2 -1
- package/src/docs/_decorators/subscribe.md +70 -9
- package/src/docs/_decorators/wait-for-ancestors.md +13 -10
- package/src/docs/_directives/sub.md +91 -0
- package/src/docs/_getting-started/ai-agents.md +56 -0
- package/src/docs/_getting-started/concorde-manual-install.md +133 -0
- package/src/docs/_getting-started/concorde-outside.md +13 -123
- package/src/docs/_getting-started/create-a-component.md +2 -0
- package/src/docs/_getting-started/my-first-component.md +236 -0
- package/src/docs/_getting-started/my-first-subscriber.md +29 -83
- package/src/docs/_getting-started/pubsub.md +21 -134
- package/src/docs/_getting-started/start.md +26 -18
- package/src/docs/_misc/api-configuration.md +79 -0
- package/src/docs/_misc/dataProviderKey.md +38 -5
- package/src/docs/_misc/docs-mock-api.md +60 -0
- package/src/docs/_misc/endpoint.md +2 -1
- package/src/docs/_misc/html-integration.md +13 -0
- package/src/docs/code.ts +58 -12
- package/src/docs/components/docs-demo-sources.ts +397 -0
- package/src/docs/components/docs-lit-demo-raw.ts +28 -0
- package/src/docs/components/docs-lit-demo.ts +166 -0
- package/src/docs/components/docs-source-link.ts +72 -0
- package/src/docs/docs-location.ts +54 -0
- package/src/docs/docs.ts +12 -0
- package/src/docs/example/decorators-demo-bind-demos.ts +41 -46
- package/src/docs/example/decorators-demo-geo.ts +16 -11
- package/src/docs/example/decorators-demo-init.ts +2 -228
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +142 -12
- package/src/docs/example/decorators-demo.ts +71 -70
- package/src/docs/example/docs-api-config-demos.ts +234 -0
- package/src/docs/example/docs-joke-demos.ts +297 -0
- package/src/docs/example/docs-list-demos.ts +179 -0
- package/src/docs/example/docs-provider-keys.ts +315 -0
- package/src/docs/example/docs-queue-demos.ts +114 -0
- package/src/docs/example/docs-router-demos.ts +89 -0
- package/src/docs/example/docs-submit-demos.ts +455 -0
- package/src/docs/example/docs-toggle-demos.ts +73 -0
- package/src/docs/example/docs-user-two-scopes.ts +37 -0
- package/src/docs/example/docs-users-list.ts +71 -0
- package/src/docs/example/users.ts +41 -24
- package/src/docs/mock-api/api-config-mock.ts +152 -0
- package/src/docs/mock-api/fixtures.ts +377 -0
- package/src/docs/mock-api/register.ts +25 -0
- package/src/docs/mock-api/router.ts +234 -0
- package/src/docs/mock-api/service-worker.ts +23 -0
- package/src/docs/mock-api/urls.ts +11 -0
- package/src/docs/navigation/navigation.ts +43 -7
- package/src/docs/search/docs-search.json +4193 -858
- package/src/docs/search/markdown-renderer.ts +7 -3
- package/src/docs/search/page.ts +11 -14
- package/src/docs/search/sonic-code-markdown.spec.ts +29 -0
- package/src/docs/search/sonic-code-markdown.ts +28 -0
- package/src/docs.ts +4 -0
- package/src/tsconfig.json +96 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- package/vite.config.mts +8 -0
- package/docs/assets/index-CaysOMFz.js +0 -5046
- package/docs/assets/index-D8mGoXzF.css +0 -1
- package/docs/src/docs/_misc/templates-demo.md +0 -19
- package/src/docs/_misc/templates-demo.md +0 -19
|
@@ -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>
|
|
@@ -2,6 +2,8 @@ export { bind } from "./subscriber/bind";
|
|
|
2
2
|
export { publish } from "./subscriber/publish";
|
|
3
3
|
export { subscribe } from "./subscriber/subscribe";
|
|
4
4
|
export { onAssign } from "./subscriber/onAssign";
|
|
5
|
+
export { handle, Skip } from "./subscriber/handle";
|
|
6
|
+
export type { HandleOptions } from "./subscriber/handle";
|
|
5
7
|
export { autoSubscribe } from "./subscriber/autoSubscribe";
|
|
6
8
|
export { autoFill } from "./subscriber/autoFill";
|
|
7
9
|
export { ancestorAttribute } from "./subscriber/ancestorAttribute";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { DataProviderKey } from "../../utils/dataProviderKey";
|
|
3
|
+
|
|
4
|
+
function isDataProviderKey(value: unknown): boolean {
|
|
5
|
+
return Object.prototype.toString.call(value) === "[object DataProviderKey]";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe("handle: désambiguïsation clé vs options", () => {
|
|
9
|
+
it("reconnaît une DataProviderKey (y compris après navigation)", () => {
|
|
10
|
+
const key = new DataProviderKey<{ total: number }>("cart");
|
|
11
|
+
expect(isDataProviderKey(key)).toBe(true);
|
|
12
|
+
expect(isDataProviderKey(key.total)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("ne confond pas un objet d'options avec une clé", () => {
|
|
16
|
+
expect(isDataProviderKey({ skip: ["emptyObject"] })).toBe(false);
|
|
17
|
+
expect(isDataProviderKey({ waitForAllDefined: true })).toBe(false);
|
|
18
|
+
expect(isDataProviderKey(undefined)).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Skip, isSkipped } from "./onAssign";
|
|
3
|
+
|
|
4
|
+
describe("isSkipped (catégories Skip de @handle)", () => {
|
|
5
|
+
it("Nullish : null et undefined uniquement", () => {
|
|
6
|
+
expect(isSkipped(null, [Skip.Nullish])).toBe(true);
|
|
7
|
+
expect(isSkipped(undefined, [Skip.Nullish])).toBe(true);
|
|
8
|
+
expect(isSkipped(0, [Skip.Nullish])).toBe(false);
|
|
9
|
+
expect(isSkipped("", [Skip.Nullish])).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("EmptyString : pas de coercition", () => {
|
|
13
|
+
expect(isSkipped("", [Skip.EmptyString])).toBe(true);
|
|
14
|
+
expect(isSkipped("a", [Skip.EmptyString])).toBe(false);
|
|
15
|
+
expect(isSkipped(0, [Skip.EmptyString])).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("EmptyObject : objet sans clé, tableau exclu", () => {
|
|
19
|
+
expect(isSkipped({}, [Skip.EmptyObject])).toBe(true);
|
|
20
|
+
expect(isSkipped({ a: 1 }, [Skip.EmptyObject])).toBe(false);
|
|
21
|
+
expect(isSkipped([], [Skip.EmptyObject])).toBe(false);
|
|
22
|
+
expect(isSkipped(null, [Skip.EmptyObject])).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("EmptyArray : tableau vide, objet exclu", () => {
|
|
26
|
+
expect(isSkipped([], [Skip.EmptyArray])).toBe(true);
|
|
27
|
+
expect(isSkipped([1], [Skip.EmptyArray])).toBe(false);
|
|
28
|
+
expect(isSkipped({}, [Skip.EmptyArray])).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("combinaison de catégories", () => {
|
|
32
|
+
const kinds = [Skip.Nullish, Skip.EmptyObject];
|
|
33
|
+
expect(isSkipped(null, kinds)).toBe(true);
|
|
34
|
+
expect(isSkipped({}, kinds)).toBe(true);
|
|
35
|
+
expect(isSkipped({ a: 1 }, kinds)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataProviderKey,
|
|
3
|
+
DataProviderKeyHost,
|
|
4
|
+
} from "../../utils/dataProviderKey";
|
|
5
|
+
import { createOnAssign, Skip } from "./onAssign";
|
|
6
|
+
|
|
7
|
+
export { Skip } from "./onAssign";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options de `@handle`, nommées d'après les cas d'usage réels rencontrés dans
|
|
11
|
+
* les projets (panier, file d'attente, formulaires multi-publishers, etc.).
|
|
12
|
+
*
|
|
13
|
+
* Par défaut (aucune option), `@handle` appelle la méthode **à chaque
|
|
14
|
+
* assignation**, même quand la valeur reçue est `null`/`undefined` (c'est la
|
|
15
|
+
* différence de comportement voulue par rapport à `@onAssign`).
|
|
16
|
+
*/
|
|
17
|
+
export type HandleOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* Attendre que **toutes** les clés surveillées soient définies (non
|
|
20
|
+
* `null`/`undefined`) avant d'appeler la méthode. Reproduit la sémantique
|
|
21
|
+
* historique de `@onAssign`.
|
|
22
|
+
*
|
|
23
|
+
* À utiliser quand la logique combine plusieurs sources et n'a de sens que
|
|
24
|
+
* lorsque toutes sont prêtes (ex. calcul de date à partir de
|
|
25
|
+
* `date` + `timeZone` + `direction`).
|
|
26
|
+
*/
|
|
27
|
+
waitForAllDefined?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Ne pas appeler la méthode si la valeur reçue appartient à l'une de ces
|
|
30
|
+
* catégories. Chaque entrée est une catégorie nommée (pas une valeur), donc
|
|
31
|
+
* aucune ambiguïté valeur/motif.
|
|
32
|
+
*
|
|
33
|
+
* - `Skip.Nullish` ignore `null`/`undefined` ;
|
|
34
|
+
* - `Skip.EmptyObject` / `Skip.EmptyArray` ignorent `{}` / `[]` ;
|
|
35
|
+
* - `Skip.EmptyString` ignore `""`.
|
|
36
|
+
*
|
|
37
|
+
* Pratique quand un publisher non initialisé émet `{}` comme état « pas encore
|
|
38
|
+
* chargé » : `skip: [Skip.EmptyObject]`. Pour une validation arbitraire sur une
|
|
39
|
+
* valeur précise, faire le test directement dans la méthode.
|
|
40
|
+
*/
|
|
41
|
+
skip?: Skip[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type HandleDecorator<Host, Fn> = (
|
|
45
|
+
target: Host,
|
|
46
|
+
propertyKey: string,
|
|
47
|
+
descriptor: TypedPropertyDescriptor<Fn>,
|
|
48
|
+
) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Détecte une `DataProviderKey` (Proxy marqué `Symbol.toStringTag`) afin de la
|
|
52
|
+
* distinguer d'un objet d'options passé en dernier argument.
|
|
53
|
+
*/
|
|
54
|
+
function isDataProviderKey(value: unknown): value is DataProviderKey<unknown> {
|
|
55
|
+
return Object.prototype.toString.call(value) === "[object DataProviderKey]";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Callback typé déclenché lorsqu'un (ou plusieurs) chemin(s) publisher sont
|
|
60
|
+
* assignés via `DataProviderKey<T>`. Invoque la méthode décorée avec la/les
|
|
61
|
+
* valeur(s) assignée(s) (calculs, effets de bord, mise à jour d'autres `@state`…).
|
|
62
|
+
*
|
|
63
|
+
* Supporte les chemins dynamiques : placeholders type `"users.${userIndex}"`.
|
|
64
|
+
*
|
|
65
|
+
* Contrairement à `@onAssign`, la méthode est appelée à chaque assignation,
|
|
66
|
+
* même quand la valeur reçue est `null`/`undefined` — sauf si une option
|
|
67
|
+
* (`waitForAllDefined`, `skip`) restreint le déclenchement.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // Mono-clé
|
|
71
|
+
* const cart = new DataProviderKey<Cart>("cart");
|
|
72
|
+
* @handle(cart.total)
|
|
73
|
+
* updateSummary(total: number) {
|
|
74
|
+
* this.summary = total * this.taxRate;
|
|
75
|
+
* }
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Multi-clés (coordination de plusieurs publishers)
|
|
79
|
+
* @handle(config.show, idle.isIdle, { waitForAllDefined: true })
|
|
80
|
+
* onModal(show: boolean, isIdle: boolean) {
|
|
81
|
+
* if (show && isIdle) this.open(); else this.close();
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Ignorer un objet vide émis par un publisher non initialisé
|
|
86
|
+
* @handle(user.profile, { skip: [Skip.EmptyObject] })
|
|
87
|
+
* onProfile(profile: Profile) { this.name = profile.name; }
|
|
88
|
+
*/
|
|
89
|
+
export function handle<A, U = any>(
|
|
90
|
+
key: DataProviderKey<A, U>,
|
|
91
|
+
options?: HandleOptions,
|
|
92
|
+
): HandleDecorator<DataProviderKeyHost<U>, (a: A) => void>;
|
|
93
|
+
export function handle<A, B, UA = any, UB = any>(
|
|
94
|
+
keyA: DataProviderKey<A, UA>,
|
|
95
|
+
keyB: DataProviderKey<B, UB>,
|
|
96
|
+
options?: HandleOptions,
|
|
97
|
+
): HandleDecorator<
|
|
98
|
+
DataProviderKeyHost<UA> & DataProviderKeyHost<UB>,
|
|
99
|
+
(a: A, b: B) => void
|
|
100
|
+
>;
|
|
101
|
+
export function handle<A, B, C, UA = any, UB = any, UC = any>(
|
|
102
|
+
keyA: DataProviderKey<A, UA>,
|
|
103
|
+
keyB: DataProviderKey<B, UB>,
|
|
104
|
+
keyC: DataProviderKey<C, UC>,
|
|
105
|
+
options?: HandleOptions,
|
|
106
|
+
): HandleDecorator<
|
|
107
|
+
DataProviderKeyHost<UA> & DataProviderKeyHost<UB> & DataProviderKeyHost<UC>,
|
|
108
|
+
(a: A, b: B, c: C) => void
|
|
109
|
+
>;
|
|
110
|
+
export function handle(
|
|
111
|
+
...args: Array<DataProviderKey<unknown> | HandleOptions | undefined>
|
|
112
|
+
) {
|
|
113
|
+
const last = args[args.length - 1];
|
|
114
|
+
const hasOptions = last !== undefined && !isDataProviderKey(last);
|
|
115
|
+
const options = (hasOptions ? last : {}) as HandleOptions;
|
|
116
|
+
const keys = (hasOptions ? args.slice(0, -1) : args) as Array<
|
|
117
|
+
DataProviderKey<unknown>
|
|
118
|
+
>;
|
|
119
|
+
const paths = keys.map((key) => key.path);
|
|
120
|
+
|
|
121
|
+
return createOnAssign(
|
|
122
|
+
{
|
|
123
|
+
dispatchWhenUndefined: !options.waitForAllDefined,
|
|
124
|
+
skip: options.skip,
|
|
125
|
+
},
|
|
126
|
+
paths,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -23,7 +23,100 @@ type Configuration = {
|
|
|
23
23
|
index: number;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Catégories de valeurs « ignorables » par le garde `skip`. Chaque catégorie est
|
|
28
|
+
* un prédicat nommé (et non une égalité de valeur), ce qui lève toute ambiguïté
|
|
29
|
+
* entre « valeur » et « motif » (notamment pour l'objet vide).
|
|
30
|
+
*/
|
|
31
|
+
export enum Skip {
|
|
32
|
+
/**
|
|
33
|
+
* `null` ou `undefined`. En pratique un publisher renvoie toujours `null`
|
|
34
|
+
* (jamais `undefined`) car `get()` coerce `undefined → null`.
|
|
35
|
+
*/
|
|
36
|
+
Nullish = "nullish",
|
|
37
|
+
/** Chaîne vide `""`. */
|
|
38
|
+
EmptyString = "emptyString",
|
|
39
|
+
/** Objet sans clé (`{}`), tableau exclu. */
|
|
40
|
+
EmptyObject = "emptyObject",
|
|
41
|
+
/** Tableau vide (`[]`). */
|
|
42
|
+
EmptyArray = "emptyArray",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SKIP_PREDICATES: Record<Skip, (value: unknown) => boolean> = {
|
|
46
|
+
[Skip.Nullish]: (v) => v === null || v === undefined,
|
|
47
|
+
[Skip.EmptyString]: (v) => v === "",
|
|
48
|
+
[Skip.EmptyObject]: (v) =>
|
|
49
|
+
typeof v === "object" &&
|
|
50
|
+
v !== null &&
|
|
51
|
+
!Array.isArray(v) &&
|
|
52
|
+
Object.keys(v).length === 0,
|
|
53
|
+
[Skip.EmptyArray]: (v) => Array.isArray(v) && v.length === 0,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `true` si `value` appartient à au moins une des catégories `kinds`.
|
|
58
|
+
*/
|
|
59
|
+
export function isSkipped(value: unknown, kinds: Skip[]): boolean {
|
|
60
|
+
return kinds.some((kind) => SKIP_PREDICATES[kind](value));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type OnAssignOptions = {
|
|
64
|
+
/**
|
|
65
|
+
* Quand `true`, le callback est invoqué à chaque assignation, même si la
|
|
66
|
+
* valeur reçue est `null`/`undefined`.
|
|
67
|
+
* Quand `false` (défaut), le callback n'est invoqué que lorsque toutes les
|
|
68
|
+
* valeurs surveillées sont définies (non `null` et non `undefined`).
|
|
69
|
+
*/
|
|
70
|
+
dispatchWhenUndefined?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Ne pas invoquer le callback si une valeur reçue appartient à l'une de ces
|
|
73
|
+
* catégories (ex. `[Skip.Nullish, Skip.EmptyObject]`).
|
|
74
|
+
*/
|
|
75
|
+
skip?: Skip[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function shouldDispatch(
|
|
79
|
+
currentValues: unknown[],
|
|
80
|
+
expectedCount: number,
|
|
81
|
+
options: OnAssignOptions,
|
|
82
|
+
): boolean {
|
|
83
|
+
// Garde structurelle : toutes les valeurs doivent être définies.
|
|
84
|
+
if (!options.dispatchWhenUndefined) {
|
|
85
|
+
const definedCount = currentValues
|
|
86
|
+
.slice(0, expectedCount)
|
|
87
|
+
.filter((v) => v !== null && v !== undefined).length;
|
|
88
|
+
if (definedCount !== expectedCount) return false;
|
|
89
|
+
}
|
|
90
|
+
// Garde de contenu : catégories skip.
|
|
91
|
+
if (options.skip && options.skip.length > 0) {
|
|
92
|
+
for (let i = 0; i < expectedCount; i++) {
|
|
93
|
+
if (isSkipped(currentValues[i], options.skip)) return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @deprecated Utiliser `@handle` à la place.
|
|
101
|
+
*
|
|
102
|
+
* `@onAssign` prend des chemins sous forme de **chaînes** non typées et n'appelle
|
|
103
|
+
* la méthode que lorsque **toutes** les valeurs sont définies. `@handle` offre la
|
|
104
|
+
* même chose en **typé** (via `DataProviderKey`), avec des options explicites :
|
|
105
|
+
*
|
|
106
|
+
* - équivalent direct : `@onAssign("a", "b")` → `@handle(keyA, keyB, { waitForAllDefined: true })`
|
|
107
|
+
* - comportement par défaut de `@handle` : appel à chaque assignation (même `null`/`undefined`)
|
|
108
|
+
* - `skip` pour ignorer des catégories de valeurs (ex. `[Skip.Nullish, Skip.EmptyObject]`)
|
|
109
|
+
*
|
|
110
|
+
* `@onAssign` reste fonctionnel et inchangé le temps de la migration.
|
|
111
|
+
*/
|
|
26
112
|
export function onAssign(...values: Array<string>) {
|
|
113
|
+
return createOnAssign({}, values);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createOnAssign(
|
|
117
|
+
options: OnAssignOptions,
|
|
118
|
+
values: Array<string>,
|
|
119
|
+
) {
|
|
27
120
|
const pathConfigs: PathConfiguration[] = values.map((path) => {
|
|
28
121
|
const dynamicDependencies = extractDynamicDependencies(path);
|
|
29
122
|
return {
|
|
@@ -69,10 +162,7 @@ export function onAssign(...values: Array<string>) {
|
|
|
69
162
|
const callbacks: Set<Callback> = new Set();
|
|
70
163
|
const onAssign = (assignedValue: unknown) => {
|
|
71
164
|
onAssignValues[i] = assignedValue;
|
|
72
|
-
if (
|
|
73
|
-
onAssignValues.filter((v) => v !== null && v !== undefined)
|
|
74
|
-
.length === values.length
|
|
75
|
-
) {
|
|
165
|
+
if (shouldDispatch(onAssignValues, values.length, options)) {
|
|
76
166
|
callbacks.forEach((callback) => callback(...onAssignValues));
|
|
77
167
|
}
|
|
78
168
|
};
|
|
@@ -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
|
+
}
|