@wcstack/state 1.10.4 → 1.11.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.ja.md +49 -0
- package/README.md +49 -0
- package/dist/index.esm.js +324 -105
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -380,6 +380,55 @@ property[#modifier]: path[@state][|filter[|filter(args)...]]
|
|
|
380
380
|
|
|
381
381
|
内部的にはコメントベースのバインディング(`<!--@@:expression-->`)に変換されます。
|
|
382
382
|
|
|
383
|
+
### Spread バインディング (`...`)
|
|
384
|
+
|
|
385
|
+
`wc-bindable` プロトコルを宣言したカスタム要素に対して、`...: target` を使うと要素の **properties + inputs を 1 行で一括配線**できます:
|
|
386
|
+
|
|
387
|
+
```html
|
|
388
|
+
<wcs-fetch data-wcs="...: usersFetch"></wcs-fetch>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
export default {
|
|
393
|
+
usersFetch: {
|
|
394
|
+
url: "/api/users",
|
|
395
|
+
method: "GET",
|
|
396
|
+
value: null,
|
|
397
|
+
loading: false,
|
|
398
|
+
error: null,
|
|
399
|
+
status: null,
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
ランタイムが `customClass.wcBindable.properties + inputs` を読み取り、各 name を個別バインディング(`usersFetch.value`、`usersFetch.url`、...)に展開します。
|
|
405
|
+
|
|
406
|
+
**対象範囲**:spread は *データサーフェス*(properties + inputs)のみを対象とします。`commands` や event token は **対象外** — pub/sub の発火点が HTML から読めるように、明示配線を維持してください。
|
|
407
|
+
|
|
408
|
+
**for ループ内**:`...: items.*`(推奨)または dot ショートカット `...: .` を使います:
|
|
409
|
+
|
|
410
|
+
```html
|
|
411
|
+
<template data-wcs="for: storesFetches">
|
|
412
|
+
<wcs-fetch data-wcs="...: storesFetches.*"></wcs-fetch>
|
|
413
|
+
</template>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**後勝ち上書き** — spread の後ろに同名 prop を書くと、明示側が優先されます:
|
|
417
|
+
|
|
418
|
+
```html
|
|
419
|
+
<wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**制約事項**:
|
|
423
|
+
|
|
424
|
+
- spread 右辺へのフィルタ(`...: target|filter`)はエラー
|
|
425
|
+
- 右辺パスの途中に `*` を含めても OK(例:`...: stores.*.fetch`)
|
|
426
|
+
- `@stateName` 修飾子は各展開エントリへ伝播(`...: fetchX@store`)
|
|
427
|
+
- カスタム要素クラスが未登録の場合、`customElements.whenDefined(tag)` 解決時に遅延展開される(autoloader による遅延ロードに対応)
|
|
428
|
+
- `wcBindable` 宣言**のない**要素はエラー(明示配線で書いてください)。spread は何を展開すべきかを契約から読み取るため
|
|
429
|
+
|
|
430
|
+
**Composite shell**(wc-bindable Composition Profile)はそのままサポートされます:composite shell は標準の `target.constructor.wcBindable` を通じて synthesized declaration を露出するため、`"s3.progress"` のような composed name はフラットな要素メンバーキーとして扱われます。state を composed 構造に合わせて (`{ s3: { progress: 0 } }`) 持てば、`...: pipeline` が自動的に nested state path へ展開されます。
|
|
431
|
+
|
|
383
432
|
## 構造ディレクティブ
|
|
384
433
|
|
|
385
434
|
構造ディレクティブは `<template>` 要素で使用します:
|
package/README.md
CHANGED
|
@@ -381,6 +381,55 @@ When `enableMustache` is `true` (default), `{{ expression }}` in text nodes is s
|
|
|
381
381
|
|
|
382
382
|
Internally converted to comment-based bindings (`<!--@@:expression-->`).
|
|
383
383
|
|
|
384
|
+
### Spread Binding (`...`)
|
|
385
|
+
|
|
386
|
+
For custom elements that declare the [`wc-bindable` protocol](#wcbindable-protocol), `...: target` wires all of the element's **properties + inputs** to a single state object in one line:
|
|
387
|
+
|
|
388
|
+
```html
|
|
389
|
+
<wcs-fetch data-wcs="...: usersFetch"></wcs-fetch>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
export default {
|
|
394
|
+
usersFetch: {
|
|
395
|
+
url: "/api/users",
|
|
396
|
+
method: "GET",
|
|
397
|
+
value: null,
|
|
398
|
+
loading: false,
|
|
399
|
+
error: null,
|
|
400
|
+
status: null,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Runtime reads `customClass.wcBindable.properties + inputs` and expands each name into an individual binding (`usersFetch.value`, `usersFetch.url`, ...).
|
|
406
|
+
|
|
407
|
+
**Scope**: spread covers the *data surfaces* (properties + inputs). `commands` and event tokens are intentionally **not** included — wire them explicitly so the pub/sub points remain visible in HTML.
|
|
408
|
+
|
|
409
|
+
**Inside a for loop**: use `...: items.*` (recommended) or the dot shortcut `...: .`:
|
|
410
|
+
|
|
411
|
+
```html
|
|
412
|
+
<template data-wcs="for: storesFetches">
|
|
413
|
+
<wcs-fetch data-wcs="...: storesFetches.*"></wcs-fetch>
|
|
414
|
+
</template>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Last-wins override** — explicit binding after `...` overrides the spread:
|
|
418
|
+
|
|
419
|
+
```html
|
|
420
|
+
<wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Constraints**:
|
|
424
|
+
|
|
425
|
+
- Filters on the spread target (`...: target|filter`) are rejected.
|
|
426
|
+
- The right-hand path may contain `*` anywhere (e.g. `...: stores.*.fetch`).
|
|
427
|
+
- `@stateName` propagates to every expanded entry (`...: fetchX@store`).
|
|
428
|
+
- If the custom element class is not yet registered, expansion is deferred until `customElements.whenDefined(tag)` resolves — autoloader-style late registration is supported.
|
|
429
|
+
- Elements **without** a `wcBindable` declaration are rejected (write bindings explicitly). Spread requires the contract to know what to expand.
|
|
430
|
+
|
|
431
|
+
**Composite shells** (wc-bindable Composition Profile) are supported transparently: a composite shell exposes its synthesized declaration through the standard `target.constructor.wcBindable` surface, and composed names like `"s3.progress"` are kept as flat element member keys. Mirror the composed structure in state (`{ s3: { progress: 0 } }`) and `...: pipeline` expands into nested state paths automatically.
|
|
432
|
+
|
|
384
433
|
## Structural Directives
|
|
385
434
|
|
|
386
435
|
Structural directives use `<template>` elements:
|
package/dist/index.esm.js
CHANGED
|
@@ -59,7 +59,7 @@ function setConfig(partialConfig) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
var version$1 = "1.
|
|
62
|
+
var version$1 = "1.11.0";
|
|
63
63
|
var pkg = {
|
|
64
64
|
version: version$1};
|
|
65
65
|
|
|
@@ -110,58 +110,6 @@ function replaceToReplaceNode(bindingInfo) {
|
|
|
110
110
|
node.parentNode.replaceChild(replaceNode, node);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function resolveNodePath(root, path) {
|
|
114
|
-
let currentNode = root;
|
|
115
|
-
if (path.length === 0)
|
|
116
|
-
return currentNode;
|
|
117
|
-
// path.reduce()だと途中でnullになる可能性があるので、
|
|
118
|
-
for (let i = 0; i < path.length; i++) {
|
|
119
|
-
currentNode = currentNode?.childNodes[path[i]] ?? null;
|
|
120
|
-
if (currentNode === null)
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
return currentNode;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getBindingInfos(node, parseBindingTextResults) {
|
|
127
|
-
const bindingInfos = [];
|
|
128
|
-
for (const parseBindingTextResult of parseBindingTextResults) {
|
|
129
|
-
if (parseBindingTextResult.bindingType !== 'text') {
|
|
130
|
-
bindingInfos.push({
|
|
131
|
-
...parseBindingTextResult,
|
|
132
|
-
node: node,
|
|
133
|
-
replaceNode: node,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
const replaceNode = document.createTextNode('');
|
|
138
|
-
bindingInfos.push({
|
|
139
|
-
...parseBindingTextResult,
|
|
140
|
-
node: node,
|
|
141
|
-
replaceNode: replaceNode,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return bindingInfos;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const bindingsByNode = new WeakMap();
|
|
149
|
-
function getBindingsByNode(node) {
|
|
150
|
-
return bindingsByNode.get(node) || null;
|
|
151
|
-
}
|
|
152
|
-
function setBindingsByNode(node, bindings) {
|
|
153
|
-
bindingsByNode.set(node, bindings);
|
|
154
|
-
}
|
|
155
|
-
function addBindingByNode(node, binding) {
|
|
156
|
-
const bindings = getBindingsByNode(node);
|
|
157
|
-
if (bindings === null) {
|
|
158
|
-
setBindingsByNode(node, [binding]);
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
bindings.push(binding);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
113
|
const DELIMITER = '.';
|
|
166
114
|
const WILDCARD = '*';
|
|
167
115
|
const MAX_WILDCARD_DEPTH = 128;
|
|
@@ -294,6 +242,237 @@ class PathInfo {
|
|
|
294
242
|
}
|
|
295
243
|
}
|
|
296
244
|
|
|
245
|
+
const cache = new WeakMap();
|
|
246
|
+
function getCustomElement(node) {
|
|
247
|
+
const cached = cache.get(node);
|
|
248
|
+
if (cached !== undefined) {
|
|
249
|
+
return cached;
|
|
250
|
+
}
|
|
251
|
+
let value = null;
|
|
252
|
+
try {
|
|
253
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
const element = node;
|
|
257
|
+
const tagName = element.tagName.toLowerCase();
|
|
258
|
+
if (tagName.includes("-")) {
|
|
259
|
+
return value = tagName;
|
|
260
|
+
}
|
|
261
|
+
if (element.hasAttribute("is")) {
|
|
262
|
+
const is = element.getAttribute("is");
|
|
263
|
+
if (is.includes("-")) {
|
|
264
|
+
return value = is;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
cache.set(node, value);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isValidWcBindable(value) {
|
|
275
|
+
if (typeof value !== "object" || value === null)
|
|
276
|
+
return false;
|
|
277
|
+
const v = value;
|
|
278
|
+
return v.protocol === "wc-bindable"
|
|
279
|
+
&& v.version === 1
|
|
280
|
+
&& Array.isArray(v.properties);
|
|
281
|
+
}
|
|
282
|
+
function makeExpandedEntry(name, base, stateName) {
|
|
283
|
+
// Dot-relative spread keeps the loop item root (`.`) without producing `..foo`.
|
|
284
|
+
const expandedPath = base === "." ? `.${name}` : `${base}.${name}`;
|
|
285
|
+
return {
|
|
286
|
+
propName: name,
|
|
287
|
+
propSegments: [name],
|
|
288
|
+
propModifiers: [],
|
|
289
|
+
statePathName: expandedPath,
|
|
290
|
+
statePathInfo: getPathInfo(expandedPath),
|
|
291
|
+
stateName,
|
|
292
|
+
inFilters: [],
|
|
293
|
+
outFilters: [],
|
|
294
|
+
bindingType: 'prop',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function dedupKey(r) {
|
|
298
|
+
switch (r.bindingType) {
|
|
299
|
+
case 'prop':
|
|
300
|
+
case 'event':
|
|
301
|
+
case 'radio':
|
|
302
|
+
case 'checkbox':
|
|
303
|
+
return `${r.bindingType}::${r.propName}`;
|
|
304
|
+
case 'spread':
|
|
305
|
+
return null;
|
|
306
|
+
default:
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Expand spread bind-text entries (`...: target`) into per-prop entries
|
|
312
|
+
* by enumerating wcBindable.properties + inputs of the element's class.
|
|
313
|
+
*
|
|
314
|
+
* Behavior:
|
|
315
|
+
* - With `allowDeferred: true` (default): if class is not yet defined, the
|
|
316
|
+
* spread entry stays so the caller can wait via customElements.whenDefined.
|
|
317
|
+
* - With `allowDeferred: false`: raises if class is not defined.
|
|
318
|
+
* - Duplicate propName: last-wins (explicit binding overrides spread).
|
|
319
|
+
* - When config.debug, console.debug logs each override.
|
|
320
|
+
* - Mid-`*` in target path is allowed (e.g. `...: stores.*.fetch`).
|
|
321
|
+
*
|
|
322
|
+
* Composite Profile (COMPOSITE.md / SPEC-extensions § 4) support:
|
|
323
|
+
* - A composite shell exposes its synthesized declaration via the standard
|
|
324
|
+
* `target.constructor.wcBindable` surface (§ 1 Discovery), so this function
|
|
325
|
+
* handles it without any composite-specific code path.
|
|
326
|
+
* - Composed property names use the `<sourceId>.<sourceName>` pattern
|
|
327
|
+
* (e.g. "s3.progress"); we keep the dotted name as a single segment so
|
|
328
|
+
* element member access stays flat (element["s3.progress"], not nested).
|
|
329
|
+
* - The expanded state path becomes `targetBase.s3.progress`, which resolves
|
|
330
|
+
* as nested state access — author state as `{ s3: { progress: 0 } }` to
|
|
331
|
+
* mirror the composed structure.
|
|
332
|
+
* - Tier claim (Symbol.for("wc-bindable.composite.tiers")) is not read here;
|
|
333
|
+
* spread covers observation (T1) and writable inputs (T2) transparently
|
|
334
|
+
* through normal property assignment, and commands stay out of spread by
|
|
335
|
+
* design regardless of tier.
|
|
336
|
+
*/
|
|
337
|
+
function expandSpread(node, results, options = {}) {
|
|
338
|
+
const allowDeferred = options.allowDeferred ?? true;
|
|
339
|
+
if (!results.some(r => r.bindingType === 'spread')) {
|
|
340
|
+
return results;
|
|
341
|
+
}
|
|
342
|
+
const expanded = [];
|
|
343
|
+
const spreadOrigin = new WeakSet();
|
|
344
|
+
for (const result of results) {
|
|
345
|
+
if (result.bindingType !== 'spread') {
|
|
346
|
+
expanded.push(result);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
350
|
+
raiseError(`Spread binding requires an element node.`);
|
|
351
|
+
}
|
|
352
|
+
const element = node;
|
|
353
|
+
const tagName = getCustomElement(element);
|
|
354
|
+
if (tagName === null) {
|
|
355
|
+
raiseError(`Spread binding "${result.statePathName}" requires a custom element with wcBindable, but <${element.tagName.toLowerCase()}> is not a custom element.`);
|
|
356
|
+
}
|
|
357
|
+
const customClass = customElements.get(tagName);
|
|
358
|
+
if (typeof customClass === "undefined") {
|
|
359
|
+
if (!allowDeferred) {
|
|
360
|
+
raiseError(`Spread binding "${result.statePathName}" requires <${tagName}> to be registered. Define the custom element before initializing this binding.`);
|
|
361
|
+
}
|
|
362
|
+
// Deferred: keep spread entry intact; caller retries via whenDefined.
|
|
363
|
+
expanded.push(result);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const bindable = customClass.wcBindable;
|
|
367
|
+
if (!isValidWcBindable(bindable)) {
|
|
368
|
+
raiseError(`Spread binding "${result.statePathName}" requires <${tagName}> to declare wcBindable (protocol="wc-bindable", version=1).`);
|
|
369
|
+
}
|
|
370
|
+
const targetBase = result.statePathName;
|
|
371
|
+
const stateName = result.stateName;
|
|
372
|
+
const seen = new Set();
|
|
373
|
+
for (const prop of bindable.properties) {
|
|
374
|
+
if (seen.has(prop.name))
|
|
375
|
+
continue;
|
|
376
|
+
seen.add(prop.name);
|
|
377
|
+
const entry = makeExpandedEntry(prop.name, targetBase, stateName);
|
|
378
|
+
spreadOrigin.add(entry);
|
|
379
|
+
expanded.push(entry);
|
|
380
|
+
}
|
|
381
|
+
// properties win over inputs when the name overlaps because they carry the
|
|
382
|
+
// full property contract (for example change events).
|
|
383
|
+
for (const input of (bindable.inputs ?? [])) {
|
|
384
|
+
if (seen.has(input.name))
|
|
385
|
+
continue;
|
|
386
|
+
seen.add(input.name);
|
|
387
|
+
const entry = makeExpandedEntry(input.name, targetBase, stateName);
|
|
388
|
+
spreadOrigin.add(entry);
|
|
389
|
+
expanded.push(entry);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Last-wins de-duplication
|
|
393
|
+
const lastIndexByKey = new Map();
|
|
394
|
+
for (let i = 0; i < expanded.length; i++) {
|
|
395
|
+
const key = dedupKey(expanded[i]);
|
|
396
|
+
if (key !== null) {
|
|
397
|
+
lastIndexByKey.set(key, i);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const final = [];
|
|
401
|
+
for (let i = 0; i < expanded.length; i++) {
|
|
402
|
+
const key = dedupKey(expanded[i]);
|
|
403
|
+
if (key === null) {
|
|
404
|
+
final.push(expanded[i]);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (lastIndexByKey.get(key) === i) {
|
|
408
|
+
final.push(expanded[i]);
|
|
409
|
+
}
|
|
410
|
+
else if (config.debug && spreadOrigin.has(expanded[i])) {
|
|
411
|
+
const overrider = expanded[lastIndexByKey.get(key)];
|
|
412
|
+
const tagText = node.nodeType === Node.ELEMENT_NODE
|
|
413
|
+
? `<${node.tagName.toLowerCase()}>`
|
|
414
|
+
: 'node';
|
|
415
|
+
console.debug(`[@wcstack/state] spread: prop "${expanded[i].propName}" of ${tagText} overridden by explicit binding (statePath: "${overrider.statePathName}").`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return final;
|
|
419
|
+
}
|
|
420
|
+
function hasUnresolvedSpread(results) {
|
|
421
|
+
return results.some(r => r.bindingType === 'spread');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function resolveNodePath(root, path) {
|
|
425
|
+
let currentNode = root;
|
|
426
|
+
if (path.length === 0)
|
|
427
|
+
return currentNode;
|
|
428
|
+
// path.reduce()だと途中でnullになる可能性があるので、
|
|
429
|
+
for (let i = 0; i < path.length; i++) {
|
|
430
|
+
currentNode = currentNode?.childNodes[path[i]] ?? null;
|
|
431
|
+
if (currentNode === null)
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
return currentNode;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getBindingInfos(node, parseBindingTextResults) {
|
|
438
|
+
const bindingInfos = [];
|
|
439
|
+
for (const parseBindingTextResult of parseBindingTextResults) {
|
|
440
|
+
if (parseBindingTextResult.bindingType !== 'text') {
|
|
441
|
+
bindingInfos.push({
|
|
442
|
+
...parseBindingTextResult,
|
|
443
|
+
node: node,
|
|
444
|
+
replaceNode: node,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const replaceNode = document.createTextNode('');
|
|
449
|
+
bindingInfos.push({
|
|
450
|
+
...parseBindingTextResult,
|
|
451
|
+
node: node,
|
|
452
|
+
replaceNode: replaceNode,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return bindingInfos;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const bindingsByNode = new WeakMap();
|
|
460
|
+
function getBindingsByNode(node) {
|
|
461
|
+
return bindingsByNode.get(node) || null;
|
|
462
|
+
}
|
|
463
|
+
function setBindingsByNode(node, bindings) {
|
|
464
|
+
bindingsByNode.set(node, bindings);
|
|
465
|
+
}
|
|
466
|
+
function addBindingByNode(node, binding) {
|
|
467
|
+
const bindings = getBindingsByNode(node);
|
|
468
|
+
if (bindings === null) {
|
|
469
|
+
setBindingsByNode(node, [binding]);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
bindings.push(binding);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
297
476
|
const STRUCTURAL_BINDING_TYPE_SET = new Set([
|
|
298
477
|
"if",
|
|
299
478
|
"elseif",
|
|
@@ -1247,6 +1426,7 @@ function parseStatePart(statePart) {
|
|
|
1247
1426
|
// elseif: statePart only (single binding for conditional rendering)
|
|
1248
1427
|
// for: statePart only (single binding for loop rendering)
|
|
1249
1428
|
// onclick: statePart, onchange: statePart etc. (event listeners)
|
|
1429
|
+
// ...: statePart (spread — expand wcBindable properties+inputs of target object)
|
|
1250
1430
|
function parseBindTextsForElement(bindText) {
|
|
1251
1431
|
const [...bindTexts] = bindText.split(';').map(trimFn).filter(s => s.length > 0);
|
|
1252
1432
|
const results = bindTexts.map((bindText) => {
|
|
@@ -1270,6 +1450,23 @@ function parseBindTextsForElement(bindText) {
|
|
|
1270
1450
|
bindingType: 'else',
|
|
1271
1451
|
};
|
|
1272
1452
|
}
|
|
1453
|
+
else if (propPart === '...') {
|
|
1454
|
+
const stateResult = parseStatePart(statePart);
|
|
1455
|
+
if (stateResult.outFilters.length > 0) {
|
|
1456
|
+
raiseError(`Invalid spread binding "${bindText}": filters are not allowed on spread targets.`);
|
|
1457
|
+
}
|
|
1458
|
+
if (stateResult.statePathName.length === 0) {
|
|
1459
|
+
raiseError(`Invalid spread binding "${bindText}": spread target path is required.`);
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
propName: '...',
|
|
1463
|
+
propSegments: ['...'],
|
|
1464
|
+
propModifiers: [],
|
|
1465
|
+
inFilters: [],
|
|
1466
|
+
...stateResult,
|
|
1467
|
+
bindingType: 'spread',
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1273
1470
|
else if (propPart === 'if'
|
|
1274
1471
|
|| propPart === 'elseif'
|
|
1275
1472
|
|| propPart === 'for'
|
|
@@ -1430,20 +1627,39 @@ function getSubscriberNodes(root) {
|
|
|
1430
1627
|
}
|
|
1431
1628
|
|
|
1432
1629
|
const registeredNodeSet = new WeakSet();
|
|
1630
|
+
function processParseResultsForNode(node, parseResults, options) {
|
|
1631
|
+
const expanded = expandSpread(node, parseResults, { allowDeferred: options.allowDeferred });
|
|
1632
|
+
if (hasUnresolvedSpread(expanded)) {
|
|
1633
|
+
const tagName = node.nodeType === Node.ELEMENT_NODE
|
|
1634
|
+
? getCustomElement(node)
|
|
1635
|
+
: null;
|
|
1636
|
+
if (tagName === null) {
|
|
1637
|
+
raiseError(`Spread binding deferred but element is not a custom element.`);
|
|
1638
|
+
}
|
|
1639
|
+
return { bindings: [], deferred: { node, tagName, parseResults } };
|
|
1640
|
+
}
|
|
1641
|
+
registeredNodeSet.add(node);
|
|
1642
|
+
const bindings = getBindingInfos(node, expanded);
|
|
1643
|
+
setBindingsByNode(node, bindings);
|
|
1644
|
+
resolveInitializedBinding(node);
|
|
1645
|
+
return { bindings, deferred: null };
|
|
1646
|
+
}
|
|
1433
1647
|
function collectNodesAndBindingInfos(root) {
|
|
1434
1648
|
const subscriberNodes = getSubscriberNodes(root);
|
|
1435
1649
|
const allBindings = [];
|
|
1650
|
+
const deferredSpreads = [];
|
|
1436
1651
|
for (const node of subscriberNodes) {
|
|
1437
|
-
if (
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1652
|
+
if (registeredNodeSet.has(node))
|
|
1653
|
+
continue;
|
|
1654
|
+
const parseResults = getParseBindTextResults(node);
|
|
1655
|
+
const result = processParseResultsForNode(node, parseResults, { allowDeferred: true });
|
|
1656
|
+
if (result.deferred !== null) {
|
|
1657
|
+
deferredSpreads.push(result.deferred);
|
|
1658
|
+
continue;
|
|
1444
1659
|
}
|
|
1660
|
+
allBindings.push(...result.bindings);
|
|
1445
1661
|
}
|
|
1446
|
-
return [subscriberNodes, allBindings];
|
|
1662
|
+
return [subscriberNodes, allBindings, deferredSpreads];
|
|
1447
1663
|
}
|
|
1448
1664
|
function collectNodesAndBindingInfosByFragment(root, nodeInfos) {
|
|
1449
1665
|
const nodes = [];
|
|
@@ -1453,17 +1669,29 @@ function collectNodesAndBindingInfosByFragment(root, nodeInfos) {
|
|
|
1453
1669
|
if (node === null) {
|
|
1454
1670
|
raiseError(`Node not found by path [${nodeInfo.nodePath.join(', ')}] in fragment.`);
|
|
1455
1671
|
}
|
|
1456
|
-
if (
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
nodes.push(node);
|
|
1463
|
-
}
|
|
1672
|
+
if (registeredNodeSet.has(node))
|
|
1673
|
+
continue;
|
|
1674
|
+
const result = processParseResultsForNode(node, nodeInfo.parseBindTextResults, { allowDeferred: false });
|
|
1675
|
+
// deferred is impossible when allowDeferred=false (expandSpread raises instead)
|
|
1676
|
+
allBindings.push(...result.bindings);
|
|
1677
|
+
nodes.push(node);
|
|
1464
1678
|
}
|
|
1465
1679
|
return [nodes, allBindings];
|
|
1466
1680
|
}
|
|
1681
|
+
function unregisterNode(node) {
|
|
1682
|
+
registeredNodeSet.delete(node);
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Re-process a deferred spread entry once the custom element class is
|
|
1686
|
+
* registered. Expands the captured parseResults, installs bindings, and
|
|
1687
|
+
* returns them so the caller can attach handlers and apply state values.
|
|
1688
|
+
*/
|
|
1689
|
+
function processDeferredNode(entry) {
|
|
1690
|
+
const { node, parseResults } = entry;
|
|
1691
|
+
unregisterNode(node);
|
|
1692
|
+
const result = processParseResultsForNode(node, parseResults, { allowDeferred: false });
|
|
1693
|
+
return result.bindings;
|
|
1694
|
+
}
|
|
1467
1695
|
|
|
1468
1696
|
const loopContextByNode = new WeakMap();
|
|
1469
1697
|
function getLoopContextByNode(node) {
|
|
@@ -1544,35 +1772,6 @@ function attachEventHandler(binding) {
|
|
|
1544
1772
|
return true;
|
|
1545
1773
|
}
|
|
1546
1774
|
|
|
1547
|
-
const cache = new WeakMap();
|
|
1548
|
-
function getCustomElement(node) {
|
|
1549
|
-
const cached = cache.get(node);
|
|
1550
|
-
if (cached !== undefined) {
|
|
1551
|
-
return cached;
|
|
1552
|
-
}
|
|
1553
|
-
let value = null;
|
|
1554
|
-
try {
|
|
1555
|
-
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
1556
|
-
return value;
|
|
1557
|
-
}
|
|
1558
|
-
const element = node;
|
|
1559
|
-
const tagName = element.tagName.toLowerCase();
|
|
1560
|
-
if (tagName.includes("-")) {
|
|
1561
|
-
return value = tagName;
|
|
1562
|
-
}
|
|
1563
|
-
if (element.hasAttribute("is")) {
|
|
1564
|
-
const is = element.getAttribute("is");
|
|
1565
|
-
if (is.includes("-")) {
|
|
1566
|
-
return value = is;
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
return value;
|
|
1570
|
-
}
|
|
1571
|
-
finally {
|
|
1572
|
-
cache.set(node, value);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
1775
|
const CHECK_TYPES = new Set(['radio', 'checkbox']);
|
|
1577
1776
|
const DEFAULT_VALUE_PROP_NAMES = new Set(['value', 'valueAsNumber', 'valueAsDate']);
|
|
1578
1777
|
function isPossibleTwoWay(node, propName) {
|
|
@@ -3665,13 +3864,7 @@ function _initializeBindings(allBindings) {
|
|
|
3665
3864
|
attachCheckboxEventHandler(binding);
|
|
3666
3865
|
}
|
|
3667
3866
|
}
|
|
3668
|
-
function
|
|
3669
|
-
const [subscriberNodes, allBindings] = collectNodesAndBindingInfos(root);
|
|
3670
|
-
for (const node of subscriberNodes) {
|
|
3671
|
-
setLoopContextByNode(node, parentLoopContext);
|
|
3672
|
-
}
|
|
3673
|
-
_initializeBindings(allBindings);
|
|
3674
|
-
// create absolute state address and register binding infos
|
|
3867
|
+
function _registerAbsoluteAddresses(allBindings) {
|
|
3675
3868
|
for (const binding of allBindings) {
|
|
3676
3869
|
const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
|
|
3677
3870
|
addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
|
|
@@ -3684,8 +3877,34 @@ function initializeBindings(root, parentLoopContext) {
|
|
|
3684
3877
|
stateElement.setPathInfo(binding.statePathName, binding.bindingType);
|
|
3685
3878
|
}
|
|
3686
3879
|
}
|
|
3880
|
+
}
|
|
3881
|
+
function _scheduleDeferredSpreads(deferredSpreads, parentLoopContext) {
|
|
3882
|
+
for (const entry of deferredSpreads) {
|
|
3883
|
+
customElements.whenDefined(entry.tagName).then(() => {
|
|
3884
|
+
if (!entry.node.isConnected)
|
|
3885
|
+
return; // node was removed before class became ready
|
|
3886
|
+
const bindings = processDeferredNode(entry);
|
|
3887
|
+
if (bindings.length === 0)
|
|
3888
|
+
return;
|
|
3889
|
+
setLoopContextByNode(entry.node, parentLoopContext);
|
|
3890
|
+
_initializeBindings(bindings);
|
|
3891
|
+
_registerAbsoluteAddresses(bindings);
|
|
3892
|
+
applyChangeFromBindings(bindings);
|
|
3893
|
+
}).catch((error) => {
|
|
3894
|
+
console.error(`[@wcstack/state] deferred spread failed for <${entry.tagName}>.`, error);
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
function initializeBindings(root, parentLoopContext) {
|
|
3899
|
+
const [subscriberNodes, allBindings, deferredSpreads] = collectNodesAndBindingInfos(root);
|
|
3900
|
+
for (const node of subscriberNodes) {
|
|
3901
|
+
setLoopContextByNode(node, parentLoopContext);
|
|
3902
|
+
}
|
|
3903
|
+
_initializeBindings(allBindings);
|
|
3904
|
+
_registerAbsoluteAddresses(allBindings);
|
|
3687
3905
|
// apply all at once
|
|
3688
3906
|
applyChangeFromBindings(allBindings);
|
|
3907
|
+
_scheduleDeferredSpreads(deferredSpreads, parentLoopContext);
|
|
3689
3908
|
}
|
|
3690
3909
|
function initializeBindingsByFragment(root, nodeInfos) {
|
|
3691
3910
|
const [subscriberNodes, allBindings] = collectNodesAndBindingInfosByFragment(root, nodeInfos);
|