@wcstack/state 1.3.18 → 1.4.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
@@ -2,13 +2,45 @@
2
2
 
3
3
  **もしHTMLにリアクティブなデータバインディングがあったら?**
4
4
 
5
- ブラウザが状態をネイティブに理解する未来を妄想してみる。データをインラインで宣言し、属性でDOMにバインドすれば、すべてが自動で同期する。仮想DOMもコンパイルも不要。ただのHTMLが、勝手にリアクティブになる。
5
+ ブラウザが状態をネイティブに理解する未来を想像してみてください。データをインラインで宣言し、属性でDOMにバインドするだけで、すべてが自動で同期します。仮想DOMもコンパイルも不要。ただのHTMLが、そのままリアクティブになる世界です。
6
6
 
7
- それが `<wcs-state>` と `data-wcs` の探求するもの。CDN一発、依存ゼロ、構文はHTML
7
+ それが `<wcs-state>` と `data-wcs` の目指すアプローチです。CDNからの読み込みだけで動作し、依存パッケージはゼロ、構文はHTMLそのままです。
8
8
 
9
- CDNスクリプトはカスタム要素の定義を登録するだけ — ロード時にはそれ以外何も起きない。`<wcs-state>` 要素がDOMに接続されたとき、状態ソースを読み取り、兄弟要素の `data-wcs` バインディングを走査し、リアクティビティを構築する。すべての初期化は要素のライフサイクルが駆動する。あなたのコードではなく。
9
+ CDNのスクリプトはカスタム要素の定義を登録するだけで、ロード時にはそれ以外の処理は走りません。`<wcs-state>` 要素がDOMに接続されたときにはじめて、状態ソースを読み取り、同一ルートノード(`document` または `ShadowRoot`)内の `data-wcs` バインディングを走査してリアクティビティを構築します。初期化プロセスはすべて要素のライフサイクルによって駆動されるため、独自の初期化コードを書く必要はありません。
10
10
 
11
- ## 4ステップで動く
11
+ ## 設計思想
12
+
13
+ ### パスが唯一の契約
14
+
15
+ 既存の多くのフレームワークでは、**コンポーネント**がUIと状態の結合点になっています。状態ストアを外部に切り出しても、コンポーネント内にフックやセレクタ、リアクティブプリミティブといった**状態を引き込むためのコード**が必ず必要になります。つまり、UIと状態は常にJavaScriptの中で結びついているのです。
16
+
17
+ `@wcstack/state` はこの結合を完全に排除しました。UIと状態を結びつけているのは**パス文字列**だけです — `user.name` や `cart.items.*.subtotal` のようなドット区切りのアドレスのみが、2つのレイヤー間の唯一の契約(コントラクト)になります:
18
+
19
+ | レイヤー | 知っていること | 知らないこと |
20
+ |----------|----------------|--------------|
21
+ | **状態** (`<wcs-state>`) | データ構造とビジネスロジック | どのDOM要素がバインドされているか |
22
+ | **UI** (`data-wcs`) | パス文字列と表示意図 | 状態がどう保存・算出されているか |
23
+ | **コンポーネント** (`@name`) | 名前付き状態から必要なパス | 他コンポーネントの内部実装 |
24
+
25
+ 3つのレベルのパス契約が疎結合を実現しています:
26
+
27
+ 1. **UI ↔ 状態** — `data-wcs="textContent: user.name"` という属性がバインディングのすべてです。フックもセレクタもリアクティブプリミティブもありません。コンポーネントのJavaScriptには、状態を参照するコードが**一行も**必要ありません。
28
+
29
+ 2. **コンポーネント ↔ コンポーネント** — コンポーネント間の通信は、名前付き状態の参照(`@stateName`)で行われます。コンポーネント同士がお互いを直接インポートしたり参照したりすることはありません。共有するのはパスの命名規約だけです。
30
+
31
+ 3. **ループコンテキスト** — `for` ループ内では `*` が抽象インデックスとして機能します。`items.*.price` のようなバインディングは自動的に現在の要素へと解決されます。テンプレートは自身の具体的な位置(インデックス)を知る必要がなく、ワイルドカードがその契約となります。
32
+
33
+ ### なぜこれが重要なのか
34
+
35
+ これはUIと状態の完全な分離を、**JavaScriptのコードを介することなく**実現していることを意味します。つまり:
36
+
37
+ - UIをすべて作り直しても、状態のロジックに触れる必要がありません。
38
+ - 状態のデータ構造をリファクタリングしても、パス文字列の更新だけで済みます。
39
+ - HTMLを読むだけで、すべてのデータ依存関係を把握できます。
40
+
41
+ このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
42
+
43
+ ## わずか4ステップで動作
12
44
 
