@wcstack/state 1.3.18 → 1.3.19

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
@@ -1609,6 +1609,35 @@ function attachEventHandler(binding) {
1609
1609
  return true;
1610
1610
  }
1611
1611
 
1612
+ const cache = new WeakMap();
1613
+ function getCustomElement(node) {
1614
+ const cached = cache.get(node);
1615
+ if (cached !== undefined) {
1616
+ return cached;
1617
+ }
1618
+ let value = null;
1619
+ try {
1620
+ if (node.nodeType !== Node.ELEMENT_NODE) {
1621
+ return value;
1622
+ }
1623
+ const element = node;
1624
+ const tagName = element.tagName.toLowerCase();
1625
+ if (tagName.includes("-")) {
1626
+ return value = tagName;
1627
+ }
1628
+ if (element.hasAttribute("is")) {
1629
+ const is = element.getAttribute("is");
1630
+ if (is.includes("-")) {
1631
+ return value = is;
1632
+ }
1633
+ }
1634
+ return value;
1635
+ }
1636
+ finally {
1637
+ cache.set(node, value);
1638
+ }
1639
+ }
1640
+
1612
1641
  const CHECK_TYPES = new Set(['radio', 'checkbox']);
1613
1642
  const DEFAULT_VALUE_PROP_NAMES = new Set(['value', 'valueAsNumber', 'valueAsDate']);
