@wcstack/state 1.3.17 → 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,11 +2,65 @@
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
+
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ステップで動作
44
+
45
+ ```html
46
+ <!-- 1. CDN を読み込む -->
47
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
48
+
49
+ <!-- 2. <wcs-state> タグを書く -->
50
+ <wcs-state>
51
+ <!-- 3. 状態オブジェクトを定義する -->
52
+ <script type="module">
53
+ export default {
54
+ message: "Hello, World!"
55
+ };
56
+ </script>
57
+ </wcs-state>
58
+
59
+ <!-- 4. data-wcs 属性でバインドする -->
60
+ <div data-wcs="textContent: message"></div>
61
+ ```
62
+
63
+ これだけです。ビルドツールも、初期化コードも、重いフレームワークも必要ありません。
10
64
 
11
65
  ## 特徴
12
66
 
@@ -156,11 +210,11 @@ this.count = 10; // パス "count"
156
210
  this["user.name"] = "Bob"; // パス "user.name"
157
211
  ```
158
212
 
159
- ルールはひとつ: **パスに代入すれば、DOMは自動的に更新される。**
213
+ ルールは1つだけです。**「パスに直接代入する」ことで、関連するDOMが自動的に更新されます。**
160
214
 
161
- ### なぜ `this.user.name = "Bob"` では動かないのか
215
+ ### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
162
216
 
163
- `this.user.name` は、まず `this.user` `user` オブジェクトを読み取り(パスの読み取り)、そのプレーンオブジェクトの `.name` に設定します — これはパスへの代入ではないため、変更は検知されません:
217
+ 通常のプロパティアクセスの書き方だと、まず `this.user` でプレーンな `user` オブジェクトを読み取り(パスの読み取り)、取得したオブジェクトの `.name` を直接書き換える挙動になります。これは「パスに対するプロパティ代入」というフックを経由していないため、システム側で変更を検知できません:
164
218
 
165
219
  ```javascript
166
220
  // ✅ パスへの代入 — 変更が検知される
@@ -172,7 +226,7 @@ this.user.name = "Bob";
172
226
 
173
227
  ### 配列
174
228
 
175
- 同じルールです: パスに新しい配列を代入します。破壊的メソッド(`push`, `splice`, `sort` 等)はパスへの代入なしに配列をその場で変更するため、非破壊的な代替メソッドを使用します:
229
+ 配列についても全く同じルールが適用されます。常に**パスに対して新しい配列を代入**してください。`push` `splice`、`sort` などの破壊的な配列メソッドは、パスへの代入を介さずに状態をその場で(in-placeに)書き換えてしまうため、変更が検知されません。代わりに、新しい配列を返す非破壊的なメソッドを使用します:
176
230
 
177
231
  ```javascript
178
232
  // ✅ 新しい配列をパスに代入 — 変更が検知される
@@ -840,14 +894,14 @@ export default {
840
894
 
841
895
  `@wcstack/state` は Shadow DOM または Light DOM を使用したカスタム要素との双方向状態バインディングに対応しています。
842
896
 
843
- 多くのフレームワークでは、コンポーネント間の状態共有に props バケツリレー、Context Provider、外部ストア(Redux, Pinia)といったパターンが用いられます。`@wcstack/state` は異なるアプローチを採ります。親子コンポーネントは**パスの契約**で接続されます。親は `data-wcs` で外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態を通常どおり読み書きするだけです:
897
+ 多くのフレームワークでは、コンポーネント間の状態共有に props のバケツリレー、Context Provider、あるいは外部ストア(Redux, Pinia など)といったパターンが用いられます。`@wcstack/state` はこれらとは異なるアプローチを採ります。親コンポーネントと子コンポーネントは**パスの契約**によって結びつけられます。親は `data-wcs` 属性を使って外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態として通常通り読み書きを行うだけです:
844
898
 
845
- 1. 子コンポーネントは自身の状態プロキシを通じて親の状態を参照・更新します — props もイベントも親の存在を意識する必要もありません。
899
+ 1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
846
900
  2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
847
- 3. 結合点は**パス名のみ**であるため、双方とも疎結合を保ち、独立してテスト可能です。
848
- 4. コストはパス解決(初回アクセス後はキャッシュにより O(1))と依存グラフを通じた変更伝播のみです。
901
+ 3. 結合点は**パス名のみ**であるため、親と子は完全に疎結合な状態を保ち、それぞれ独立してテスト可能です。
902
+ 4. 実行コストは、パスの解決(初回アクセス後はキャッシュされるため O(1) で動作します)と、依存グラフを通じた変更の伝播のみです。
849
903
 
850
- これは、コンポーネントレベルの抽象化ではなくパス解決に基づく、コンポーネント間状態管理への軽量なアプローチです。
904
+ これは、コンポーネントレベルの複雑な抽象化ではなく、「パスの解決」に基づいたコンポーネント間状態管理への軽量なアプローチです。
851
905
 
852
906
  ### コンポーネント定義(Shadow DOM)
853
907
 
@@ -1004,13 +1058,13 @@ customElements.define("my-component", MyComponent);
1004
1058
  | `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
