@wcstack/state 1.10.4 → 1.11.1

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/dist/index.esm.js CHANGED
@@ -59,7 +59,7 @@ function setConfig(partialConfig) {
59
59
  }
60
60
  }
61
61
 
62
- var version$1 = "1.10.4";
62
+ var version$1 = "1.11.1";
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,119 @@ 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
+ }
1695
+
1696
+ const _cache$3 = new WeakMap();
1697
+ const _cacheNullListIndex$1 = new WeakMap();
1698
+ class StateAddress {
1699
+ pathInfo;
1700
+ listIndex;
1701
+ _parentAddress;
1702
+ constructor(pathInfo, listIndex) {
1703
+ this.pathInfo = pathInfo;
1704
+ this.listIndex = listIndex;
1705
+ }
1706
+ get parentAddress() {
1707
+ if (typeof this._parentAddress !== 'undefined') {
1708
+ return this._parentAddress;
1709
+ }
1710
+ const parentPathInfo = this.pathInfo.parentPathInfo;
1711
+ if (parentPathInfo === null) {
1712
+ return null;
1713
+ }
1714
+ const lastSegment = this.pathInfo.segments[this.pathInfo.segments.length - 1];
1715
+ let parentListIndex = null;
1716
+ if (lastSegment === WILDCARD) {
1717
+ parentListIndex = this.listIndex?.parentListIndex ?? null;
1718
+ }
1719
+ else {
1720
+ parentListIndex = this.listIndex;
1721
+ }
1722
+ return this._parentAddress = createStateAddress(parentPathInfo, parentListIndex);
1723
+ }
1724
+ }
1725
+ function createStateAddress(pathInfo, listIndex) {
1726
+ if (listIndex === null) {
1727
+ let cached = _cacheNullListIndex$1.get(pathInfo);
1728
+ if (typeof cached !== "undefined") {
1729
+ return cached;
1730
+ }
1731
+ cached = new StateAddress(pathInfo, null);
1732
+ _cacheNullListIndex$1.set(pathInfo, cached);
1733
+ return cached;
1734
+ }
1735
+ else {
1736
+ let cacheByPathInfo = _cache$3.get(listIndex);
1737
+ if (typeof cacheByPathInfo === "undefined") {
1738
+ cacheByPathInfo = new WeakMap();
1739
+ _cache$3.set(listIndex, cacheByPathInfo);
1740
+ }
1741
+ let cached = cacheByPathInfo.get(pathInfo);
1742
+ if (typeof cached !== "undefined") {
1743
+ return cached;
1744
+ }
1745
+ cached = new StateAddress(pathInfo, listIndex);
1746
+ cacheByPathInfo.set(pathInfo, cached);
1747
+ return cached;
1748
+ }
1749
+ }
1750
+
1751
+ // _subscribers は Set のため挿入順を保持する。
1752
+ // emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
1753
+ class CommandToken {
1754
+ _name;
1755
+ _subscribers = new Set();
1756
+ constructor(name) {
1757
+ this._name = name;
1758
+ }
1759
+ get name() {
1760
+ return this._name;
1761
+ }
1762
+ get size() {
1763
+ return this._subscribers.size;
1764
+ }
1765
+ subscribe(fn) {
1766
+ this._subscribers.add(fn);
1767
+ return () => {
1768
+ this._subscribers.delete(fn);
1769
+ };
1770
+ }
1771
+ unsubscribe(fn) {
1772
+ return this._subscribers.delete(fn);
1773
+ }
1774
+ emit(...args) {
1775
+ const results = [];
1776
+ for (const fn of this._subscribers) {
1777
+ results.push(fn(...args));
1778
+ }
1779
+ return results;
1780
+ }
1781
+ }
1782
+ function isCommandToken(value) {
1783
+ return value instanceof CommandToken;
1784
+ }
1467
1785
 
1468
1786
  const loopContextByNode = new WeakMap();
