@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 +52 -20
- package/README.md +33 -1
- package/dist/index.esm.js +137 -72
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -2,13 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
**もしHTMLにリアクティブなデータバインディングがあったら?**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
ブラウザが状態をネイティブに理解する未来を想像してみてください。データをインラインで宣言し、属性でDOMにバインドするだけで、すべてが自動で同期します。仮想DOMもコンパイルも不要。ただのHTMLが、そのままリアクティブになる世界です。
|
|
6
6
|
|
|
7
|
-
それが `<wcs-state>` と `data-wcs`
|
|
7
|
+
それが `<wcs-state>` と `data-wcs` の目指すアプローチです。CDNからの読み込みだけで動作し、依存パッケージはゼロ、構文はHTMLそのままです。
|
|
8
8
|
|
|
9
|
-
CDN
|
|
9
|
+
CDNのスクリプトはカスタム要素の定義を登録するだけで、ロード時にはそれ以外の処理は走りません。`<wcs-state>` 要素がDOMに接続されたときにはじめて、状態ソースを読み取り、同一ルートノード(`document` または `ShadowRoot`)内の `data-wcs` バインディングを走査してリアクティビティを構築します。初期化プロセスはすべて要素のライフサイクルによって駆動されるため、独自の初期化コードを書く必要はありません。
|
|
10
10
|
|
|
11
|
-
##
|
|
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
|
-
|
|
213
|
+
ルールは1つだけです。**「パスに直接代入する」ことで、関連するDOMが自動的に更新されます。**
|
|
182
214
|
|
|
183
|
-
### なぜ `this.user.name = "Bob"`
|
|
215
|
+
### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
|
|
184
216
|
|
|
185
|
-
|
|
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
|
-
|
|
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
|
|
897
|
+
多くのフレームワークでは、コンポーネント間の状態共有に props のバケツリレー、Context Provider、あるいは外部ストア(Redux, Pinia など)といったパターンが用いられます。`@wcstack/state` はこれらとは異なるアプローチを採ります。親コンポーネントと子コンポーネントは**パスの契約**によって結びつけられます。親は `data-wcs` 属性を使って外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態として通常通り読み書きを行うだけです:
|
|
866
898
|
|
|
867
|
-
1.
|
|
899
|
+
1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
|
|
868
900
|
2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
|
|
869
|
-
3.
|
|
870
|
-
4.
|
|
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
|
|
1061
|
+
リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
|
|
1030
1062
|
|
|
1031
|
-
- フック内の `this`
|
|
1032
|
-
- `$connectedCallback`
|
|
1033
|
-
- `$disconnectedCallback`
|
|
1034
|
-
- `$updatedCallback(paths, indexesListByPath)`
|
|
1035
|
-
- Web 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
|
|
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
|
-
|
|
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
|
-
|
|
5095
|
-
|
|
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
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
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
|
-
|
|
5184
|
-
|
|
5224
|
+
const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
|
|
5225
|
+
setLastValueByAbsoluteStateAddress(absStateAddress, value);
|
|
5226
|
+
});
|
|
5185
5227
|
});
|
|
5186
|
-
|
|
5187
|
-
|
|
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
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
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
|
-
|
|
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
|
|
5225
|
-
return
|
|
5284
|
+
if (outerAbsPathInfo !== null) {
|
|
5285
|
+
return true;
|
|
5226
5286
|
}
|
|
5227
|
-
|
|
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);
|