13
45
  ```html
14
46
  <!-- 1. CDN を読み込む -->
@@ -28,7 +60,7 @@ CDNスクリプトはカスタム要素の定義を登録するだけ — ロー
28
60
  <div data-wcs="textContent: message"></div>
29
61
  ```
30
62
 
31
- これだけ。ビルドなし、初期化コードなし、フレームワークなし。
63
+ これだけです。ビルドツールも、初期化コードも、重いフレームワークも必要ありません。
32
64
 
33
65
  ## 特徴
34
66
 
@@ -178,11 +210,11 @@ this.count = 10; // パス "count"
178
210
  this["user.name"] = "Bob"; // パス "user.name"
179
211
  ```
180
212
 
181
- ルールはひとつ: **パスに代入すれば、DOMは自動的に更新される。**
213
+ ルールは1つだけです。**「パスに直接代入する」ことで、関連するDOMが自動的に更新されます。**
182
214
 
183
- ### なぜ `this.user.name = "Bob"` では動かないのか
215
+ ### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
184
216
 
185
- `this.user.name` は、まず `this.user` `user` オブジェクトを読み取り(パスの読み取り)、そのプレーンオブジェクトの `.name` に設定します — これはパスへの代入ではないため、変更は検知されません:
217
+ 通常のプロパティアクセスの書き方だと、まず `this.user` でプレーンな `user` オブジェクトを読み取り(パスの読み取り)、取得したオブジェクトの `.name` を直接書き換える挙動になります。これは「パスに対するプロパティ代入」というフックを経由していないため、システム側で変更を検知できません:
186
218
 
187
219
  ```javascript
188
220
  // ✅ パスへの代入 — 変更が検知される
@@ -194,7 +226,7 @@ this.user.name = "Bob";
194
226
 
195
227
  ### 配列
196
228
 
197
- 同じルールです: パスに新しい配列を代入します。破壊的メソッド(`push`, `splice`, `sort` 等)はパスへの代入なしに配列をその場で変更するため、非破壊的な代替メソッドを使用します:
229
+ 配列についても全く同じルールが適用されます。常に**パスに対して新しい配列を代入**してください。`push` `splice`、`sort` などの破壊的な配列メソッドは、パスへの代入を介さずに状態をその場で(in-placeに)書き換えてしまうため、変更が検知されません。代わりに、新しい配列を返す非破壊的なメソッドを使用します:
198
230
 
199
231
  ```javascript
200
232
  // ✅ 新しい配列をパスに代入 — 変更が検知される
@@ -862,14 +894,14 @@ export default {
862
894
 
863
895
  `@wcstack/state` は Shadow DOM または Light DOM を使用したカスタム要素との双方向状態バインディングに対応しています。
864
896
 
865
- 多くのフレームワークでは、コンポーネント間の状態共有に props バケツリレー、Context Provider、外部ストア(Redux, Pinia)といったパターンが用いられます。`@wcstack/state` は異なるアプローチを採ります。親子コンポーネントは**パスの契約**で接続されます。親は `data-wcs` で外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態を通常どおり読み書きするだけです:
897
+ 多くのフレームワークでは、コンポーネント間の状態共有に props のバケツリレー、Context Provider、あるいは外部ストア(Redux, Pinia など)といったパターンが用いられます。`@wcstack/state` はこれらとは異なるアプローチを採ります。親コンポーネントと子コンポーネントは**パスの契約**によって結びつけられます。親は `data-wcs` 属性を使って外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態として通常通り読み書きを行うだけです:
866
898
 
867
- 1. 子コンポーネントは自身の状態プロキシを通じて親の状態を参照・更新します — props もイベントも親の存在を意識する必要もありません。
899
+ 1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
868
900
  2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
869
- 3. 結合点は**パス名のみ**であるため、双方とも疎結合を保ち、独立してテスト可能です。
870
- 4. コストはパス解決(初回アクセス後はキャッシュにより O(1))と依存グラフを通じた変更伝播のみです。
901
+ 3. 結合点は**パス名のみ**であるため、親と子は完全に疎結合な状態を保ち、それぞれ独立してテスト可能です。
902
+ 4. 実行コストは、パスの解決(初回アクセス後はキャッシュされるため O(1) で動作します)と、依存グラフを通じた変更の伝播のみです。
871
903
 
872
- これは、コンポーネントレベルの抽象化ではなくパス解決に基づく、コンポーネント間状態管理への軽量なアプローチです。
904
+ これは、コンポーネントレベルの複雑な抽象化ではなく、「パスの解決」に基づいたコンポーネント間状態管理への軽量なアプローチです。
873
905
 
874
906
  ### コンポーネント定義(Shadow DOM)
875
907
 
@@ -1026,13 +1058,13 @@ customElements.define("my-component", MyComponent);
1026
1058
  | `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