1469
1787
  function getLoopContextByNode(node) {
@@ -1493,13 +1811,18 @@ const connectedCallbackSymbol = Symbol("$$connectedCallback");
1493
1811
  const disconnectedCallbackSymbol = Symbol("$$disconnectedCallback");
1494
1812
  const updatedCallbackSymbol = Symbol("$$updatedCallback");
1495
1813
 
1814
+ // onclick: $command.<name> のように、DOM イベントから command token を直接 emit する形式かを判定する。
1815
+ // 右辺が $command 名前空間配下のパス($command.<token>)のときに true。
1816
+ function isCommandTokenPath(statePathName) {
1817
+ return statePathName.startsWith(STATE_COMMAND_NAMESPACE_NAME + ".");
1818
+ }
1496
1819
  const handlerByHandlerKey$3 = new Map();
1497
1820
  const bindingSetByHandlerKey$3 = new Map();
1498
1821
  function getHandlerKey$3(binding) {
1499
1822
  const modifierKey = binding.propModifiers.filter(m => m === 'prevent' || m === 'stop').sort().join(',');
1500
1823
  return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
1501
1824
  }
1502
- const stateEventHandlerFunction = (stateName, handlerName, modifiers) => (event) => {
1825
+ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathInfo) => (event) => {
1503
1826
  if (modifiers.includes('prevent'))
1504
1827
  event.preventDefault();
1505
1828
  if (modifiers.includes('stop'))
@@ -1511,13 +1834,23 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers) => (event)
1511
1834
  raiseError(`State element with name "${stateName}" not found for event handler.`);
1512
1835
  }
1513
1836
  const loopContext = getLoopContextByNode(node);
1837
+ const isCommand = isCommandTokenPath(handlerName);
1514
1838
  stateElement.createStateAsync("writable", async (state) => {
1515
1839
  state[setLoopContextSymbol](loopContext, () => {
1840
+ const indexes = loopContext?.listIndex.indexes ?? [];
1841
+ if (isCommand) {
1842
+ // command token を解決して emit。引数はハンドラ呼び出しと同じく (event, ...listIndexes) を透過する。
1843
+ const token = state[getByAddressSymbol](createStateAddress(statePathInfo, null));
1844
+ if (!isCommandToken(token)) {
1845
+ raiseError(`Event binding "${handlerName}" did not resolve to a CommandToken. Declare the name in $commandTokens and reference it as $command.<name>.`);
1846
+ }
1847
+ return token.emit(event, ...indexes);
1848
+ }
1516
1849
  const handler = state[handlerName];
1517
1850
  if (typeof handler !== "function") {
1518
1851
  raiseError(`Handler "${handlerName}" is not a function on state "${stateName}".`);
1519
1852
  }
1520
- return Reflect.apply(handler, state, [event, ...(loopContext?.listIndex.indexes ?? [])]);
1853
+ return Reflect.apply(handler, state, [event, ...indexes]);
1521
1854
  });
1522
1855
  });
1523
1856
  };
