@supersoniks/concorde 4.6.0 → 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 +127 -127
- package/concorde-core.es.js +1435 -1364
- package/dist/altcha-widget.js +2662 -0
- package/dist/concorde-core.bundle.js +127 -127
- package/dist/concorde-core.es.js +1435 -1364
- 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/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/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>
|
|
@@ -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;
|
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
CoreJSType,
|
|
10
10
|
PublisherContentType,
|
|
11
11
|
} from "../_types/types";
|
|
12
|
+
import type { DataProviderKey } from "./dataProviderKey";
|
|
13
|
+
import { resolveStaticPublisherPath } from "./dataProviderKey";
|
|
12
14
|
import HTML from "./HTML";
|
|
13
15
|
import Objects from "./Objects";
|
|
14
16
|
import { sonicClassPrefix } from "./Utils";
|
|
@@ -1058,13 +1060,17 @@ export const getObservables = <T = any>(
|
|
|
1058
1060
|
return new Set<DataProvider<T>>([observable as DataProvider<T>]);
|
|
1059
1061
|
};
|
|
1060
1062
|
|
|
1061
|
-
// get value
|
|
1062
|
-
export
|
|
1063
|
-
|
|
1064
|
-
|
|
1063
|
+
// get value (snapshot at call time)
|
|
1064
|
+
export function get<T>(id: DataProviderKey<T>): T;
|
|
1065
|
+
export function get<T = any>(id: string): T;
|
|
1066
|
+
export function get<T>(id: string | DataProviderKey<T>): T {
|
|
1067
|
+
const path = resolveStaticPublisherPath(id);
|
|
1068
|
+
return getObservables<T>(path).values().next().value?.get() as T;
|
|
1069
|
+
}
|
|
1065
1070
|
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1071
|
+
function deepee<T>(id: string | DataProviderKey<T>, _defaultValue?: T) {
|
|
1072
|
+
const path = resolveStaticPublisherPath(id);
|
|
1073
|
+
const value = getObservables<T>(path).values().next().value as DataProvider<T>;
|
|
1068
1074
|
// if (defaultValue !== undefined && value) {
|
|
1069
1075
|
// const innerValue = value.get();
|
|
1070
1076
|
// if (Objects.isEmpty(innerValue as Record<string, any>)) {
|
|
@@ -1073,20 +1079,29 @@ const deepee = <T = any>(id: string) => {
|
|
|
1073
1079
|
// }
|
|
1074
1080
|
|
|
1075
1081
|
return value;
|
|
1076
|
-
}
|
|
1082
|
+
}
|
|
1077
1083
|
|
|
1078
|
-
export
|
|
1079
|
-
|
|
1084
|
+
export function dataProvider<T>(id: DataProviderKey<T>, defaultValue?: T): DataProvider<T>;
|
|
1085
|
+
export function dataProvider<T = any>(id: string, defaultValue?: T): DataProvider<T>;
|
|
1086
|
+
export function dataProvider<T>(
|
|
1087
|
+
id: string | DataProviderKey<T>,
|
|
1080
1088
|
defaultValue?: T,
|
|
1081
|
-
)
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
)
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1089
|
+
): DataProvider<T> {
|
|
1090
|
+
return deepee(id, defaultValue);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export function dp<T>(id: DataProviderKey<T>, defaultValue?: T): DataProvider<T>;
|
|
1094
|
+
export function dp<T = any>(id: string, defaultValue?: T): DataProvider<T>;
|
|
1095
|
+
export function dp<T>(id: string | DataProviderKey<T>, defaultValue?: T): DataProvider<T> {
|
|
1096
|
+
return deepee(id, defaultValue);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export function set<T>(id: DataProviderKey<T>, value: T): void;
|
|
1100
|
+
export function set<T = any>(id: string, value: T): void;
|
|
1101
|
+
export function set<T>(id: string | DataProviderKey<T>, value: T): void {
|
|
1102
|
+
const path = resolveStaticPublisherPath(id);
|
|
1103
|
+
getObservables(path).values().next().value?.set(value);
|
|
1104
|
+
}
|
|
1090
1105
|
|
|
1091
1106
|
/**
|
|
1092
1107
|
* next back handling data invalidation
|
|
@@ -93,6 +93,29 @@ export interface DataProviderKeyConstructor {
|
|
|
93
93
|
new <T, U = any>(path: string): DataProviderKey<T, U>;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Placeholders `${…}` / `{$…}` — resolved only by decorators (@subscribe, @publish, …). */
|
|
97
|
+
const DYNAMIC_PUBLISHER_PATH = /\$\{|\{\$/;
|
|
98
|
+
|
|
99
|
+
export function isDynamicPublisherPath(path: string): boolean {
|
|
100
|
+
return DYNAMIC_PUBLISHER_PATH.test(path);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type PublisherPathInput = string | {path: string};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolves a static publisher path for get / set / dp.
|
|
107
|
+
* Rejects DataProviderKey templates with dynamic placeholders.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveStaticPublisherPath(input: PublisherPathInput): string {
|
|
110
|
+
const path = typeof input === "string" ? input : input.path;
|
|
111
|
+
if (isDynamicPublisherPath(path)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"Static publisher path required for get/set/dp. Use @subscribe, @publish, or @handle for dynamic DataProviderKey paths.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
97
120
|
export const DataProviderKey: DataProviderKeyConstructor = function (
|
|
98
121
|
this: any,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
DataProviderKey,
|
|
4
|
+
isDynamicPublisherPath,
|
|
5
|
+
resolveStaticPublisherPath,
|
|
6
|
+
} from "./dataProviderKey";
|
|
7
|
+
import { dp, get, set } from "./PublisherProxy";
|
|
8
|
+
|
|
9
|
+
type Counter = { count: number };
|
|
10
|
+
|
|
11
|
+
describe("resolveStaticPublisherPath", () => {
|
|
12
|
+
it("accepte une string statique", () => {
|
|
13
|
+
expect(resolveStaticPublisherPath("myCounter")).toBe("myCounter");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("accepte une DataProviderKey statique", () => {
|
|
17
|
+
const key = new DataProviderKey<Counter>("myCounter").count;
|
|
18
|
+
expect(resolveStaticPublisherPath(key)).toBe("myCounter.count");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejette un chemin avec placeholder ${…}", () => {
|
|
22
|
+
expect(isDynamicPublisherPath("users.${userIndex}")).toBe(true);
|
|
23
|
+
expect(() =>
|
|
24
|
+
resolveStaticPublisherPath("users.${userIndex}"),
|
|
25
|
+
).toThrow(/Static publisher path required/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("rejette une clé dynamique", () => {
|
|
29
|
+
const key = new DataProviderKey<Counter, { userIndex: number }>(
|
|
30
|
+
"users.${userIndex}",
|
|
31
|
+
);
|
|
32
|
+
expect(() => resolveStaticPublisherPath(key)).toThrow(
|
|
33
|
+
/Static publisher path required/,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("get / set / dp with DataProviderKey", () => {
|
|
39
|
+
const counterKey = new DataProviderKey<{ count: number }>(
|
|
40
|
+
"publisherPathKeySpecCounter",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
it("set et get snapshot via clé racine", () => {
|
|
44
|
+
set(counterKey, { count: 0 });
|
|
45
|
+
expect(get(counterKey)).toEqual({ count: 0 });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("dp sur sous-clé et écriture feuille", () => {
|
|
49
|
+
set(counterKey, { count: 0 });
|
|
50
|
+
dp(counterKey.count).set(3);
|
|
51
|
+
expect(get(counterKey)).toEqual({ count: 3 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("accepte encore un chemin string", () => {
|
|
55
|
+
set(`${counterKey.path}.legacy`, { count: 7 });
|
|
56
|
+
expect(get(`${counterKey.path}.legacy`)).toEqual({ count: 7 });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Data flow
|
|
2
|
+
|
|
3
|
+
Recommended patterns for new Concorde apps (Lit + TypeScript). Under the hood, data lives in a **DataProvider** store (legacy **Publisher** API: [Legacy: Sharing data](#docs/_getting-started/pubsub.md/pubsub)).
|
|
4
|
+
|
|
5
|
+
## Quick map
|
|
6
|
+
|
|
7
|
+
| Need | Use |
|
|
8
|
+
|------|-----|
|
|
9
|
+
| Read/write in code | `get` / `set` / `dp` + `DataProviderKey` (static paths only) |
|
|
10
|
+
| Reactive Lit template | `sub(key)` or `@subscribe` |
|
|
11
|
+
| Read component state from store | `@subscribe` + `DataProviderKey<T, U>` + `@state` |
|
|
12
|
+
| Inherit ancestor attributes | `@ancestorAttribute` |
|
|
13
|
+
| Write from component state | `@publish` |
|
|
14
|
+
| React to assignments | `@handle` |
|
|
15
|
+
| HTTP GET | `@get` + `Endpoint`, or `sonic-list` / `sonic-queue` with `fetch` |
|
|
16
|
+
| Forms | `formDataProvider` + `name` on fields |
|
|
17
|
+
| Offline doc demos | `serviceURL="/docs-mock-api"` — [Local API demos](#docs/_misc/docs-mock-api.md/docs-mock-api) |
|
|
18
|
+
|
|
19
|
+
Skill: `concorde-get-set-dp` in the package `ai/` folder.
|
|
20
|
+
|
|
21
|
+
## DataProviderKey
|
|
22
|
+
|
|
23
|
+
<sonic-code language="typescript">
|
|
24
|
+
<template>
|
|
25
|
+
import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
|
|
26
|
+
import { dp, get, set } from "@supersoniks/concorde/utils";
|
|
27
|
+
|
|
28
|
+
const cartKey = new DataProviderKey<{ items: string[] }>("cart");
|
|
29
|
+
|
|
30
|
+
set(cartKey, { items: [] });
|
|
31
|
+
dp(cartKey.items).set(["a", "b"]);
|
|
32
|
+
get(cartKey);
|
|
33
|
+
</template>
|
|
34
|
+
</sonic-code>
|
|
35
|
+
|
|
36
|
+
Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.${id}")` in imperative code.
|
|
37
|
+
|
|
38
|
+
[DataProviderKey](#docs/_misc/dataProviderKey.md/dataProviderKey)
|
|
39
|
+
|
|
40
|
+
## Decorators
|
|
41
|
+
|
|
42
|
+
| Decorator | Role |
|
|
43
|
+
|-----------|------|
|
|
44
|
+
| `@subscribe` | Read-only property from store — [data configuration](#docs/_getting-started/my-first-component.md/my-first-component) (type + key + scope) |
|
|
45
|
+
| `@publish` | Push property writes to store |
|
|
46
|
+
| `@handle` | Method called on assignment |
|
|
47
|
+
| `@ancestorAttribute` | Copy ancestor HTML attribute onto property |
|
|
48
|
+
| `@get` | HTTP GET into `ApiGetResult<T>` |
|
|
49
|
+
|
|
50
|
+
Walkthrough: [My first component](#docs/_getting-started/my-first-component.md/my-first-component)
|
|
51
|
+
|
|
52
|
+
## HTTP and lists
|
|
53
|
+
|
|
54
|
+
- [@get](#docs/_decorators/get.md/get) — single request on a component
|
|
55
|
+
- [List](#core/components/functional/list/list.md/list) — `fetch` + `key="data"` + `/docs-mock-api/api/users`
|
|
56
|
+
- [Queue](#core/components/functional/queue/queue.md/queue) — lazy `offset=$offset&per_page=$limit` + optional `dataFilterProvider` (form → query)
|
|
57
|
+
|
|
58
|
+
## Starter kit
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @supersoniks/create-concorde-ts-starter my-app
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Interactive routes mirror these patterns (`/concepts/*`, `/demo/*`).
|
|
65
|
+
|
|
66
|
+
## Legacy integration
|
|
67
|
+
|
|
68
|
+
| Topic | Page |
|
|
69
|
+
|-------|------|
|
|
70
|
+
| `Subscriber` / `Fetcher` mixins on app code | [Legacy: Subscriber mixin](#docs/_core-concept/subscriber.md/subscriber), [Legacy: My first subscriber](#docs/_getting-started/my-first-subscriber.md/my-first-subscriber) |
|
|
71
|
+
| `data-bind` HTML (plain HTML hosts) | [HTML integration](#docs/_misc/html-integration.md/html-integration) — doc demos use Lit in `src/docs/example/` |
|
|
72
|
+
| `@onAssign` | [@onAssign](#docs/_decorators/on-assign.md/on-assign) (prefer `@handle`) |
|
|
73
|
+
| `sonic-fetch` alone | [Fetch](#core/components/functional/fetch/fetch.md/fetch) |
|