1027
1059
  | `$updatedCallback(paths, indexesListByPath)` | 状態変更が適用された後に呼び出し | 戻り値は未使用(待機されない) |
1028
1060
 
1029
- リアクティブ Proxy は全てのプロパティ代入を変更として検知するため、標準の `async/await` とプロパティへの直接代入だけで非同期処理は完結します。ローディングフラグ、取得データ、エラーメッセージの更新は全てプロパティ代入であり、非同期状態管理のための追加の抽象化を必要としません。
1061
+ リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
1030
1062
 
1031
- - フック内の `this` は読み書き可能な状態プロキシです
1032
- - `$connectedCallback` は要素が接続される**たびに**呼ばれます(削除後の再接続を含む)。再確立が必要なセットアップ処理に適しています
1033
- - `$disconnectedCallback` は同期的に呼び出されます — タイマーのクリア、イベントリスナーの削除、リソースの解放などのクリーンアップに使用します
1034
- - `$updatedCallback(paths, indexesListByPath)` は更新された状態パスの一覧を受け取ります。ワイルドカードパス更新時は `indexesListByPath` で更新対象インデックスも受け取れます
1035
- - Web Component では `async $stateReadyCallback(stateProp)` を定義すると、`bind-component` でバインドされた状態が利用可能になったタイミングで呼び出されます
1063
+ - フック内の `this` は読み書き可能な状態プロキシです。
1064
+ - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
1065
+ - `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
1066
+ - `$updatedCallback(paths, indexesListByPath)` は更新された状態パスの一覧を受け取ります。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。
1067
+ - Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
1036
1068
 
1037
1069
  ## 設定
1038
1070
 
package/README.md CHANGED
@@ -6,7 +6,39 @@ Imagine a future where the browser natively understands state — you declare da
6
6
 
7
7
  That's what `<wcs-state>` and `data-wcs` explore. One CDN import, zero dependencies, pure HTML syntax.
8
8
 
9
- The CDN script only registers the custom element definition — nothing else happens at load time. When a `<wcs-state>` element connects to the DOM, it reads its state source, scans sibling elements for `data-wcs` bindings, and wires up reactivity. All initialization is driven by the element's lifecycle, not by your code.
9
+ The CDN script only registers the custom element definition — nothing else happens at load time. When a `<wcs-state>` element connects to the DOM, it reads its state source, scans all `data-wcs` bindings within the same root node (`document` or `ShadowRoot`), and wires up reactivity. All initialization is driven by the element's lifecycle, not by your code.
10
+
11
+ ## Design Philosophy
12
+
13
+ ### Path as the Universal Contract
14
+
15
+ In every existing framework, the **component** is the coupling point between UI and state. Components import state hooks, selectors, or reactive primitives, and the binding happens inside JavaScript. No matter how cleanly you separate your state store, there is always glue code in the component that pulls state in.
16
+
17
+ `@wcstack/state` eliminates that coupling entirely. The **only** thing connecting UI and state is a **path string** — a dot-separated address like `user.name` or `cart.items.*.subtotal`. This is the sole contract between the two layers:
18
+
19
+ | Layer | What it knows | What it doesn't know |
20
+ |-------|---------------|----------------------|
21
+ | **State** (`<wcs-state>`) | Data structure and business logic | Which DOM nodes are bound |
22
+ | **UI** (`data-wcs`) | Path strings and display intent | How state is stored or computed |
23
+ | **Components** (`@name`) | The path they need from a named state | The other component's internals |
24
+
25
+ Three levels of path contracts keep everything loosely coupled:
26
+
27
+ 1. **UI ↔ State** — A `data-wcs="textContent: user.name"` attribute is the entire binding. No hooks, no selectors, no reactive primitives. The component's JavaScript doesn't contain a single line that references state.
28
+
29
+ 2. **Component ↔ Component** — Cross-component communication happens through named state references (`@stateName`). Components never import or depend on each other; they share a naming convention, nothing more.
30
+
31
+ 3. **Loop context** — Inside a `for` loop, `*` acts as an abstract index. Bindings like `items.*.price` resolve to the current element automatically. The template doesn't know its concrete position — the wildcard is the contract.
32
+
33
+ ### Why This Matters
34
+
35
+ This is complete separation of UI and state with **no JavaScript intermediary**. You can:
36
+
37
+ - Redesign the entire UI without touching state logic
38
+ - Refactor state structure and only update path strings
39
+ - Read the HTML alone and understand every data dependency
40
+
41
+ The path contract works like a URL in a REST API — a simple string that both sides agree on, with no shared code between them. It's the natural result of building on HTML's declarative nature rather than inventing a template language on top of JavaScript.
10
42
 
11
43
  ## 4 Steps to Reactive HTML
12
44
 
package/dist/index.esm.js CHANGED
@@ -1121,6 +1121,7 @@ function parseFilterArgs(argsText) {
1121
1121
  const args = [];
1122
1122
  let current = '';
1123
1123
  let inQuote = null;
1124
+ let hasQuote = false;
1124
1125
  for (let i = 0; i < argsText.length; i++) {
1125
1126
  const char = argsText[i];
1126
1127
  if (inQuote) {
@@ -1133,17 +1134,20 @@ function parseFilterArgs(argsText) {
1133
1134
  }
1134
1135
  else if (char === '"' || char === "'") {
1135
1136
  inQuote = char;
1137
+ hasQuote = true;
1136
1138
  }
1137
1139
  else if (char === ',') {
1138
1140
  args.push(current.trim());
1139
1141
  current = '';
1142
+ hasQuote = false;
1140
1143
  }
1141
1144
  else {
1142
1145
  current += char;
1143
1146
  }
1144
1147
  }
1145
- if (current.trim()) {
1146
- args.push(current.trim());
1148
+ const last = current.trim();
1149
+ if (last || hasQuote) {
1150
+ args.push(last);
1147
1151
  }
1148
1152
  return args;
1149
1153
  }
@@ -1609,6 +1613,35 @@ function attachEventHandler(binding) {
1609
1613
  return true;
1610
1614
  }
1611
1615
 
1616
+ const cache = new WeakMap();
1617
+ function getCustomElement(node) {
1618
+ const cached = cache.get(node);
1619
+ if (cached !== undefined) {
1620
+ return cached;
1621
+ }
1622
+ let value = null;
1623
+ try {
1624
+ if (node.nodeType !== Node.ELEMENT_NODE) {
1625
+ return value;
1626
+ }
1627
+ const element = node;
1628
+ const tagName = element.tagName.toLowerCase();
1629
+ if (tagName.includes("-")) {
1630
+ return value = tagName;
1631
+ }
1632
+ if (element.hasAttribute("is")) {
1633
+ const is = element.getAttribute("is");
1634
+ if (is.includes("-")) {
1635
+ return value = is;
1636
+ }
1637
+ }
1638
+ return value;
1639
+ }
1640
+ finally {
1641
+ cache.set(node, value);
1642
+ }
1643
+ }
1644
+
1612
1645
  const CHECK_TYPES = new Set(['radio', 'checkbox']);
1613
1646
  const DEFAULT_VALUE_PROP_NAMES = new Set(['value', 'valueAsNumber', 'valueAsDate']);
1614
1647
  function isPossibleTwoWay(node, propName) {
@@ -1635,18 +1668,49 @@ function isPossibleTwoWay(node, propName) {
1635
1668
  if (tagName === 'textarea' && propName === 'value') {
1636
1669
  return true;
1637
1670
  }
1671
+ const customTagName = getCustomElement(element);
1672
+ if (customTagName !== null) {
1673
+ const customClass = customElements.get(customTagName);
1674
+ if (typeof customClass === "undefined") {
1675
+ raiseError(`Custom element <${customTagName}> is not defined. Cannot determine if property "${propName}" is suitable for two-way binding.`);
1676
+ }
1677
+ const bindable = customClass.wcBindable;
1678
+ if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
1679
+ if (bindable.properties.some(p => p.name === propName)) {
1680
+ return true;
1681
+ }
1682
+ }
1683
+ }
1638
1684
  return false;
1639
1685
  }
1640
1686
 
1641
1687
  const handlerByHandlerKey$2 = new Map();
1642
1688
  const bindingSetByHandlerKey$2 = new Map();
1643
- function getHandlerKey$2(binding, eventName) {
1689
+ const DEFAULT_GETTER = (e) => e.detail;
1690
+ function getHandlerKey$2(binding, eventName, hasGetter) {
1644
1691
  const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
1645
- return `${binding.stateName}::${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}`;
1692
+ return `${binding.stateName}::${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}::${hasGetter ? 'g' : 'n'}`;
1646
1693
  }
1647
1694
  function getEventName$2(binding) {
1648
1695
  const tagName = binding.node.tagName.toLowerCase();
1696
+ // 1.default event name
1649
1697
  let eventName = (tagName === 'select') ? 'change' : 'input';
1698
+ // 2.wcBindable protocol
1699
+ const customTagName = getCustomElement(binding.node);
1700
+ if (customTagName !== null) {
1701
+ const customClass = customElements.get(customTagName);
1702
+ if (typeof customClass === "undefined") {
1703
+ raiseError(`Custom element <${customTagName}> is not defined. Cannot determine event name for two-way binding.`);
1704
+ }
1705
+ const bindable = customClass.wcBindable;
1706
+ if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
1707
+ const propDesc = bindable.properties.find(p => p.name === binding.propName);
1708
+ if (propDesc) {
1709
+ eventName = propDesc.event;
1710
+ }
1711
+ }
1712
+ }
1713
+ // 3.modifier
1650
1714
  for (const modifier of binding.propModifiers) {
1651
1715
  if (modifier.startsWith('on')) {
1652
1716
  eventName = modifier.slice(2);
@@ -1654,17 +1718,39 @@ function getEventName$2(binding) {
1654
1718
  }
1655
1719
  return eventName;
1656
1720
  }
1657
- const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilters) => (event) => {
1721
+ function getValueGetter(binding) {
1722
+ const customTagName = getCustomElement(binding.node);
1723
+ if (customTagName !== null) {
1724
+ const customClass = customElements.get(customTagName);
1725
+ if (customClass) {
1726
+ const bindable = customClass.wcBindable;
1727
+ if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
1728
+ const propDesc = bindable.properties.find(p => p.name === binding.propName);
1729
+ if (propDesc) {
1730
+ return propDesc.getter ?? DEFAULT_GETTER;
1731
+ }
1732
+ }
1733
+ }
1734
+ }
1735
+ return null;
1736
+ }
1737
+ const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilters, valueGetter) => (event) => {
1658
1738
  const node = event.target;
1659
1739
  if (node === null) {
1660
1740
  console.warn(`[@wcstack/state] event.target is null.`);
1661
1741
  return;
1662
1742
  }
1663
- if (!(propName in node)) {
1664
- console.warn(`[@wcstack/state] Property "${propName}" does not exist on target element.`);
1665
- return;
1743
+ let newValue;
1744
+ if (valueGetter !== null) {
1745
+ newValue = valueGetter(event);
1746
+ }
1747
+ else {
1748
+ if (!(propName in node)) {
1749
+ console.warn(`[@wcstack/state] Property "${propName}" does not exist on target element.`);
1750
+ return;
1751
+ }
1752
+ newValue = node[propName];
1666
1753
  }
1667
- const newValue = node[propName];
1668
1754
  let filteredNewValue = newValue;
1669
1755
  for (const filter of inFilters) {
1670
1756
  filteredNewValue = filter.filterFn(filteredNewValue);
@@ -1682,12 +1768,23 @@ const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilter
1682
1768
  });
1683
1769
  };
1684
1770
  function attachTwowayEventHandler(binding) {
1771
+ const customTagName = getCustomElement(binding.node);
1772
+ if (customTagName !== null) {
1773
+ const customClass = customElements.get(customTagName);
1774
+ if (typeof customClass === "undefined") {
1775
+ customElements.whenDefined(customTagName).then(() => {
1776
+ attachTwowayEventHandler(binding);
1777
+ });
1778
+ return;
1779
+ }
1780
+ }
1685
1781
  if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
1686
1782
  const eventName = getEventName$2(binding);
1687
- const key = getHandlerKey$2(binding, eventName);
1783
+ const valueGetter = getValueGetter(binding);
1784
+ const key = getHandlerKey$2(binding, eventName, valueGetter !== null);
1688
1785
  let twowayEventHandler = handlerByHandlerKey$2.get(key);
1689
1786
  if (typeof twowayEventHandler === "undefined") {
1690
- twowayEventHandler = twowayEventHandlerFunction(binding.stateName, binding.propName, binding.statePathName, binding.inFilters);
1787
+ twowayEventHandler = twowayEventHandlerFunction(binding.stateName, binding.propName, binding.statePathName, binding.inFilters, valueGetter);
1691
1788
  handlerByHandlerKey$2.set(key, twowayEventHandler);
1692
1789
  }
1693
1790
  binding.node.addEventListener(eventName, twowayEventHandler);
@@ -1699,9 +1796,7 @@ function attachTwowayEventHandler(binding) {
1699
1796
  else {
1700
1797
  bindingSet.add(binding);
1701
1798
  }
1702
- return true;
1703
1799
  }
1704
- return false;
1705
1800
  }
1706
1801
 
1707
1802
  const lastListValueByAbsoluteStateAddress = new WeakMap();
@@ -1909,35 +2004,6 @@ function clearAbsoluteStateAddressByBinding(binding) {
1909
2004
  absoluteStateAddressByBinding.delete(binding);
1910
2005
  }
1911
2006
 
1912
- const cache = new WeakMap();
1913
- function getCustomElement(node) {
1914
- const cached = cache.get(node);
1915
- if (cached !== undefined) {
1916
- return cached;
1917
- }
1918
- let value = null;
1919
- try {
1920
- if (node.nodeType !== Node.ELEMENT_NODE) {
1921
- return value;
1922
- }
1923
- const element = node;
1924
- const tagName = element.tagName.toLowerCase();
1925
- if (tagName.includes("-")) {
1926
- return value = tagName;
1927
- }
1928
- if (element.hasAttribute("is")) {
1929
- const is = element.getAttribute("is");
1930
- if (is.includes("-")) {
1931
- return value = is;
1932
- }
1933
- }
1934
- return value;
1935
- }
1936
- finally {
1937
- cache.set(node, value);
1938
- }
1939
- }
1940
-
1941
2007
  const completeByStateElementByWebComponent = new WeakMap();
1942
2008
  function markWebComponentAsComplete(webComponent, stateElement) {
1943
2009
  let completeByStateElement = completeByStateElementByWebComponent.get(webComponent);
@@ -2814,7 +2880,18 @@ function applyChangeToProperty(binding, _context, newValue) {
2814
2880
  if (propSegments.length === 1) {
2815
2881
  const firstSegment = propSegments[0];
2816
2882
  if (element[firstSegment] !== newValue) {
2817
- element[firstSegment] = newValue;
2883
+ try {
2884
+ element[firstSegment] = newValue;
2885
+ }
2886
+ catch (error) {
2887
+ if (config.debug) {
2888
+ console.warn(`Failed to set property '${firstSegment}' on element.`, {
2889
+ element,
2890
+ newValue,
2891
+ error
2892
+ });
2893
+ }
2894
+ }
2818
2895
  }
2819
2896
  return;
2820
2897
  }
@@ -2840,7 +2917,20 @@ function applyChangeToProperty(binding, _context, newValue) {
2840
2917
  }
2841
2918
  return;
2842
2919
  }
2843
- subObject[propSegments[propSegments.length - 1]] = newValue;
2920
+ try {
2921
+ subObject[propSegments[propSegments.length - 1]] = newValue;
2922
+ }
2923
+ catch (error) {
2924
+ if (config.debug) {
2925
+ console.warn(`Failed to set property on sub-object.`, {
2926
+ element,
2927
+ propSegments,
2928
+ oldValue,
2929
+ newValue,
2930
+ error
2931
+ });
2932
+ }
2933
+ }
2844
2934
  }
2845
2935
  }
2846
2936
 
@@ -5076,7 +5166,8 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo) {
5076
5166
  // 内側からのアクセスの場合、ルールがなければプライマリルールから新たにルールとバインディングを生成する
5077
5167
  const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
5078
5168
  if (typeof primaryMappingRuleSet === 'undefined') {
5079
- raiseError('Primary mapping rule set not found for web component.');
5169
+ // マッピングルールが存在しない場合はnullを返し、ローカル状態へのフォールバックを許可する
5170
+ return null;
5080
5171
  }
5081
5172
  let primaryMappingRule = null;
5082
5173
  for (const currentPrimaryMappingRule of primaryMappingRuleSet) {
@@ -5091,9 +5182,8 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo) {
5091
5182
  break;
5092
5183
  }
5093
5184
  if (primaryMappingRule === null) {
5094
- raiseError(`Mapping rule not found for inner path "${innerAbsPathInfo.pathInfo.path}". ` +
5095
- `Did you forget to bind this property in the component's data-wcs attribute? ` +
5096
- `Available mappings: ${Array.from(primaryMappingRuleSet).map(r => r.innerAbsPathInfo.pathInfo.path).join(', ')}`);
5185
+ // マッピングルールに一致しない場合はnullを返し、ローカル状態へのフォールバックを許可する
5186
+ return null;
5097
5187
  }
