@wcstack/state 1.10.3 → 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 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.10.3";
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 (!registeredNodeSet.has(node)) {
1438
- registeredNodeSet.add(node);
1439
- const parseBindingTextResults = getParseBindTextResults(node);
1440
- const bindings = getBindingInfos(node, parseBindingTextResults);
1441
- setBindingsByNode(node, bindings);
1442
- resolveInitializedBinding(node);
1443
- allBindings.push(...bindings);
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 (!registeredNodeSet.has(node)) {
1457
- registeredNodeSet.add(node);
1458
- const bindings = getBindingInfos(node, nodeInfo.parseBindTextResults);
1459
- setBindingsByNode(node, bindings);
1460
- resolveInitializedBinding(node);
1461
- allBindings.push(...bindings);
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 initializeBindings(root, parentLoopContext) {
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);