1005
1059
  | `$updatedCallback(paths, indexesListByPath)` | 状態変更が適用された後に呼び出し | 戻り値は未使用(待機されない) |
1006
1060
 
1007
- リアクティブ Proxy は全てのプロパティ代入を変更として検知するため、標準の `async/await` とプロパティへの直接代入だけで非同期処理は完結します。ローディングフラグ、取得データ、エラーメッセージの更新は全てプロパティ代入であり、非同期状態管理のための追加の抽象化を必要としません。
1061
+ リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
1008
1062
 
1009
- - フック内の `this` は読み書き可能な状態プロキシです
1010
- - `$connectedCallback` は要素が接続される**たびに**呼ばれます(削除後の再接続を含む)。再確立が必要なセットアップ処理に適しています
1011
- - `$disconnectedCallback` は同期的に呼び出されます — タイマーのクリア、イベントリスナーの削除、リソースの解放などのクリーンアップに使用します
1012
- - `$updatedCallback(paths, indexesListByPath)` は更新された状態パスの一覧を受け取ります。ワイルドカードパス更新時は `indexesListByPath` で更新対象インデックスも受け取れます
1013
- - 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` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
1014
1068
 
1015
1069
  ## 設定
1016
1070
 
@@ -1077,12 +1131,21 @@ bootstrapState();
1077
1131
 
1078
1132
  ```
1079
1133
  bootstrapState()
1080
- ├── registerComponents() // <wcs-state> カスタム要素を登録
1081
- └── registerHandler() // DOMContentLoaded ハンドラ
1082
- ├── waitForStateInitialize() // 全 <wcs-state> の読み込み待機
1083
- ├── convertMustacheToComments() // {{ }} → コメントノードに変換
1084
- ├── collectStructuralFragments() // for/if テンプレートを収集
1085
- └── initializeBindings() // DOM 走査、data-wcs 解析、バインディング設定
1134
+ └── registerComponents() // <wcs-state> カスタム要素を登録
1135
+
1136
+ <wcs-state> connectedCallback
1137
+ ├── _initializeBindWebComponent() // bind-component: 親コンポーネントから状態を取得
1138
+ ├── _initialize() // 状態をロード (state属性 / src / json / script / API)
1139
+ └── setStateElementByName() // WeakMap<Node, Map<name, element>> に登録
1140
+ │ └── (rootNode への初回登録時)
1141
+ │ └── queueMicrotask → buildBindings()
1142
+ ├── _callStateConnectedCallback() // $connectedCallback が定義されていれば呼び出し
1143
+
1144
+ buildBindings(root)
1145
+ ├── waitForStateInitialize() // 全 <wcs-state> の initializePromise を待機
1146
+ ├── convertMustacheToComments() // {{ }} → コメントノードに変換
1147
+ ├── collectStructuralFragments() // for/if テンプレートを収集
1148
+ └── initializeBindings() // DOM 走査、data-wcs 解析、バインディング設定
1086
1149
  ```
1087
1150
 
1088
1151
  ### リアクティビティフロー
package/README.md CHANGED
@@ -6,7 +6,61 @@ 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.
42
+
43
+ ## 4 Steps to Reactive HTML
44
+
45
+ ```html
46
+ <!-- 1. Load the CDN -->
47
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
48
+
49
+ <!-- 2. Write a <wcs-state> tag -->
50
+ <wcs-state>
51
+ <!-- 3. Define your state object -->
52
+ <script type="module">
53
+ export default {
54
+ message: "Hello, World!"
55
+ };
56
+ </script>
57
+ </wcs-state>
58
+
59
+ <!-- 4. Bind with data-wcs attributes -->
60
+ <div data-wcs="textContent: message"></div>
61
+ ```
62
+
63
+ That's it. No build, no bootstrap code, no framework.
10
64
 
11
65
  ## Features
12
66
 
@@ -1077,12 +1131,21 @@ bootstrapState();
1077
1131
 
1078
1132
  ```
