@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 +87 -24
- package/README.md +70 -7
- package/dist/index.esm.js +143 -74
- 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,11 +2,65 @@
|
|
|
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
|
+
|
|
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
|
-
|
|
213
|
+
ルールは1つだけです。**「パスに直接代入する」ことで、関連するDOMが自動的に更新されます。**
|
|
160
214
|
|
|
161
|
-
### なぜ `this.user.name = "Bob"`
|
|
215
|
+
### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
|
|
162
216
|
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
897
|
+
多くのフレームワークでは、コンポーネント間の状態共有に props のバケツリレー、Context Provider、あるいは外部ストア(Redux, Pinia など)といったパターンが用いられます。`@wcstack/state` はこれらとは異なるアプローチを採ります。親コンポーネントと子コンポーネントは**パスの契約**によって結びつけられます。親は `data-wcs` 属性を使って外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態として通常通り読み書きを行うだけです:
|
|
844
898
|
|
|
845
|
-
1.
|
|
899
|
+
1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
|
|
846
900
|
2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
|
|
847
|
-
3.
|
|
848
|
-
4.
|
|
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
|
|
1061
|
+
リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
|
|
1008
1062
|
|
|
1009
|
-
- フック内の `this`
|
|
1010
|
-
- `$connectedCallback`
|
|
1011
|
-
- `$disconnectedCallback`
|
|
1012
|
-
- `$updatedCallback(paths, indexesListByPath)`
|
|
1013
|
-
- Web 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
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
|
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
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
2971
|
-
|
|
2972
|
-
if (customElements.get(
|
|
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
|
-
|
|
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
|
-
|
|
5092
|
-
|
|
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
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
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
|
-
|
|
5181
|
-
|
|
5224
|
+
const absStateAddress = createAbsoluteStateAddress(outerAbsPathInfo, listIndex);
|
|
5225
|
+
setLastValueByAbsoluteStateAddress(absStateAddress, value);
|
|
5226
|
+
});
|
|
5182
5227
|
});
|
|
5183
|
-
|
|
5184
|
-
|
|
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
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
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
|
-
|
|
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
|
|
5222
|
-
return
|
|
5284
|
+
if (outerAbsPathInfo !== null) {
|
|
5285
|
+
return true;
|
|
5223
5286
|
}
|
|
5224
|
-
|
|
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
|
-
|
|
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(
|
|
5574
|
+
await customElements.whenDefined(customTagName.toLowerCase());
|
|
5506
5575
|
// data-wcs属性がある場合は、上位の状態によりbinding情報の設定が完了するまで待機する
|
|
5507
5576
|
if (boundComponent.hasAttribute(config.bindAttributeName)) {
|
|
5508
5577
|
await waitInitializeBinding(boundComponent);
|