@@ -1528,7 +1861,7 @@ function attachEventHandler(binding) {
1528
1861
  const key = getHandlerKey$3(binding);
1529
1862
  let stateEventHandler = handlerByHandlerKey$3.get(key);
1530
1863
  if (typeof stateEventHandler === "undefined") {
1531
- stateEventHandler = stateEventHandlerFunction(binding.stateName, binding.statePathName, binding.propModifiers);
1864
+ stateEventHandler = stateEventHandlerFunction(binding.stateName, binding.statePathName, binding.propModifiers, binding.statePathInfo);
1532
1865
  handlerByHandlerKey$3.set(key, stateEventHandler);
1533
1866
  }
1534
1867
  const eventName = binding.propName.slice(2);
@@ -1544,35 +1877,6 @@ function attachEventHandler(binding) {
1544
1877
  return true;
1545
1878
  }
1546
1879
 
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
1880
  const CHECK_TYPES = new Set(['radio', 'checkbox']);
1577
1881
  const DEFAULT_VALUE_PROP_NAMES = new Set(['value', 'valueAsNumber', 'valueAsDate']);
1578
1882
  function isPossibleTwoWay(node, propName) {
@@ -1738,19 +2042,19 @@ function setLastListValueByAbsoluteStateAddress(address, value) {
1738
2042
  lastListValueByAbsoluteStateAddress.set(address, value);
1739
2043
  }
1740
2044
 
1741
- const _cache$3 = new WeakMap();
2045
+ const _cache$2 = new WeakMap();
1742
2046
  function getAbsolutePathInfo(stateElement, pathInfo) {
1743
- if (_cache$3.has(stateElement)) {
1744
- const pathMap = _cache$3.get(stateElement);
2047
+ if (_cache$2.has(stateElement)) {
2048
+ const pathMap = _cache$2.get(stateElement);
1745
2049
  if (pathMap.has(pathInfo)) {
1746
2050
  return pathMap.get(pathInfo);
1747
2051
  }
1748
2052
  }
1749
2053
  else {
1750
- _cache$3.set(stateElement, new WeakMap());
2054
+ _cache$2.set(stateElement, new WeakMap());
1751
2055
  }
1752
2056
  const absolutePathInfo = Object.freeze(new AbsolutePathInfo(stateElement, pathInfo));
1753
- _cache$3.get(stateElement).set(pathInfo, absolutePathInfo);
2057
+ _cache$2.get(stateElement).set(pathInfo, absolutePathInfo);
1754
2058
  return absolutePathInfo;
1755
2059
  }
1756
2060
  class AbsolutePathInfo {
@@ -1771,8 +2075,8 @@ class AbsolutePathInfo {
1771
2075
  }
1772
2076
  }
1773
2077
 
1774
- const _cache$2 = new WeakMap();
1775
- const _cacheNullListIndex$1 = new WeakMap();
2078
+ const _cache$1 = new WeakMap();
2079
+ const _cacheNullListIndex = new WeakMap();
1776
2080
  class AbsoluteStateAddress {
1777
2081
  absolutePathInfo;
1778
2082
  listIndex;
@@ -1802,19 +2106,19 @@ class AbsoluteStateAddress {
1802
2106
  }
1803
2107
  function createAbsoluteStateAddress(absolutePathInfo, listIndex) {
1804
2108
  if (listIndex === null) {
1805
- let cached = _cacheNullListIndex$1.get(absolutePathInfo);
2109
+ let cached = _cacheNullListIndex.get(absolutePathInfo);
1806
2110
  if (typeof cached !== "undefined") {
1807
2111
  return cached;
1808
2112
  }
1809
2113
  cached = new AbsoluteStateAddress(absolutePathInfo, null);
1810
- _cacheNullListIndex$1.set(absolutePathInfo, cached);
2114
+ _cacheNullListIndex.set(absolutePathInfo, cached);
1811
2115
  return cached;
1812
2116
  }
1813
2117
  else {
1814
- let cacheByAbsolutePathInfo = _cache$2.get(listIndex);
2118
+ let cacheByAbsolutePathInfo = _cache$1.get(listIndex);
1815
2119
  if (typeof cacheByAbsolutePathInfo === "undefined") {
1816
2120
  cacheByAbsolutePathInfo = new WeakMap();
1817
- _cache$2.set(listIndex, cacheByAbsolutePathInfo);
2121
+ _cache$1.set(listIndex, cacheByAbsolutePathInfo);
1818
2122
  }
1819
2123
  let cached = cacheByAbsolutePathInfo.get(absolutePathInfo);
1820
2124
  if (typeof cached !== "undefined") {
@@ -1990,41 +2294,6 @@ function applyChangeToClass(binding, _context, newValue) {
1990
2294
  element.classList.toggle(className, newValue);
1991
2295
  }
1992
2296
 
1993
- // _subscribers は Set のため挿入順を保持する。
1994
- // emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
1995
- class CommandToken {
1996
- _name;
1997
- _subscribers = new Set();
1998
- constructor(name) {
1999
- this._name = name;
2000
- }
2001
- get name() {
2002
- return this._name;
2003
- }
2004
- get size() {
2005
- return this._subscribers.size;
2006
- }
2007
- subscribe(fn) {
2008
- this._subscribers.add(fn);
2009
- return () => {
2010
- this._subscribers.delete(fn);
2011
- };
2012
- }
2013
- unsubscribe(fn) {
2014
- return this._subscribers.delete(fn);
2015
- }
2016
- emit(...args) {
2017
- const results = [];
2018
- for (const fn of this._subscribers) {
2019
- results.push(fn(...args));
2020
- }
2021
- return results;
2022
- }
2023
- }
2024
- function isCommandToken(value) {
2025
- return value instanceof CommandToken;
2026
- }
2027
-
2028
2297
  /**
2029
2298
  * command.<methodName>: <commandToken-path> バインディングの適用ハンドラ。
2030
2299
  *
@@ -2107,61 +2376,6 @@ function applyChangeToCommand(binding, _context, newValue) {
2107
2376
  subscribedBindings.set(binding, { token, unsubscribe, elementRef });
2108
2377
  }
2109
2378
 
2110
- const _cache$1 = new WeakMap();
2111
- const _cacheNullListIndex = new WeakMap();
2112
- class StateAddress {
2113
- pathInfo;
2114
- listIndex;
2115
- _parentAddress;
2116
- constructor(pathInfo, listIndex) {
2117
- this.pathInfo = pathInfo;
2118
- this.listIndex = listIndex;
2119
- }
2120
- get parentAddress() {
2121
- if (typeof this._parentAddress !== 'undefined') {
2122
- return this._parentAddress;
2123
- }
2124
- const parentPathInfo = this.pathInfo.parentPathInfo;
2125
- if (parentPathInfo === null) {
2126
- return null;
2127
- }
2128
- const lastSegment = this.pathInfo.segments[this.pathInfo.segments.length - 1];
2129
- let parentListIndex = null;
2130
- if (lastSegment === WILDCARD) {
2131
- parentListIndex = this.listIndex?.parentListIndex ?? null;
2132
- }
2133
- else {
2134
- parentListIndex = this.listIndex;
2135
- }
2136
- return this._parentAddress = createStateAddress(parentPathInfo, parentListIndex);
2137
- }
2138
- }
2139
- function createStateAddress(pathInfo, listIndex) {
2140
- if (listIndex === null) {
2141
- let cached = _cacheNullListIndex.get(pathInfo);
2142
- if (typeof cached !== "undefined") {
2143
- return cached;
2144
- }
2145
- cached = new StateAddress(pathInfo, null);
2146
- _cacheNullListIndex.set(pathInfo, cached);
2147
- return cached;
2148
- }
2149
- else {
2150
- let cacheByPathInfo = _cache$1.get(listIndex);
2151
- if (typeof cacheByPathInfo === "undefined") {
2152
- cacheByPathInfo = new WeakMap();
2153
- _cache$1.set(listIndex, cacheByPathInfo);
2154
- }
2155
- let cached = cacheByPathInfo.get(pathInfo);
2156
- if (typeof cached !== "undefined") {
2157
- return cached;
2158
- }
2159
- cached = new StateAddress(pathInfo, listIndex);
2160
- cacheByPathInfo.set(pathInfo, cached);
2161
- return cached;
2162
- }
2163
- }
2164
-
2165
2379
  const indexBindingsByContent = new WeakMap();
2166
2380
  function getIndexBindingsByContent(content) {
2167
2381
  return indexBindingsByContent.get(content) ?? [];
@@ -3665,13 +3879,7 @@ function _initializeBindings(allBindings) {
3665
3879
  attachCheckboxEventHandler(binding);
3666
3880
  }
3667
3881
  }
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
3882
+ function _registerAbsoluteAddresses(allBindings) {
3675
3883
  for (const binding of allBindings) {
3676
3884
  const absoluteStateAddress = getAbsoluteStateAddressByBinding(binding);
3677
3885
  addBindingByAbsoluteStateAddress(absoluteStateAddress, binding);
@@ -3684,8 +3892,34 @@ function initializeBindings(root, parentLoopContext) {
3684
3892
  stateElement.setPathInfo(binding.statePathName, binding.bindingType);
3685
3893
  }
3686
3894
  }
3895
+ }
3896
+ function _scheduleDeferredSpreads(deferredSpreads, parentLoopContext) {
3897
+ for (const entry of deferredSpreads) {
3898
+ customElements.whenDefined(entry.tagName).then(() => {
3899
+ if (!entry.node.isConnected)
3900
+ return; // node was removed before class became ready
3901
+ const bindings = processDeferredNode(entry);
3902
+ if (bindings.length === 0)
3903
+ return;
3904
+ setLoopContextByNode(entry.node, parentLoopContext);
3905
+ _initializeBindings(bindings);
3906
+ _registerAbsoluteAddresses(bindings);
3907
+ applyChangeFromBindings(bindings);
3908
+ }).catch((error) => {
3909
+ console.error(`[@wcstack/state] deferred spread failed for <${entry.tagName}>.`, error);
3910
+ });
3911
+ }
3912
+ }
3913
+ function initializeBindings(root, parentLoopContext) {
3914
+ const [subscriberNodes, allBindings, deferredSpreads] = collectNodesAndBindingInfos(root);
3915
+ for (const node of subscriberNodes) {
3916
+ setLoopContextByNode(node, parentLoopContext);
3917
+ }
3918
+ _initializeBindings(allBindings);
3919
+ _registerAbsoluteAddresses(allBindings);
3687
3920
  // apply all at once
3688
3921
  applyChangeFromBindings(allBindings);
3922
+ _scheduleDeferredSpreads(deferredSpreads, parentLoopContext);
3689
3923
  }
3690
3924
  function initializeBindingsByFragment(root, nodeInfos) {
3691
3925
  const [subscriberNodes, allBindings] = collectNodesAndBindingInfosByFragment(root, nodeInfos);