1079
1133
  bootstrapState()
1080
- ├── registerComponents() // Register <wcs-state> custom element
1081
- └── registerHandler() // DOMContentLoaded handler
1082
- ├── waitForStateInitialize() // Wait for all <wcs-state> to load
1083
- ├── convertMustacheToComments() // {{ }} comment nodes
1084
- ├── collectStructuralFragments() // Collect for/if templates
1085
- └── initializeBindings() // Walk DOM, parse data-wcs, set up bindings
1134
+ └── registerComponents() // Register <wcs-state> custom element
1135
+
1136
+ <wcs-state> connectedCallback
1137
+ ├── _initializeBindWebComponent() // bind-component: get state from parent component
1138
+ ├── _initialize() // Load state (state attr / src / json / script / API)
1139
+ └── setStateElementByName() // Register to WeakMap<Node, Map<name, element>>
1140
+ │ └── (first registration per rootNode)
1141
+ │ └── queueMicrotask → buildBindings()
1142
+ ├── _callStateConnectedCallback() // Call $connectedCallback if defined
1143
+
1144
+ buildBindings(root)
1145
+ ├── waitForStateInitialize() // Wait for all <wcs-state> initializePromise
1146
+ ├── convertMustacheToComments() // {{ }} → comment nodes
1147
+ ├── collectStructuralFragments() // Collect for/if templates
1148
+ └── initializeBindings() // Walk DOM, parse data-wcs, set up bindings
1086
1149
  ```
1087
1150
 
1088
1151
  ### Reactivity Flow
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,32 +1979,6 @@ function clearAbsoluteStateAddressByBinding(binding) {
1909
1979
  absoluteStateAddressByBinding.delete(binding);
1910
1980
  }
1911
1981
 
1912
- const cache = new WeakMap();
1913
- function isCustomElement(node) {
1914
- let value = cache.get(node);
1915
- if (value !== undefined) {
1916
- return value;
1917
- }
1918
- try {
1919
- if (node.nodeType !== Node.ELEMENT_NODE) {
1920
- return value = false;
1921
- }
1922
- const element = node;
1923
- if (element.tagName.includes("-")) {
1924
- return value = true;
1925
- }
1926
- if (element.hasAttribute("is")) {
1927
- if (element.getAttribute("is")?.includes("-")) {
1928
- return value = true;
1929
- }
1930
- }
1931
- return value = false;
1932
- }
1933
- finally {
1934
- cache.set(node, value ?? false);
1935
- }
1936
- }
1937
-
1938
1982
  const completeByStateElementByWebComponent = new WeakMap();
1939
1983
  function markWebComponentAsComplete(webComponent, stateElement) {
1940
1984
  let completeByStateElement = completeByStateElementByWebComponent.get(webComponent);
@@ -2967,9 +3011,9 @@ function applyChange(binding, context) {
2967
3011
  if (binding.bindingType === "event") {
2968
3012
  return;
2969
3013
  }
2970
- if (isCustomElement(binding.replaceNode)) {
2971
- const element = binding.replaceNode;
2972
- if (customElements.get(element.tagName.toLowerCase()) === undefined) {
3014
+ const customTag = getCustomElement(binding.replaceNode);
3015
+ if (customTag) {
3016
+ if (customElements.get(customTag) === undefined) {
2973
3017
  // cutomElement側の初期化を期待
2974
3018
  return;
2975
3019
  }
@@ -5073,7 +5117,8 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo) {
5073
5117
  // 内側からのアクセスの場合、ルールがなければプライマリルールから新たにルールとバインディングを生成する
5074
5118
  const primaryMappingRuleSet = primaryMappingRuleSetByElement.get(webComponent);
5075
5119
  if (typeof primaryMappingRuleSet === 'undefined') {
5076
- raiseError('Primary mapping rule set not found for web component.');
5120
+ // マッピングルールが存在しない場合はnullを返し、ローカル状態へのフォールバックを許可する
5121
+ return null;
5077
5122
  }
5078
5123
  let primaryMappingRule = null;
5079
5124
  for (const currentPrimaryMappingRule of primaryMappingRuleSet) {
@@ -5088,9 +5133,8 @@ function getOuterAbsolutePathInfo(webComponent, innerAbsPathInfo) {
5088
5133
  break;
5089
5134
  }
5090
5135
  if (primaryMappingRule === null) {
5091
- raiseError(`Mapping rule not found for inner path "${innerAbsPathInfo.pathInfo.path}". ` +
5092
- `Did you forget to bind this property in the component's data-wcs attribute? ` +
5093
- `Available mappings: ${Array.from(primaryMappingRuleSet).map(r => r.innerAbsPathInfo.pathInfo.path).join(', ')}`);
5136
+ // マッピングルールに一致しない場合はnullを返し、ローカル状態へのフォールバックを許可する
5137
+ return null;
5094
5138
  }