1614
1643
  function isPossibleTwoWay(node, propName) {
@@ -1635,6 +1664,20 @@ function isPossibleTwoWay(node, propName) {
1635
1664
  if (tagName === 'textarea' && propName === 'value') {
1636
1665
  return true;
1637
1666
  }
1667
+ const customTagName = getCustomElement(element);
1668
+ if (customTagName !== null) {
1669
+ const customClass = customElements.get(customTagName);
1670
+ if (typeof customClass === "undefined") {
1671
+ raiseError(`Custom element <${customTagName}> is not defined. Cannot determine if property "${propName}" is suitable for two-way binding.`);
1672
+ }
1673
+ const reactivityInfo = customClass.wcsReactivity;
1674
+ if (reactivityInfo) {
1675
+ if (reactivityInfo.properties?.includes(propName)
1676
+ || (reactivityInfo.propertyMap?.[propName] ?? null) !== null) {
1677
+ return true;
1678
+ }
1679
+ }
1680
+ }
1638
1681
  return false;
1639
1682
  }
1640
1683
 
@@ -1646,7 +1689,26 @@ function getHandlerKey$2(binding, eventName) {
1646
1689
  }
1647
1690
  function getEventName$2(binding) {
1648
1691
  const tagName = binding.node.tagName.toLowerCase();
1692
+ // 1.default event name
1649
1693
  let eventName = (tagName === 'select') ? 'change' : 'input';
1694
+ // 2.protocol
1695
+ const customTagName = getCustomElement(binding.node);
1696
+ if (customTagName !== null) {
1697
+ const customClass = customElements.get(customTagName);
1698
+ if (typeof customClass === "undefined") {
1699
+ raiseError(`Custom element <${customTagName}> is not defined. Cannot determine event name for two-way binding.`);
1700
+ }
1701
+ const reactivityInfo = customClass.wcsReactivity;
1702
+ if (reactivityInfo) {
1703
+ if (reactivityInfo.properties?.includes(binding.propName)) {
1704
+ eventName = reactivityInfo.defaultEvent;
1705
+ }
1706
+ if (reactivityInfo.propertyMap?.[binding.propName]) {
1707
+ eventName = reactivityInfo.propertyMap[binding.propName];
1708
+ }
1709
+ }
1710
+ }
1711
+ // 3.modifier
1650
1712
  for (const modifier of binding.propModifiers) {
1651
1713
  if (modifier.startsWith('on')) {
1652
1714
  eventName = modifier.slice(2);
@@ -1682,6 +1744,16 @@ const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilter
1682
1744
  });
1683
1745
  };
1684
1746
  function attachTwowayEventHandler(binding) {
1747
+ const customTagName = getCustomElement(binding.node);
1748
+ if (customTagName !== null) {
1749
+ const customClass = customElements.get(customTagName);
1750
+ if (typeof customClass === "undefined") {
1751
+ customElements.whenDefined(customTagName).then(() => {
1752
+ attachTwowayEventHandler(binding);
1753
+ });
1754
+ return;
1755
+ }
1756
+ }
1685
1757
  if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
1686
1758
  const eventName = getEventName$2(binding);
1687
1759
  const key = getHandlerKey$2(binding, eventName);
@@ -1699,9 +1771,7 @@ function attachTwowayEventHandler(binding) {
1699
1771
  else {
1700
1772
  bindingSet.add(binding);
1701
1773
  }
1702
- return true;
1703
1774
  }
1704
- return false;
1705
1775
  }
1706
1776
 
1707
1777
  const lastListValueByAbsoluteStateAddress = new WeakMap();
@@ -1909,35 +1979,6 @@ function clearAbsoluteStateAddressByBinding(binding) {
1909
1979
  absoluteStateAddressByBinding.delete(binding);
1910
1980
  }
1911
1981
 
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
1982
  const completeByStateElementByWebComponent = new WeakMap();
1942
1983
  function markWebComponentAsComplete(webComponent, stateElement) {
1943
1984
  let completeByStateElement = completeByStateElementByWebComponent.get(webComponent);
@@ -5076,7 +5117,8 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo) {
5076
5117
  // 内側からのアクセスの場合、ルールがなければプライマリルールから新たにルールとバインディングを生成する
5077
5118
  const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
5078
5119
  if (typeof primaryMappingRuleSet === 'undefined') {
5079
- raiseError('Primary mapping rule set not found for web component.');
5120
+ // マッピングルールが存在しない場合はnullを返し、ローカル状態へのフォールバックを許可する
5121
+ return null;
5080
5122
  }
5081
5123
  let primaryMappingRule = null;
5082
5124
  for (const currentPrimaryMappingRule of primaryMappingRuleSet) {
@@ -5091,9 +5133,8 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo) {
5091
5133
  break;
5092
5134
  }
5093
5135
  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(', ')}`);
5136
+ // マッピングルールに一致しない場合はnullを返し、ローカル状態へのフォールバックを許可する
5137
+ return null;
5097
5138
  }
5098
5139
  // マッチした残りのパスをouterPathInfoに付与して新たなルールを生成
5099
5140
  const primaryBinding = primaryBindingByMappingRule.get(primaryMappingRule);
@@ -5155,36 +5196,43 @@ class InnerStateProxyHandler {
5155
5196
  // Promiseのthenと誤認識されるのを防ぐため、Promiseに存在するプロパティはProxyのgetで処理しない
5156
5197
  return undefined;
5157
5198
  }
5158
- if (prop in target) {
5159
- return Reflect.get(target, prop, receiver);
5160
- }
5161
5199
  if (prop[0] === '$') {
5162
5200
  return undefined;
5163
5201
  }
5202
+ // 1. getter完全一致 → ローカル計算(this = receiverで依存自動追跡)
5203
+ if (this._innerStateElement.getterPaths.has(prop) && prop in target) {
5204
+ return Reflect.get(target, prop, receiver);
5205
+ }
5206
+ // 2 & 3. マッピング完全一致 / サブパス → 親の状態
5164
5207
  const innerPathInfo = getPathInfo(prop);
5165
5208
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5166
5209
  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);
5210
+ if (outerAbsPathInfo !== null) {
5211
+ const loopContext = getLoopContextByNode(this._webComponent);
5212
+ let value = undefined;
5213
+ outerAbsPathInfo.stateElement.createState("readonly", (state) => {
5214
+ state[setLoopContextSymbol](loopContext, () => {
5215
+ value = state[outerAbsPathInfo.pathInfo.path];
5216
+ let listIndex = null;
5217
+ if (loopContext !== null && loopContext.listIndex !== null) {
5218
+ if (outerAbsPathInfo.pathInfo.wildcardCount > 0) {
5219
+ // wildcardPathSetとloopContextのpathInfoSetのintersectionのうち、segment数が最も多いものをouterAbsPathInfoにする
5220
+ // 例: outerPathInfoが "todos.*.name"で、loopContextのpathInfoSetに "todos.0.name", "todos.1.name"がある場合、"todos.0.name"や"todos.1.name"をouterAbsPathInfoにする
5221
+ listIndex = loopContext.listIndex.at(outerAbsPathInfo.pathInfo.wildcardCount - 1);
5222
+ }
5181
5223
  }
5182
- }
5183
- const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
5184
- setLastValueByAbsoluteStateAddress(absStateAddress, value);
5224
+ const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
5225
+ setLastValueByAbsoluteStateAddress(absStateAddress, value);
5226
+ });
5185
5227
  });
5186
- });
5187
- return value;
5228
+ return value;
5229
+ }
5230
+ // 4. ローカルデータプロパティ → ローカル値
5231
+ if (prop in target) {
5232
+ return Reflect.get(target, prop, receiver);
5233
+ }
5234
+ // 5. エラー
5235
+ raiseError(`Property "${prop}" not found in inner state: no mapping rule and no local state property.`);
5188
5236
  }
5189
5237
  else {
5190
5238
  return Reflect.get(target, prop, receiver);
@@ -5192,19 +5240,29 @@ class InnerStateProxyHandler {
5192
5240
  }
5193
5241
  set(target, prop, value, receiver) {
5194
5242
  if (typeof prop === 'string') {
5243
+ // 1. setter完全一致 → ローカル処理(this = receiverで親への書き込み可能)
5244
+ if (this._innerStateElement.setterPaths.has(prop) && prop in target) {
5245
+ return Reflect.set(target, prop, value, receiver);
5246
+ }
5247
+ // 2 & 3. マッピング完全一致 / サブパス → 親に書く
5195
5248
  const innerPathInfo = getPathInfo(prop);
5196
5249
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5197
5250
  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;
5251
+ if (outerAbsPathInfo !== null) {
5252
+ const loopContext = getLoopContextByNode(this._webComponent);
5253
+ outerAbsPathInfo.stateElement.createState("writable", (state) => {
5254
+ state[setLoopContextSymbol](loopContext, () => {
5255
+ state[outerAbsPathInfo.pathInfo.path] = value;
5256
+ });
5205
5257
  });
5206
- });
5207
- return true;
5258
+ return true;
5259
+ }
5260
+ // 4. ローカルデータプロパティ → ローカルに書く
5261
+ if (prop in target) {
5262
+ return Reflect.set(target, prop, value, receiver);
5263
+ }
5264
+ // 5. エラー
5265
+ raiseError(`Property "${prop}" not found in inner state: no mapping rule and no local state property.`);
5208
5266
  }
5209
5267
  else {
5210
5268
  return Reflect.set(target, prop, value, receiver);
@@ -5212,19 +5270,26 @@ class InnerStateProxyHandler {
5212
5270
  }
5213
5271
  has(target, prop) {
5214
5272
  if (typeof prop === 'string') {
5215
- if (prop in target) {
5216
- return Reflect.has(target, prop);
5217
- }
5218
5273
  if (prop[0] === '$') {
5219
5274
  return false;
5220
5275
  }
5276
+ // 1. getter/setter完全一致
5277
+ if ((this._innerStateElement.getterPaths.has(prop) || this._innerStateElement.setterPaths.has(prop)) && prop in target) {
5278
+ return true;
5279
+ }
5280
+ // 2 & 3. マッピング
5221
5281
  const innerPathInfo = getPathInfo(prop);
5222
5282
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5223
5283
  const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
5224
- if (outerAbsPathInfo === null) {
5225
- return false;
5284
+ if (outerAbsPathInfo !== null) {
5285
+ return true;
5226
5286
  }
5227
- return true;
5287
+ // 4. ローカルデータ
5288
+ if (prop in target) {
5289
+ return true;
5290
+ }
5291
+ // 5. 存在しない
5292
+ return false;
5228
5293
  }
5229
5294
  else {
5230
5295
  return Reflect.has(target, prop);