@supersoniks/concorde 4.7.3 → 4.8.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/README.md +1 -1
- package/ai/AGENTS.md +4 -0
- package/ai/cursor/rules/concorde.mdc +11 -1
- package/ai/jetbrains/rules/concorde.md +8 -0
- package/ai/skills/concorde/SKILL.md +29 -2
- package/ai/skills/concorde-scope/SKILL.md +2 -2
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +289 -289
- package/concorde-core.es.js +4839 -4546
- package/dist/concorde-core.bundle.js +289 -289
- package/dist/concorde-core.es.js +4839 -4546
- package/dist/docs-mock-api-sw.js +19 -0
- package/dist/docs-mock-api-sw.js.map +2 -2
- package/dist/robots.txt +2 -0
- package/docs/assets/index-wyNMyWT9.js +11196 -0
- package/docs/docs-mock-api-sw.js +19 -0
- package/docs/docs-mock-api-sw.js.map +2 -2
- package/docs/index.html +1 -1
- package/docs/robots.txt +2 -0
- package/package.json +9 -1
- package/public/docs-mock-api-sw.js +19 -0
- package/public/docs-mock-api-sw.js.map +2 -2
- package/public/robots.txt +2 -0
- package/src/core/components/functional/example/example.ts +3 -3
- package/src/core/components/ui/captcha/captcha.md +0 -12
- package/src/core/components/ui/icon/icon.ts +17 -2
- package/src/core/components/ui/menu/menu.ts +12 -3
- package/src/core/decorators/api.post.spec.ts +293 -0
- package/src/core/decorators/api.spec.ts +7 -14
- package/src/core/decorators/api.ts +648 -15
- package/src/core/decorators/subscriber/bind.ts +13 -5
- package/src/core/decorators/subscriber/dynamicPath.spec.ts +53 -0
- package/src/core/decorators/subscriber/dynamicPath.ts +23 -1
- package/src/core/decorators/subscriber/handle.ts +3 -1
- package/src/core/decorators/subscriber/onAssign.ts +10 -2
- package/src/core/decorators/subscriber/publish.ts +12 -2
- package/src/core/utils/PublisherProxy.ts +95 -11
- package/src/core/utils/api.ts +72 -3
- package/src/core/utils/dpOptions.spec.ts +56 -0
- package/src/core/utils/endpoint.ts +3 -3
- package/src/decorators.ts +17 -1
- package/src/docs/_core-concept/dataFlow.md +9 -3
- package/src/docs/_decorators/bind.md +2 -2
- package/src/docs/_decorators/get.md +13 -4
- package/src/docs/_decorators/handle.md +5 -1
- package/src/docs/_decorators/on-assign.md +2 -0
- package/src/docs/_decorators/patch.md +45 -0
- package/src/docs/_decorators/post.md +93 -0
- package/src/docs/_decorators/publish.md +1 -1
- package/src/docs/_decorators/put.md +43 -0
- package/src/docs/_decorators/subscribe.md +4 -1
- package/src/docs/_directives/sub.md +1 -1
- package/src/docs/_getting-started/my-first-component.md +1 -1
- package/src/docs/_misc/api-configuration.md +3 -1
- package/src/docs/_misc/dataProviderKey.md +2 -2
- package/src/docs/_misc/dynamic-path.md +71 -0
- package/src/docs/_misc/endpoint.md +5 -3
- package/src/docs/components/docs-demo-sources.ts +102 -3
- package/src/docs/components/docs-lit-demo-raw.ts +2 -26
- package/src/docs/components/docs-lit-demo.ts +9 -42
- package/src/docs/components/docs-source-excerpt.ts +53 -0
- package/src/docs/components/docs-source-link.ts +24 -8
- package/src/docs/components/docs-source-raw.ts +34 -0
- package/src/docs/example/decorators-demo-geo.ts +2 -2
- package/src/docs/example/decorators-demo-post.ts +249 -0
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +5 -5
- package/src/docs/example/decorators-demo.ts +1 -0
- package/src/docs/example/docs-api-config-demos.ts +5 -5
- package/src/docs/mock-api/router.ts +20 -0
- package/src/docs/navigation/navigation.ts +16 -0
- package/src/docs/search/docs-search.json +540 -15
- package/src/tsconfig.json +24 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- package/vite.config.mts +1 -1
- package/docs/assets/index-D9pxaQYK.js +0 -7508
- package/docs/src/core/components/functional/date/date.md +0 -290
- package/docs/src/core/components/functional/fetch/fetch.md +0 -125
- package/docs/src/core/components/functional/if/if.md +0 -9
- package/docs/src/core/components/functional/list/list.md +0 -65
- package/docs/src/core/components/functional/mix/mix.md +0 -41
- package/docs/src/core/components/functional/queue/queue.md +0 -72
- package/docs/src/core/components/functional/router/router.md +0 -94
- package/docs/src/core/components/functional/sdui/default-library.json +0 -108
- package/docs/src/core/components/functional/sdui/example.json +0 -99
- package/docs/src/core/components/functional/sdui/sdui.md +0 -356
- package/docs/src/core/components/functional/states/states.md +0 -87
- package/docs/src/core/components/functional/submit/submit.md +0 -114
- package/docs/src/core/components/functional/subscriber/subscriber.md +0 -91
- package/docs/src/core/components/functional/value/value.md +0 -35
- package/docs/src/core/components/ui/alert/alert.md +0 -121
- package/docs/src/core/components/ui/alert-messages/alert-messages.md +0 -0
- package/docs/src/core/components/ui/badge/badge.md +0 -127
- package/docs/src/core/components/ui/button/button.md +0 -182
- package/docs/src/core/components/ui/captcha/captcha.md +0 -24
- package/docs/src/core/components/ui/card/card.md +0 -97
- package/docs/src/core/components/ui/divider/divider.md +0 -35
- package/docs/src/core/components/ui/form/checkbox/checkbox.md +0 -77
- package/docs/src/core/components/ui/form/fieldset/fieldset.md +0 -129
- package/docs/src/core/components/ui/form/form-actions/form-actions.md +0 -77
- package/docs/src/core/components/ui/form/form-layout/form-layout.md +0 -44
- package/docs/src/core/components/ui/form/input/input.md +0 -142
- package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +0 -133
- package/docs/src/core/components/ui/form/radio/radio.md +0 -57
- package/docs/src/core/components/ui/form/select/select.md +0 -71
- package/docs/src/core/components/ui/form/switch/switch.md +0 -57
- package/docs/src/core/components/ui/form/textarea/textarea.md +0 -65
- package/docs/src/core/components/ui/group/group.md +0 -75
- package/docs/src/core/components/ui/icon/icon.md +0 -125
- package/docs/src/core/components/ui/icon/icons.json +0 -1
- package/docs/src/core/components/ui/image/image.md +0 -107
- package/docs/src/core/components/ui/link/link.md +0 -43
- package/docs/src/core/components/ui/loader/loader.md +0 -55
- package/docs/src/core/components/ui/menu/menu.md +0 -329
- package/docs/src/core/components/ui/modal/modal.md +0 -119
- package/docs/src/core/components/ui/pop/pop.md +0 -96
- package/docs/src/core/components/ui/progress/progress.md +0 -63
- package/docs/src/core/components/ui/table/table.md +0 -455
- package/docs/src/core/components/ui/toast/toast.md +0 -166
- package/docs/src/core/components/ui/tooltip/tooltip.md +0 -82
- package/docs/src/docs/_core-concept/dataFlow.md +0 -73
- package/docs/src/docs/_core-concept/overview.md +0 -57
- package/docs/src/docs/_core-concept/subscriber.md +0 -75
- package/docs/src/docs/_decorators/ancestor-attribute.md +0 -79
- package/docs/src/docs/_decorators/auto-subscribe.md +0 -202
- package/docs/src/docs/_decorators/bind.md +0 -167
- package/docs/src/docs/_decorators/get.md +0 -68
- package/docs/src/docs/_decorators/handle.md +0 -171
- package/docs/src/docs/_decorators/on-assign.md +0 -388
- package/docs/src/docs/_decorators/publish.md +0 -55
- package/docs/src/docs/_decorators/subscribe.md +0 -97
- package/docs/src/docs/_decorators/wait-for-ancestors.md +0 -163
- package/docs/src/docs/_directives/sub.md +0 -91
- package/docs/src/docs/_getting-started/ai-agents.md +0 -56
- package/docs/src/docs/_getting-started/concorde-manual-install.md +0 -133
- package/docs/src/docs/_getting-started/concorde-outside.md +0 -33
- package/docs/src/docs/_getting-started/create-a-component.md +0 -139
- package/docs/src/docs/_getting-started/my-first-component.md +0 -236
- package/docs/src/docs/_getting-started/my-first-subscriber.md +0 -120
- package/docs/src/docs/_getting-started/pubsub.md +0 -37
- package/docs/src/docs/_getting-started/start.md +0 -47
- package/docs/src/docs/_getting-started/theming.md +0 -91
- package/docs/src/docs/_misc/api-configuration.md +0 -79
- package/docs/src/docs/_misc/dataProviderKey.md +0 -168
- package/docs/src/docs/_misc/docs-mock-api.md +0 -60
- package/docs/src/docs/_misc/endpoint.md +0 -43
- package/docs/src/docs/_misc/html-integration.md +0 -13
- package/docs/src/docs/search/docs-search.json +0 -8532
- package/docs/src/tag-list.json +0 -1
- package/docs/src/tsconfig-model.json +0 -23
- package/docs/src/tsconfig.json +0 -1050
- package/php/get-challenge.php +0 -34
- package/php/some-service.php +0 -42
|
@@ -4,10 +4,15 @@ import type {
|
|
|
4
4
|
} from "../../utils/dataProviderKey";
|
|
5
5
|
import { ConnectedComponent, setSubscribable } from "./common";
|
|
6
6
|
import {
|
|
7
|
+
type DynamicPathOptions,
|
|
7
8
|
extractDynamicDependencies,
|
|
8
9
|
hasPath,
|
|
9
10
|
resolveDynamicPath,
|
|
10
11
|
} from "./dynamicPath";
|
|
12
|
+
|
|
13
|
+
export type BindOptions = DynamicPathOptions & {
|
|
14
|
+
reflect?: boolean;
|
|
15
|
+
};
|
|
11
16
|
import {
|
|
12
17
|
bindDynamicWatchKeys,
|
|
13
18
|
observeDynamicProperty,
|
|
@@ -16,9 +21,12 @@ import { getPublisherFromPath } from "./publisherPath";
|
|
|
16
21
|
|
|
17
22
|
function bindImpl(
|
|
18
23
|
path: string,
|
|
19
|
-
options?:
|
|
24
|
+
options?: BindOptions,
|
|
20
25
|
): (target: unknown, propertyKey: string) => void {
|
|
21
26
|
const reflect = options?.reflect ?? false;
|
|
27
|
+
const pathOptions: DynamicPathOptions = {
|
|
28
|
+
skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
|
|
29
|
+
};
|
|
22
30
|
const dynamicDependencies = extractDynamicDependencies(path);
|
|
23
31
|
const isDynamicPath = dynamicDependencies.length > 0;
|
|
24
32
|
|
|
@@ -139,7 +147,7 @@ function bindImpl(
|
|
|
139
147
|
|
|
140
148
|
const refreshSubscription = () => {
|
|
141
149
|
if (isDynamicPath) {
|
|
142
|
-
const resolution = resolveDynamicPath(component, path);
|
|
150
|
+
const resolution = resolveDynamicPath(component, path, pathOptions);
|
|
143
151
|
if (!resolution.ready) {
|
|
144
152
|
subscribeToPath(null);
|
|
145
153
|
return;
|
|
@@ -201,18 +209,18 @@ function bindImpl(
|
|
|
201
209
|
*/
|
|
202
210
|
export function bind(
|
|
203
211
|
path: string,
|
|
204
|
-
options?:
|
|
212
|
+
options?: BindOptions,
|
|
205
213
|
): (target: unknown, propertyKey: string) => void;
|
|
206
214
|
export function bind<T, U = any>(
|
|
207
215
|
key: DataProviderKey<T, U>,
|
|
208
|
-
options?:
|
|
216
|
+
options?: BindOptions,
|
|
209
217
|
): <K extends string>(
|
|
210
218
|
target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
|
|
211
219
|
propertyKey: K,
|
|
212
220
|
) => void;
|
|
213
221
|
export function bind(
|
|
214
222
|
pathOrKey: string | DataProviderKey<unknown, unknown>,
|
|
215
|
-
options?:
|
|
223
|
+
options?: BindOptions,
|
|
216
224
|
): (target: unknown, propertyKey: string) => void {
|
|
217
225
|
const path = hasPath(pathOrKey) ? pathOrKey.path : pathOrKey;
|
|
218
226
|
return bindImpl(path, options);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveDynamicPath } from "./dynamicPath";
|
|
3
|
+
|
|
4
|
+
describe("resolveDynamicPath", () => {
|
|
5
|
+
const host = { sessionId: "alpha", count: 0, label: "", missing: null as null };
|
|
6
|
+
|
|
7
|
+
it("est prêt avec une valeur définie", () => {
|
|
8
|
+
expect(resolveDynamicPath(host, "api/sessions/${sessionId}/sync")).toEqual({
|
|
9
|
+
ready: true,
|
|
10
|
+
path: "api/sessions/alpha/sync",
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("n'est pas prêt pour undefined/null", () => {
|
|
15
|
+
expect(resolveDynamicPath({}, "api/sessions/${sessionId}/sync")).toEqual({
|
|
16
|
+
ready: false,
|
|
17
|
+
path: null,
|
|
18
|
+
});
|
|
19
|
+
expect(
|
|
20
|
+
resolveDynamicPath({ sessionId: null }, "api/sessions/${sessionId}/sync"),
|
|
21
|
+
).toEqual({ ready: false, path: null });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("insère 0 et false par défaut", () => {
|
|
25
|
+
expect(resolveDynamicPath(host, "items/${count}")).toEqual({
|
|
26
|
+
ready: true,
|
|
27
|
+
path: "items/0",
|
|
28
|
+
});
|
|
29
|
+
expect(
|
|
30
|
+
resolveDynamicPath({ flag: false }, "flags/${flag}"),
|
|
31
|
+
).toEqual({ ready: true, path: "flags/false" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("accepte une chaîne vide par défaut", () => {
|
|
35
|
+
expect(resolveDynamicPath(host, "api/sessions/${label}/sync")).toEqual({
|
|
36
|
+
ready: true,
|
|
37
|
+
path: "api/sessions//sync",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("skipEmptyPlaceholder: true — chaîne vide non prête", () => {
|
|
42
|
+
expect(
|
|
43
|
+
resolveDynamicPath(host, "api/sessions/${label}/sync", {
|
|
44
|
+
skipEmptyPlaceholder: true,
|
|
45
|
+
}),
|
|
46
|
+
).toEqual({ ready: false, path: null });
|
|
47
|
+
expect(
|
|
48
|
+
resolveDynamicPath(host, "api/sessions/${sessionId}/sync", {
|
|
49
|
+
skipEmptyPlaceholder: true,
|
|
50
|
+
}),
|
|
51
|
+
).toEqual({ ready: true, path: "api/sessions/alpha/sync" });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** Lit / décorateurs : chemins publisher avec `${prop}` ou `{$prop}`. */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Options de résolution des placeholders dynamiques.
|
|
5
|
+
* `skipEmptyPlaceholder` ne concerne que les chaînes vides `''` — pas `0`, `false`, `null`, etc.
|
|
6
|
+
*/
|
|
7
|
+
export type DynamicPathOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Si `true`, un placeholder résolu en `''` est traité comme non prêt (`ready: false`).
|
|
10
|
+
* N'affecte pas les autres valeurs (`0` → `"0"`, `false` → `"false"`, …).
|
|
11
|
+
*/
|
|
12
|
+
skipEmptyPlaceholder?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
3
15
|
export function cleanPlaceholder(value: string): string {
|
|
4
16
|
return value.trim().replace(/^this\./, "");
|
|
5
17
|
}
|
|
@@ -25,15 +37,25 @@ export function getValueFromExpression(
|
|
|
25
37
|
return current;
|
|
26
38
|
}
|
|
27
39
|
|
|
40
|
+
function isPlaceholderUnresolved(
|
|
41
|
+
resolved: unknown,
|
|
42
|
+
options?: DynamicPathOptions,
|
|
43
|
+
): boolean {
|
|
44
|
+
if (resolved === undefined || resolved === null) return true;
|
|
45
|
+
if (options?.skipEmptyPlaceholder && resolved === "") return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
export function resolveDynamicPath(
|
|
29
50
|
component: unknown,
|
|
30
51
|
template: string,
|
|
52
|
+
options?: DynamicPathOptions,
|
|
31
53
|
): { ready: boolean; path: string | null } {
|
|
32
54
|
let missing = false;
|
|
33
55
|
const replaceValue = (_match: string, expression: string) => {
|
|
34
56
|
const cleaned = cleanPlaceholder(expression);
|
|
35
57
|
const resolved = getValueFromExpression(component, cleaned);
|
|
36
|
-
if (resolved
|
|
58
|
+
if (isPlaceholderUnresolved(resolved, options)) {
|
|
37
59
|
missing = true;
|
|
38
60
|
return "";
|
|
39
61
|
}
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
DataProviderKey,
|
|
3
3
|
DataProviderKeyHost,
|
|
4
4
|
} from "../../utils/dataProviderKey";
|
|
5
|
+
import type { DynamicPathOptions } from "./dynamicPath";
|
|
5
6
|
import { createOnAssign, Skip } from "./onAssign";
|
|
6
7
|
|
|
7
8
|
export { Skip } from "./onAssign";
|
|
@@ -14,7 +15,7 @@ export { Skip } from "./onAssign";
|
|
|
14
15
|
* assignation**, même quand la valeur reçue est `null`/`undefined` (c'est la
|
|
15
16
|
* différence de comportement voulue par rapport à `@onAssign`).
|
|
16
17
|
*/
|
|
17
|
-
export type HandleOptions = {
|
|
18
|
+
export type HandleOptions = DynamicPathOptions & {
|
|
18
19
|
/**
|
|
19
20
|
* Attendre que **toutes** les clés surveillées soient définies (non
|
|
20
21
|
* `null`/`undefined`) avant d'appeler la méthode. Reproduit la sémantique
|
|
@@ -122,6 +123,7 @@ export function handle(
|
|
|
122
123
|
{
|
|
123
124
|
dispatchWhenUndefined: !options.waitForAllDefined,
|
|
124
125
|
skip: options.skip,
|
|
126
|
+
skipEmptyPlaceholder: options.skipEmptyPlaceholder,
|
|
125
127
|
},
|
|
126
128
|
paths,
|
|
127
129
|
);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import DataProvider from "../../utils/PublisherProxy";
|
|
2
2
|
import { ConnectedComponent, setSubscribable } from "./common";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type DynamicPathOptions,
|
|
5
|
+
extractDynamicDependencies,
|
|
6
|
+
resolveDynamicPath,
|
|
7
|
+
} from "./dynamicPath";
|
|
4
8
|
import {
|
|
5
9
|
onAssignDynamicWatchKeys,
|
|
6
10
|
observeDynamicProperty,
|
|
@@ -60,7 +64,7 @@ export function isSkipped(value: unknown, kinds: Skip[]): boolean {
|
|
|
60
64
|
return kinds.some((kind) => SKIP_PREDICATES[kind](value));
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
export type OnAssignOptions = {
|
|
67
|
+
export type OnAssignOptions = DynamicPathOptions & {
|
|
64
68
|
/**
|
|
65
69
|
* Quand `true`, le callback est invoqué à chaque assignation, même si la
|
|
66
70
|
* valeur reçue est `null`/`undefined`.
|
|
@@ -125,6 +129,9 @@ export function createOnAssign(
|
|
|
125
129
|
isDynamic: dynamicDependencies.length > 0,
|
|
126
130
|
};
|
|
127
131
|
});
|
|
132
|
+
const pathOptions: DynamicPathOptions = {
|
|
133
|
+
skipEmptyPlaceholder: options.skipEmptyPlaceholder,
|
|
134
|
+
};
|
|
128
135
|
|
|
129
136
|
return function (
|
|
130
137
|
target: unknown,
|
|
@@ -215,6 +222,7 @@ export function createOnAssign(
|
|
|
215
222
|
const resolution = resolveDynamicPath(
|
|
216
223
|
component,
|
|
217
224
|
conf.pathConfig.originalPath,
|
|
225
|
+
pathOptions,
|
|
218
226
|
);
|
|
219
227
|
if (!resolution.ready) {
|
|
220
228
|
subscribeToPath(conf, null);
|
|
@@ -4,7 +4,13 @@ import type {
|
|
|
4
4
|
} from "../../utils/dataProviderKey";
|
|
5
5
|
import DataProvider from "../../utils/PublisherProxy";
|
|
6
6
|
import { ConnectedComponent, setSubscribable } from "./common";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type DynamicPathOptions,
|
|
9
|
+
extractDynamicDependencies,
|
|
10
|
+
resolveDynamicPath,
|
|
11
|
+
} from "./dynamicPath";
|
|
12
|
+
|
|
13
|
+
export type PublishOptions = DynamicPathOptions;
|
|
8
14
|
import {
|
|
9
15
|
publishDynamicWatchKeys,
|
|
10
16
|
observeDynamicProperty,
|
|
@@ -29,11 +35,15 @@ import { getPublisherFromPath } from "./publisherPath";
|
|
|
29
35
|
*/
|
|
30
36
|
export function publish<T, U = any>(
|
|
31
37
|
key: DataProviderKey<T, U>,
|
|
38
|
+
options?: PublishOptions,
|
|
32
39
|
): <K extends string>(
|
|
33
40
|
target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
|
|
34
41
|
propertyKey: K,
|
|
35
42
|
) => void {
|
|
36
43
|
const path = key.path;
|
|
44
|
+
const pathOptions: DynamicPathOptions = {
|
|
45
|
+
skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
|
|
46
|
+
};
|
|
37
47
|
const dynamicDependencies = extractDynamicDependencies(path);
|
|
38
48
|
|
|
39
49
|
return function (target: object, propertyKey: string) {
|
|
@@ -93,7 +103,7 @@ export function publish<T, U = any>(
|
|
|
93
103
|
const updatePublisher = () => {
|
|
94
104
|
let resolvedPath: string | null;
|
|
95
105
|
if (dynamicDependencies.length) {
|
|
96
|
-
const resolution = resolveDynamicPath(component, path);
|
|
106
|
+
const resolution = resolveDynamicPath(component, path, pathOptions);
|
|
97
107
|
resolvedPath = resolution.ready ? resolution.path : null;
|
|
98
108
|
} else {
|
|
99
109
|
resolvedPath = path;
|
|
@@ -28,6 +28,24 @@ type PublisherProxyOptions = {
|
|
|
28
28
|
invalidateOnPageShow?: boolean;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
/** Options de `dp()` / `dataProvider()` — même surface que `PublisherManager.get()`. */
|
|
32
|
+
export type DpOptions = PublisherProxyOptions;
|
|
33
|
+
|
|
34
|
+
const DP_OPTION_KEYS = new Set<keyof DpOptions>([
|
|
35
|
+
"localStorageMode",
|
|
36
|
+
"expirationDelayMs",
|
|
37
|
+
"invalidateOnPageShow",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function isDpOptions(value: unknown): value is DpOptions {
|
|
41
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return Object.keys(value).some((key) =>
|
|
45
|
+
DP_OPTION_KEYS.has(key as keyof DpOptions),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
function isLeaf(value: any) {
|
|
32
50
|
return Object.prototype.hasOwnProperty.call(value, "__value");
|
|
33
51
|
}
|
|
@@ -1041,6 +1059,7 @@ try {
|
|
|
1041
1059
|
|
|
1042
1060
|
export const getObservables = <T = any>(
|
|
1043
1061
|
observable: string,
|
|
1062
|
+
options?: DpOptions,
|
|
1044
1063
|
): Set<DataProvider<T>> => {
|
|
1045
1064
|
if (typeof observable === "function") {
|
|
1046
1065
|
const func = observable as () => any;
|
|
@@ -1051,7 +1070,7 @@ export const getObservables = <T = any>(
|
|
|
1051
1070
|
if (typeof observable === "string") {
|
|
1052
1071
|
const split = observable.split(".");
|
|
1053
1072
|
const dataProvider: string = split.shift() || "";
|
|
1054
|
-
let publisher = PublisherManager.get(dataProvider);
|
|
1073
|
+
let publisher = PublisherManager.get(dataProvider, options);
|
|
1055
1074
|
publisher = Objects.traverse(publisher, split);
|
|
1056
1075
|
const set = new Set<DataProvider<T>>();
|
|
1057
1076
|
set.add(publisher as DataProvider<T>);
|
|
@@ -1068,9 +1087,14 @@ export function get<T>(id: string | DataProviderKey<T>): T {
|
|
|
1068
1087
|
return getObservables<T>(path).values().next().value?.get() as T;
|
|
1069
1088
|
}
|
|
1070
1089
|
|
|
1071
|
-
function deepee<T>(
|
|
1090
|
+
function deepee<T>(
|
|
1091
|
+
id: string | DataProviderKey<T>,
|
|
1092
|
+
_defaultValue?: T,
|
|
1093
|
+
options?: DpOptions,
|
|
1094
|
+
) {
|
|
1072
1095
|
const path = resolveStaticPublisherPath(id);
|
|
1073
|
-
const value = getObservables<T>(path).values().next()
|
|
1096
|
+
const value = getObservables<T>(path, options).values().next()
|
|
1097
|
+
.value as DataProvider<T>;
|
|
1074
1098
|
// if (defaultValue !== undefined && value) {
|
|
1075
1099
|
// const innerValue = value.get();
|
|
1076
1100
|
// if (Objects.isEmpty(innerValue as Record<string, any>)) {
|
|
@@ -1081,19 +1105,79 @@ function deepee<T>(id: string | DataProviderKey<T>, _defaultValue?: T) {
|
|
|
1081
1105
|
return value;
|
|
1082
1106
|
}
|
|
1083
1107
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1108
|
+
type MirrorDpSource = DataProvider<any> | string | DataProviderKey<any>;
|
|
1109
|
+
|
|
1110
|
+
function isDataProvider(value: unknown): value is DataProvider<any> {
|
|
1111
|
+
return (
|
|
1112
|
+
value != null &&
|
|
1113
|
+
typeof value === "object" &&
|
|
1114
|
+
typeof (value as DataProvider<any>).get === "function" &&
|
|
1115
|
+
typeof (value as DataProvider<any>).set === "function" &&
|
|
1116
|
+
typeof (value as DataProvider<any>).onAssign === "function"
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function resolvePublisherRef(
|
|
1121
|
+
ref: MirrorDpSource,
|
|
1122
|
+
options?: DpOptions,
|
|
1123
|
+
): DataProvider<any> {
|
|
1124
|
+
if (isDataProvider(ref)) return ref;
|
|
1125
|
+
return deepee(ref, undefined, options);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export function dataProvider<T>(
|
|
1129
|
+
id: DataProviderKey<T>,
|
|
1130
|
+
options: DpOptions,
|
|
1131
|
+
): DataProvider<T>;
|
|
1132
|
+
export function dataProvider<T = any>(id: string, options: DpOptions): DataProvider<T>;
|
|
1133
|
+
export function dataProvider<T>(id: DataProviderKey<T>): DataProvider<T>;
|
|
1134
|
+
export function dataProvider<T = any>(id: string): DataProvider<T>;
|
|
1135
|
+
export function dataProvider<T>(id: DataProviderKey<T>, defaultValue: T): DataProvider<T>;
|
|
1136
|
+
export function dataProvider<T = any>(id: string, defaultValue: T): DataProvider<T>;
|
|
1086
1137
|
export function dataProvider<T>(
|
|
1087
1138
|
id: string | DataProviderKey<T>,
|
|
1088
|
-
|
|
1139
|
+
second?: T | DpOptions,
|
|
1089
1140
|
): DataProvider<T> {
|
|
1090
|
-
return deepee(id,
|
|
1141
|
+
if (isDpOptions(second)) return deepee(id, undefined, second);
|
|
1142
|
+
return deepee(id, second as T | undefined);
|
|
1091
1143
|
}
|
|
1092
1144
|
|
|
1093
|
-
export function dp<T>(id: DataProviderKey<T>,
|
|
1094
|
-
export function dp<T = any>(id: string,
|
|
1095
|
-
export function dp<T>(id:
|
|
1096
|
-
|
|
1145
|
+
export function dp<T>(id: DataProviderKey<T>, options: DpOptions): DataProvider<T>;
|
|
1146
|
+
export function dp<T = any>(id: string, options: DpOptions): DataProvider<T>;
|
|
1147
|
+
export function dp<T>(id: DataProviderKey<T>): DataProvider<T>;
|
|
1148
|
+
export function dp<T = any>(id: string): DataProvider<T>;
|
|
1149
|
+
export function dp<T>(id: DataProviderKey<T>, defaultValue: T): DataProvider<T>;
|
|
1150
|
+
export function dp<T = any>(id: string, defaultValue: T): DataProvider<T>;
|
|
1151
|
+
export function dp<T>(
|
|
1152
|
+
id: string | DataProviderKey<T>,
|
|
1153
|
+
second?: T | DpOptions,
|
|
1154
|
+
): DataProvider<T> {
|
|
1155
|
+
if (isDpOptions(second)) return deepee(id, undefined, second);
|
|
1156
|
+
return deepee(id, second as T | undefined);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Alias sémantique pour `PublisherManager.set` : enregistre `aliasKey` sur la même
|
|
1161
|
+
* instance publisher que `source`. Utile pour exposer une clé legacy (`scan`) tout en
|
|
1162
|
+
* stockant l'état sous une racine (`app.scan`) — `dp("scan")` et `appState.scan` partagent
|
|
1163
|
+
* alors la même instance, sans copie à chaque assignation.
|
|
1164
|
+
*/
|
|
1165
|
+
export function mirrorDp<T>(
|
|
1166
|
+
aliasKey: string | DataProviderKey<any>,
|
|
1167
|
+
source: DataProvider<T>,
|
|
1168
|
+
): void;
|
|
1169
|
+
export function mirrorDp(
|
|
1170
|
+
aliasKey: string | DataProviderKey<any>,
|
|
1171
|
+
source: string | DataProviderKey<any>,
|
|
1172
|
+
): void;
|
|
1173
|
+
export function mirrorDp(
|
|
1174
|
+
aliasKey: string | DataProviderKey<any>,
|
|
1175
|
+
source: DataProvider<any> | string | DataProviderKey<any>,
|
|
1176
|
+
): void {
|
|
1177
|
+
PublisherManager.getInstance().set(
|
|
1178
|
+
resolveStaticPublisherPath(aliasKey),
|
|
1179
|
+
resolvePublisherRef(source),
|
|
1180
|
+
);
|
|
1097
1181
|
}
|
|
1098
1182
|
|
|
1099
1183
|
export function set<T>(id: DataProviderKey<T>, value: T): void;
|
package/src/core/utils/api.ts
CHANGED
|
@@ -37,8 +37,8 @@ export type APIResponse = {
|
|
|
37
37
|
processed: ResultTypeInterface;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
/** Valeur assignée par `@get
|
|
41
|
-
export type
|
|
40
|
+
/** Valeur assignée par `@get`, `@post`, `@put`, `@patch`. */
|
|
41
|
+
export type ApiResult<T> = {
|
|
42
42
|
request: Request;
|
|
43
43
|
/** Absent / `undefined` pour les chemins `dataProvider(...)` (pas d’appel réseau). */
|
|
44
44
|
response?: Response;
|
|
@@ -46,6 +46,9 @@ export type ApiGetResult<T> = {
|
|
|
46
46
|
result: T;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/** @deprecated Utiliser `ApiResult`. */
|
|
50
|
+
export type ApiGetResult<T> = ApiResult<T>;
|
|
51
|
+
|
|
49
52
|
/** Extrait le corps typé depuis le résultat traité par `handleResult`. */
|
|
50
53
|
export function extractTypedApiResult<T>(
|
|
51
54
|
processed: ResultTypeInterface | null | undefined,
|
|
@@ -434,7 +437,7 @@ class API {
|
|
|
434
437
|
async getDetailed<T>(
|
|
435
438
|
path: string,
|
|
436
439
|
additionalHeaders?: HeadersInit,
|
|
437
|
-
): Promise<
|
|
440
|
+
): Promise<ApiResult<T> | undefined> {
|
|
438
441
|
const isDataProvider = /dataProvider\((.*?)\)(.*?)$/.test(path);
|
|
439
442
|
|
|
440
443
|
const processed = await this.get<T>(path, additionalHeaders);
|
|
@@ -468,6 +471,72 @@ class API {
|
|
|
468
471
|
};
|
|
469
472
|
}
|
|
470
473
|
|
|
474
|
+
/**
|
|
475
|
+
* S’appuie sur `post()` puis reformate la réponse : `Request` avec body JSON,
|
|
476
|
+
* `Response` via `lastResult`, `result` typé `T`.
|
|
477
|
+
*/
|
|
478
|
+
async sendDetailed<T, B>(
|
|
479
|
+
path: string,
|
|
480
|
+
body: B,
|
|
481
|
+
method: "POST" | "PUT" | "PATCH",
|
|
482
|
+
additionalHeaders?: HeadersInit,
|
|
483
|
+
): Promise<ApiResult<T> | undefined> {
|
|
484
|
+
const processed = await this.send<T, B>(
|
|
485
|
+
path,
|
|
486
|
+
body,
|
|
487
|
+
method,
|
|
488
|
+
additionalHeaders,
|
|
489
|
+
);
|
|
490
|
+
if (processed == null) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const result = extractTypedApiResult<T>(processed as ResultTypeInterface);
|
|
495
|
+
const url = this.computeURL(path);
|
|
496
|
+
const headers = await this.createHeaders({
|
|
497
|
+
Accept: "application/json",
|
|
498
|
+
"Content-Type": "application/json",
|
|
499
|
+
...(additionalHeaders as Record<string, string> | undefined),
|
|
500
|
+
});
|
|
501
|
+
const request = new Request(url, {
|
|
502
|
+
method,
|
|
503
|
+
headers: new Headers(headers as HeadersInit),
|
|
504
|
+
credentials: this.credentials,
|
|
505
|
+
body: JSON.stringify(body),
|
|
506
|
+
keepalive: this.keepAlive,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
request,
|
|
511
|
+
response: this.lastResult,
|
|
512
|
+
result,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async postDetailed<T, B>(
|
|
517
|
+
path: string,
|
|
518
|
+
body: B,
|
|
519
|
+
additionalHeaders?: HeadersInit,
|
|
520
|
+
): Promise<ApiResult<T> | undefined> {
|
|
521
|
+
return this.sendDetailed<T, B>(path, body, "POST", additionalHeaders);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async putDetailed<T, B>(
|
|
525
|
+
path: string,
|
|
526
|
+
body: B,
|
|
527
|
+
additionalHeaders?: HeadersInit,
|
|
528
|
+
): Promise<ApiResult<T> | undefined> {
|
|
529
|
+
return this.sendDetailed<T, B>(path, body, "PUT", additionalHeaders);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async patchDetailed<T, B>(
|
|
533
|
+
path: string,
|
|
534
|
+
body: B,
|
|
535
|
+
additionalHeaders?: HeadersInit,
|
|
536
|
+
): Promise<ApiResult<T> | undefined> {
|
|
537
|
+
return this.sendDetailed<T, B>(path, body, "PATCH", additionalHeaders);
|
|
538
|
+
}
|
|
539
|
+
|
|
471
540
|
/**
|
|
472
541
|
* Création du header, avec authentification si besoin
|
|
473
542
|
* ajout du language via le header accept-language qui contient le langue du navigateur
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { DataProviderKey } from "./dataProviderKey";
|
|
3
|
+
import { dp, get, mirrorDp } from "./PublisherProxy";
|
|
4
|
+
|
|
5
|
+
describe("dp() options", () => {
|
|
6
|
+
const storageKey = new DataProviderKey<{ label: string }>(
|
|
7
|
+
"dpOptionsSpecStorage",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
it("active le localStorage via le 2e argument options", () => {
|
|
11
|
+
const publisher = dp(storageKey, { localStorageMode: "enabled" });
|
|
12
|
+
expect((publisher as any)._is_savable_).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("reste désactivé sans option", () => {
|
|
16
|
+
const plainKey = new DataProviderKey<{ label: string }>(
|
|
17
|
+
"dpOptionsSpecPlain",
|
|
18
|
+
);
|
|
19
|
+
const publisher = dp(plainKey);
|
|
20
|
+
expect((publisher as any)._is_savable_).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("mirrorDp()", () => {
|
|
25
|
+
it("alias une clé legacy vers un publisher déjà résolu (appState)", () => {
|
|
26
|
+
type App = { scan: { code: string } };
|
|
27
|
+
const appKey = new DataProviderKey<App>("mirrorDpSpecApp");
|
|
28
|
+
const scanKey = new DataProviderKey<{ code: string }>("mirrorDpSpecScan");
|
|
29
|
+
|
|
30
|
+
const appState = dp(appKey);
|
|
31
|
+
appState.scan.code.set("ABC");
|
|
32
|
+
|
|
33
|
+
mirrorDp(scanKey, appState.scan);
|
|
34
|
+
|
|
35
|
+
expect(get(scanKey)).toEqual({ code: "ABC" });
|
|
36
|
+
dp(scanKey).code.set("XYZ");
|
|
37
|
+
expect(appState.scan.code.get()).toBe("XYZ");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepte des chemins string", () => {
|
|
41
|
+
type App = { legacyApi: { total: number } };
|
|
42
|
+
const appKey = new DataProviderKey<App>("mirrorDpSpecAppString");
|
|
43
|
+
const legacyKey = new DataProviderKey<{ total: number }>(
|
|
44
|
+
"mirrorDpSpecLegacyApi",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const appState = dp(appKey);
|
|
48
|
+
appState.legacyApi.total.set(12);
|
|
49
|
+
|
|
50
|
+
mirrorDp(legacyKey.path, `${appKey.path}.legacyApi`);
|
|
51
|
+
|
|
52
|
+
expect(get(legacyKey)).toEqual({ total: 12 });
|
|
53
|
+
dp(legacyKey).total.set(99);
|
|
54
|
+
expect(appState.legacyApi.total.get()).toBe(99);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ApiResult } from "./api";
|
|
2
2
|
import { DataProviderKey } from "./dataProviderKey";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -25,8 +25,8 @@ export class Endpoint<T, U = any> {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Même path qu’`Endpoint` ; le 2ᵉ générique `U` est propagé sur la clé publisher. */
|
|
28
|
-
getDataProviderKey(): DataProviderKey<
|
|
29
|
-
return new DataProviderKey<
|
|
28
|
+
getDataProviderKey(): DataProviderKey<ApiResult<T>, U> {
|
|
29
|
+
return new DataProviderKey<ApiResult<T>, U>(this.path);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
package/src/decorators.ts
CHANGED
|
@@ -16,12 +16,25 @@ export const ancestorAttribute = mySubscriber.ancestorAttribute;
|
|
|
16
16
|
export const autoSubscribe = mySubscriber.autoSubscribe;
|
|
17
17
|
export const autoFill = mySubscriber.autoFill;
|
|
18
18
|
export const get = api.get;
|
|
19
|
+
export const post = api.post;
|
|
20
|
+
export const put = api.put;
|
|
21
|
+
export const patch = api.patch;
|
|
19
22
|
export {
|
|
20
23
|
DataProviderKey,
|
|
21
24
|
type DataProviderKeyHost,
|
|
22
25
|
} from "./core/utils/dataProviderKey";
|
|
23
26
|
export { Endpoint };
|
|
24
|
-
export type { ApiGetResult } from "./core/utils/api";
|
|
27
|
+
export type { ApiResult, ApiGetResult } from "./core/utils/api";
|
|
28
|
+
export type {
|
|
29
|
+
ApiSendOptions,
|
|
30
|
+
GetOptions,
|
|
31
|
+
PostOptions,
|
|
32
|
+
PutOptions,
|
|
33
|
+
PatchOptions,
|
|
34
|
+
} from "./core/decorators/api";
|
|
35
|
+
export type { DynamicPathOptions } from "./core/decorators/subscriber/dynamicPath";
|
|
36
|
+
export type { BindOptions } from "./core/decorators/subscriber/bind";
|
|
37
|
+
export type { PublishOptions } from "./core/decorators/subscriber/publish";
|
|
25
38
|
export const awaitConnectedAncestors = lifecycle.awaitConnectedAncestors;
|
|
26
39
|
export const dispatchConnectedEvent = lifecycle.dispatchConnectedEvent;
|
|
27
40
|
export const CONNECTED = lifecycle.CONNECTED;
|
|
@@ -41,4 +54,7 @@ window["concorde-decorator-subscriber"] = {
|
|
|
41
54
|
autoSubscribe: mySubscriber.autoSubscribe,
|
|
42
55
|
autoFill: mySubscriber.autoFill,
|
|
43
56
|
get: api.get,
|
|
57
|
+
post: api.post,
|
|
58
|
+
put: api.put,
|
|
59
|
+
patch: api.patch,
|
|
44
60
|
};
|
|
@@ -13,6 +13,8 @@ Recommended patterns for new Concorde apps (Lit + TypeScript). Under the hood, d
|
|
|
13
13
|
| Write from component state | `@publish` |
|
|
14
14
|
| React to assignments | `@handle` |
|
|
15
15
|
| HTTP GET | `@get` + `Endpoint`, or `sonic-list` / `sonic-queue` with `fetch` |
|
|
16
|
+
| HTTP POST (body from store) | `@post` + `Endpoint` + body `DataProviderKey` |
|
|
17
|
+
| HTTP PUT / PATCH (body from store) | `@put` / `@patch` — same model as `@post` |
|
|
16
18
|
| Forms | `formDataProvider` + `name` on fields |
|
|
17
19
|
| Offline doc demos | `serviceURL="/docs-mock-api"` — [Local API demos](#docs/_misc/docs-mock-api.md/docs-mock-api) |
|
|
18
20
|
|
|
@@ -33,7 +35,7 @@ get(cartKey);
|
|
|
33
35
|
</template>
|
|
34
36
|
</sonic-code>
|
|
35
37
|
|
|
36
|
-
Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.${id}")` in imperative code.
|
|
38
|
+
Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.${id}")` in imperative code. Resolution rules: [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path).
|
|
37
39
|
|
|
38
40
|
[DataProviderKey](#docs/_misc/dataProviderKey.md/dataProviderKey)
|
|
39
41
|
|
|
@@ -45,13 +47,17 @@ Dynamic paths (`users.${userId}`) → decorators or `sub()` — not `get("users.
|
|
|
45
47
|
| `@publish` | Push property writes to store |
|
|
46
48
|
| `@handle` | Method called on assignment |
|
|
47
49
|
| `@ancestorAttribute` | Copy ancestor HTML attribute onto property |
|
|
48
|
-
| `@get` | HTTP GET into `
|
|
50
|
+
| `@get` | HTTP GET into `ApiResult<T>` |
|
|
51
|
+
| `@post` | HTTP POST into `ApiResult<T>` (body from a publisher) |
|
|
52
|
+
| `@put` / `@patch` | HTTP PUT / PATCH into `ApiResult<T>` |
|
|
49
53
|
|
|
50
54
|
Walkthrough: [My first component](#docs/_getting-started/my-first-component.md/my-first-component)
|
|
51
55
|
|
|
52
56
|
## HTTP and lists
|
|
53
57
|
|
|
54
|
-
- [@get](#docs/_decorators/get.md/get) — single
|
|
58
|
+
- [@get](#docs/_decorators/get.md/get) — single GET on a component
|
|
59
|
+
- [@post](#docs/_decorators/post.md/post) — POST with body read from a `DataProviderKey`
|
|
60
|
+
- [@put](#docs/_decorators/put.md/put) · [@patch](#docs/_decorators/patch.md/patch) — PUT / PATCH (same options as `@post`)
|
|
55
61
|
- [List](#core/components/functional/list/list.md/list) — `fetch` + `key="data"` + `/docs-mock-api/api/users`
|
|
56
62
|
- [Queue](#core/components/functional/queue/queue.md/queue) — lazy `offset=$offset&per_page=$limit` + optional `dataFilterProvider` (form → query)
|
|
57
63
|
|
|
@@ -4,7 +4,7 @@ Binds a class property to a path in a publisher. The property updates when publi
|
|
|
4
4
|
|
|
5
5
|
For Lit re-renders, also add `@state()` on the same property.
|
|
6
6
|
|
|
7
|
-
**See also:** [@subscribe](#docs/_decorators/subscribe.md/subscribe), [@handle](#docs/_decorators/handle.md/handle), [@publish](#docs/_decorators/publish.md/publish), [@get](#docs/_decorators/get.md/get).
|
|
7
|
+
**See also:** [@subscribe](#docs/_decorators/subscribe.md/subscribe), [@handle](#docs/_decorators/handle.md/handle), [@publish](#docs/_decorators/publish.md/publish), [@get](#docs/_decorators/get.md/get), [@post](#docs/_decorators/post.md/post), [@put](#docs/_decorators/put.md/put), [@patch](#docs/_decorators/patch.md/patch).
|
|
8
8
|
|
|
9
9
|
## Principle
|
|
10
10
|
|
|
@@ -144,7 +144,7 @@ export class DemoBindReflect extends LitElement {
|
|
|
144
144
|
|
|
145
145
|
### Dynamic paths
|
|
146
146
|
|
|
147
|
-
Use `${prop}` or `${this.prop}` inside a **normal string literal** (not a JS template literal with backticks). `@bind` re-subscribes when a reactive dependency changes.
|
|
147
|
+
Use `${prop}` or `${this.prop}` inside a **normal string literal** (not a JS template literal with backticks). `@bind` re-subscribes when a reactive dependency changes. While a placeholder is `null`/`undefined`, the bind is inactive; optional `{ skipEmptyPlaceholder: true }` also waits on `""`. See [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path).
|
|
148
148
|
|
|
149
149
|
> Properties referenced in the pattern must be reactive (`@property`, etc.) or you must call `requestUpdate` manually.
|
|
150
150
|
|