5095
5139
  // マッチした残りのパスをouterPathInfoに付与して新たなルールを生成
5096
5140
  const primaryBinding = primaryBindingByMappingRule.get(primaryMappingRule);
@@ -5152,36 +5196,43 @@ class InnerStateProxyHandler {
5152
5196
  // Promiseのthenと誤認識されるのを防ぐため、Promiseに存在するプロパティはProxyのgetで処理しない
5153
5197
  return undefined;
5154
5198
  }
5155
- if (prop in target) {
5156
- return Reflect.get(target, prop, receiver);
5157
- }
5158
5199
  if (prop[0] === '$') {
5159
5200
  return undefined;
5160
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. マッピング完全一致 / サブパス → 親の状態
5161
5207
  const innerPathInfo = getPathInfo(prop);
5162
5208
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5163
5209
  const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
5164
- if (outerAbsPathInfo === null) {
5165
- raiseError(`Outer path info not found for inner path "${innerAbsPathInfo.pathInfo.path}" on web component.`);
5166
- }
5167
- const loopContext = getLoopContextByNode(this._webComponent);
5168
- let value = undefined;
5169
- outerAbsPathInfo.stateElement.createState("readonly", (state) => {
5170
- state[setLoopContextSymbol](loopContext, () => {
5171
- value = state[outerAbsPathInfo.pathInfo.path];
5172
- let listIndex = null;
5173
- if (loopContext !== null && loopContext.listIndex !== null) {
5174
- if (outerAbsPathInfo.pathInfo.wildcardCount > 0) {
5175
- // wildcardPathSetとloopContextのpathInfoSetのintersectionのうち、segment数が最も多いものをouterAbsPathInfoにする
5176
- // 例: outerPathInfoが "todos.*.name"で、loopContextのpathInfoSetに "todos.0.name", "todos.1.name"がある場合、"todos.0.name"や"todos.1.name"をouterAbsPathInfoにする
5177
- 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
+ }
5178
5223
  }
5179
- }
5180
- const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
5181
- setLastValueByAbsoluteStateAddress(absStateAddress, value);
5224
+ const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
5225
+ setLastValueByAbsoluteStateAddress(absStateAddress, value);
5226
+ });
5182
5227
  });