5098
5188
  // マッチした残りのパスをouterPathInfoに付与して新たなルールを生成
5099
5189
  const primaryBinding = primaryBindingByMappingRule.get(primaryMappingRule);
@@ -5155,36 +5245,43 @@ class InnerStateProxyHandler {
5155
5245
  // Promiseのthenと誤認識されるのを防ぐため、Promiseに存在するプロパティはProxyのgetで処理しない
5156
5246
  return undefined;
5157
5247
  }
5158
- if (prop in target) {
5159
- return Reflect.get(target, prop, receiver);
5160
- }
5161
5248
  if (prop[0] === '$') {
5162
5249
  return undefined;
5163
5250
  }
5251
+ // 1. getter完全一致 → ローカル計算(this = receiverで依存自動追跡)
5252
+ if (this._innerStateElement.getterPaths.has(prop) && prop in target) {
5253
+ return Reflect.get(target, prop, receiver);
5254
+ }
5255
+ // 2 & 3. マッピング完全一致 / サブパス → 親の状態
5164
5256
  const innerPathInfo = getPathInfo(prop);
5165
5257
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5166
5258
  const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
5167
- if (outerAbsPathInfo === null) {
5168
- raiseError(`Outer path info not found for inner path "${innerAbsPathInfo.pathInfo.path}" on web component.`);
5169
- }
5170
- const loopContext = getLoopContextByNode(this._webComponent);
5171
- let value = undefined;
5172
- outerAbsPathInfo.stateElement.createState("readonly", (state) => {
5173
- state[setLoopContextSymbol](loopContext, () => {
5174
- value = state[outerAbsPathInfo.pathInfo.path];
5175
- let listIndex = null;
5176
- if (loopContext !== null && loopContext.listIndex !== null) {
5177
- if (outerAbsPathInfo.pathInfo.wildcardCount > 0) {
5178
- // wildcardPathSetとloopContextのpathInfoSetのintersectionのうち、segment数が最も多いものをouterAbsPathInfoにする
5179
- // 例: outerPathInfoが "todos.*.name"で、loopContextのpathInfoSetに "todos.0.name", "todos.1.name"がある場合、"todos.0.name"や"todos.1.name"をouterAbsPathInfoにする
5180
- listIndex = loopContext.listIndex.at(outerAbsPathInfo.pathInfo.wildcardCount - 1);
5259
+ if (outerAbsPathInfo !== null) {
5260
+ const loopContext = getLoopContextByNode(this._webComponent);
5261
+ let value = undefined;
5262
+ outerAbsPathInfo.stateElement.createState("readonly", (state) => {
5263
+ state[setLoopContextSymbol](loopContext, () => {
5264
+ value = state[outerAbsPathInfo.pathInfo.path];
5265
+ let listIndex = null;
5266
+ if (loopContext !== null && loopContext.listIndex !== null) {
5267
+ if (outerAbsPathInfo.pathInfo.wildcardCount > 0) {
5268
+ // wildcardPathSetとloopContextのpathInfoSetのintersectionのうち、segment数が最も多いものをouterAbsPathInfoにする
5269
+ // 例: outerPathInfoが "todos.*.name"で、loopContextのpathInfoSetに "todos.0.name", "todos.1.name"がある場合、"todos.0.name"や"todos.1.name"をouterAbsPathInfoにする
5270
+ listIndex = loopContext.listIndex.at(outerAbsPathInfo.pathInfo.wildcardCount - 1);
5271
+ }
5181
5272
  }
5182
- }
5183
- const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
5184
- setLastValueByAbsoluteStateAddress(absStateAddress, value);
5273
+ const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
5274
+ setLastValueByAbsoluteStateAddress(absStateAddress, value);
5275
+ });
5185
5276
  });
5186
- });
5187
- return value;
5277
+ return value;
5278
+ }
5279
+ // 4. ローカルデータプロパティ → ローカル値
5280
+ if (prop in target) {
5281
+ return Reflect.get(target, prop, receiver);
5282
+ }
5283
+ // 5. エラー
5284
+ raiseError(`Property "${prop}" not found in inner state: no mapping rule and no local state property.`);
5188
5285
  }
5189
5286
  else {
5190
5287
  return Reflect.get(target, prop, receiver);
@@ -5192,19 +5289,29 @@ class InnerStateProxyHandler {
5192
5289
  }
5193
5290
  set(target, prop, value, receiver) {
5194
5291
  if (typeof prop === 'string') {
5292
+ // 1. setter完全一致 → ローカル処理(this = receiverで親への書き込み可能)
5293
+ if (this._innerStateElement.setterPaths.has(prop) && prop in target) {
5294
+ return Reflect.set(target, prop, value, receiver);
5295
+ }
5296
+ // 2 & 3. マッピング完全一致 / サブパス → 親に書く
5195
5297
  const innerPathInfo = getPathInfo(prop);
5196
5298
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5197
5299
  const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
5198
- if (outerAbsPathInfo === null) {
5199
- raiseError(`Outer path info not found for inner path "${innerAbsPathInfo.pathInfo.path}" on web component.`);
5200
- }
5201
- const loopContext = getLoopContextByNode(this._webComponent);
5202
- outerAbsPathInfo.stateElement.createState("writable", (state) => {
5203
- state[setLoopContextSymbol](loopContext, () => {
5204
- state[outerAbsPathInfo.pathInfo.path] = value;
5300
+ if (outerAbsPathInfo !== null) {
5301
+ const loopContext = getLoopContextByNode(this._webComponent);
5302
+ outerAbsPathInfo.stateElement.createState("writable", (state) => {
5303
+ state[setLoopContextSymbol](loopContext, () => {
5304
+ state[outerAbsPathInfo.pathInfo.path] = value;
5305
+ });
5205
5306
  });
5206
- });
5207
- return true;
5307
+ return true;
5308
+ }
5309
+ // 4. ローカルデータプロパティ → ローカルに書く
5310
+ if (prop in target) {
5311
+ return Reflect.set(target, prop, value, receiver);
5312
+ }
5313
+ // 5. エラー
5314
+ raiseError(`Property "${prop}" not found in inner state: no mapping rule and no local state property.`);
5208
5315
  }
5209
5316
  else {
5210
5317
  return Reflect.set(target, prop, value, receiver);
@@ -5212,19 +5319,26 @@ class InnerStateProxyHandler {
5212
5319
  }
5213
5320
  has(target, prop) {
5214
5321
  if (typeof prop === 'string') {
5215
- if (prop in target) {
5216
- return Reflect.has(target, prop);
5217
- }
5218
5322
  if (prop[0] === '$') {
5219
5323
  return false;
5220
5324
  }
5325
+ // 1. getter/setter完全一致
5326
+ if ((this._innerStateElement.getterPaths.has(prop) || this._innerStateElement.setterPaths.has(prop)) && prop in target) {
5327
+ return true;
5328
+ }
5329
+ // 2 & 3. マッピング
5221
5330
  const innerPathInfo = getPathInfo(prop);
5222
5331
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5223
5332
  const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
5224
- if (outerAbsPathInfo === null) {
5225
- return false;
5333
+ if (outerAbsPathInfo !== null) {
5334
+ return true;
5226
5335
  }
5227
- return true;
5336
+ // 4. ローカルデータ
5337
+ if (prop in target) {
5338
+ return true;
5339
+ }
5340
+ // 5. 存在しない
5341
+ return false;
5228
5342
  }
5229
5343
  else {
5230
5344
  return Reflect.has(target, prop);
@@ -5467,7 +5581,7 @@ class State extends HTMLElement {
5467
5581
  else {
5468
5582
  const script = this.querySelector('script[type="module"]');
5469
5583
  if (script) {
5470
- this._state = await loadFromInnerScript(script, `state#${this._name}`);
5584
+ this._state = await loadFromInnerScript(script, `${this._name}`);
5471
5585
  }
5472
5586
  else {
5473
5587
  const timerId = setTimeout(() => {