5183
- });
5184
- 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.`);
5185
5236
  }
5186
5237
  else {
5187
5238
  return Reflect.get(target, prop, receiver);
@@ -5189,19 +5240,29 @@ class InnerStateProxyHandler {
5189
5240
  }
5190
5241
  set(target, prop, value, receiver) {
5191
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. マッピング完全一致 / サブパス → 親に書く
5192
5248
  const innerPathInfo = getPathInfo(prop);
5193
5249
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5194
5250
  const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
5195
- if (outerAbsPathInfo === null) {
5196
- raiseError(`Outer path info not found for inner path "${innerAbsPathInfo.pathInfo.path}" on web component.`);
5197
- }
5198
- const loopContext = getLoopContextByNode(this._webComponent);
5199
- outerAbsPathInfo.stateElement.createState("writable", (state) => {
5200
- state[setLoopContextSymbol](loopContext, () => {
5201
- 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
+ });
5202
5257
  });
5203
- });
5204
- 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.`);
5205
5266
  }
5206
5267
  else {
5207
5268
  return Reflect.set(target, prop, value, receiver);
@@ -5209,19 +5270,26 @@ class InnerStateProxyHandler {
5209
5270
  }
5210
5271
  has(target, prop) {
5211
5272
  if (typeof prop === 'string') {
5212
- if (prop in target) {
5213
- return Reflect.has(target, prop);
5214
- }
5215
5273
  if (prop[0] === '$') {
5216
5274
  return false;
5217
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. マッピング
5218
5281
  const innerPathInfo = getPathInfo(prop);
5219
5282
  const innerAbsPathInfo = getAbsolutePathInfo(this._innerStateElement, innerPathInfo);
5220
5283
  const outerAbsPathInfo = getOuterAbsolutePathInfo(this._webComponent, innerAbsPathInfo);
5221
- if (outerAbsPathInfo === null) {
5222
- return false;
5284
+ if (outerAbsPathInfo !== null) {
5285
+ return true;
5223
5286
  }
5224
- return true;
5287
+ // 4. ローカルデータ
5288
+ if (prop in target) {
5289
+ return true;
5290
+ }
5291
+ // 5. 存在しない
5292
+ return false;
5225
5293
  }
5226
5294
  else {
5227
5295
  return Reflect.has(target, prop);
@@ -5494,7 +5562,8 @@ class State extends HTMLElement {
5494
5562
  : parentNode instanceof Element
5495
5563
  ? parentNode
5496
5564
  : null;
5497
- if (boundComponent === null || !isCustomElement(boundComponent)) {
5565
+ const customTagName = boundComponent ? getCustomElement(boundComponent) : null;
5566
+ if (boundComponent === null || customTagName === null) {
5498
5567
  raiseError(`"bind-component" requires <${config.tagNames.state}> to be a direct child of a custom element.`);
5499
5568
  }
5500
5569
  // LightDOMの場合、名前空間が上位スコープと共有されるためnameが必須
@@ -5502,7 +5571,7 @@ class State extends HTMLElement {
5502
5571
  raiseError(`"bind-component" in Light DOM requires a "name" attribute to avoid namespace conflicts with the parent scope.`);
5503
5572
  }
5504
5573
  const boundComponentStateProp = this.getAttribute("bind-component");
5505
- await customElements.whenDefined(boundComponent.tagName.toLowerCase());
5574
+ await customElements.whenDefined(customTagName.toLowerCase());
5506
5575
  // data-wcs属性がある場合は、上位の状態によりbinding情報の設定が完了するまで待機する
5507
5576
  if (boundComponent.hasAttribute(config.bindAttributeName)) {
5508
5577
  await waitInitializeBinding(boundComponent);