@wcstack/state 1.9.0 → 1.9.1

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
@@ -1,1380 +1,1380 @@
1
- # @wcstack/state
2
-
3
- **これは便利な既存FWの別実装ではありません。フロントエンド開発の前提を組み替える、別系譜の試みです。**
4
-
5
- 多くのライブラリは、UI・状態・コンポーネントの結合点を JavaScript の中に置きます。`@wcstack/state` はそこを選びません。仮想DOMも、コンパイルも、hook も、selector も前提にせず、HTML とパス文字列だけを契約として UI と状態を結びつけます。
6
-
7
- それが `<wcs-state>` と `data-wcs` のアプローチです。CDNからの読み込みだけで動作し、依存パッケージはゼロ、構文はHTMLそのままです。CDNのスクリプトはカスタム要素の定義を登録するだけで、ロード時にはそれ以外の処理は走りません。`<wcs-state>` 要素がDOMに接続されたときにはじめて、状態ソースを読み取り、同一ルートノード(`document` または `ShadowRoot`)内の `data-wcs` バインディングを走査してリアクティビティを構築します。初期化プロセスはすべて要素のライフサイクルによって駆動されるため、独自の初期化コードを書く必要はありません。
8
-
9
- ## ここには存在しないもの
10
-
11
- 以下は未実装ではありません。**設計上、存在しません。**
12
-
13
- - 変数を取り出す API
14
- - 要素ごとに状態を束縛するオブジェクト
15
- - hook
16
- - selector
17
- - reactive primitive をコンポーネントへ引き込むための glue code
18
-
19
- None of these exist by design.
20
-
21
- なぜなら、このライブラリでは UI と状態の結合点を JavaScript の中に置かないからです。状態を「取り出して」コンポーネントへ渡すのではなく、HTML 側がパス文字列によって状態を参照します。要素は状態を所有せず、状態も要素を知りません。両者が共有するのはパスだけです。
22
-
23
- ## 既存FWとは比較しません
24
-
25
- これは React / Vue / Solid と同じ問題を別の方法で解いているのではありません。**前提自体が違います。**
26
-
27
- | 一般的なFWが前提にするもの | `@wcstack/state` が前提にするもの |
28
- |---|---|
29
- | コンポーネントが UI と状態の結合点 | パス文字列が UI と状態の結合点 |
30
- | JavaScript が描画の中心 | HTML と DOM が中心 |
31
- | state を取り出して component へ流し込む | path を宣言して DOM を状態へ接続する |
32
- | hook / selector / signal で購読する | 属性とパスで束縛する |
33
- | フレームワークの実行モデルにアプリ全体を載せる | ブラウザ標準の上に薄い reactive layer を足す |
34
-
35
- 比較表を作るより先に、この前提差を理解してください。同じ棚に置いても、解いている問題の切り取り方が違います。
36
-
37
- ## 第一原理: パスが唯一の契約
38
-
39
- 既存の多くのフレームワークでは、**コンポーネント**がUIと状態の結合点になっています。状態ストアを外部に切り出しても、コンポーネント内にフックやセレクタ、リアクティブプリミティブといった**状態を引き込むためのコード**が必ず必要になります。つまり、UIと状態は常にJavaScriptの中で結びついているのです。
40
-
41
- `@wcstack/state` はこの結合を完全に排除しました。UIと状態を結びつけているのは**パス文字列**だけです — `user.name` や `cart.items.*.subtotal` のようなドット区切りのアドレスのみが、2つのレイヤー間の唯一の契約(コントラクト)になります:
42
-
43
- | レイヤー | 知っていること | 知らないこと |
44
- |----------|----------------|--------------|
45
- | **状態** (`<wcs-state>`) | データ構造とビジネスロジック | どのDOM要素がバインドされているか |
46
- | **UI** (`data-wcs`) | パス文字列と表示意図 | 状態がどう保存・算出されているか |
47
- | **コンポーネント** (`@name`) | 名前付き状態から必要なパス | 他コンポーネントの内部実装 |
48
-
49
- 3つのレベルのパス契約が疎結合を実現しています:
50
-
51
- 1. **UI ↔ 状態** — `data-wcs="textContent: user.name"` という属性がバインディングのすべてです。フックもセレクタもリアクティブプリミティブもありません。コンポーネントのJavaScriptには、状態を参照するコードが**一行も**必要ありません。
52
-
53
- 2. **コンポーネント ↔ コンポーネント** — コンポーネント間の通信は、名前付き状態の参照(`@stateName`)で行われます。コンポーネント同士がお互いを直接インポートしたり参照したりすることはありません。共有するのはパスの命名規約だけです。
54
-
55
- 3. **ループコンテキスト** — `for` ループ内では `*` が抽象インデックスとして機能します。`items.*.price` のようなバインディングは自動的に現在の要素へと解決されます。テンプレートは自身の具体的な位置(インデックス)を知る必要がなく、ワイルドカードがその契約となります。
56
-
57
- ### なぜこれが重要なのか
58
-
59
- これはUIと状態の完全な分離を、**JavaScriptのコードを介することなく**実現していることを意味します。つまり:
60
-
61
- - UIをすべて作り直しても、状態のロジックに触れる必要がありません。
62
- - 状態のデータ構造をリファクタリングしても、パス文字列の更新だけで済みます。
63
- - HTMLを読むだけで、すべてのデータ依存関係を把握できます。
64
-
65
- このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
66
-
67
- 以下の機能はすべて、この原理の帰結です。機能が先にあり、その説明として哲学を後付けしているのではありません。
68
-
69
- ## わずか4ステップで動作
70
-
71
- ```html
72
- <!-- 1. CDN を読み込む -->
73
- <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
74
-
75
- <!-- 2. <wcs-state> タグを書く -->
76
- <wcs-state>
77
- <!-- 3. 状態オブジェクトを定義する -->
78
- <script type="module">
79
- export default {
80
- message: "Hello, World!"
81
- };
82
- </script>
83
- </wcs-state>
84
-
85
- <!-- 4. data-wcs 属性でバインドする -->
86
- <div data-wcs="textContent: message"></div>
87
- ```
88
-
89
- これだけです。ビルドツールも、初期化コードも、重いフレームワークも必要ありません。
90
-
91
- ## この原理から導かれる機能
92
-
93
- - **宣言的データバインディング** — `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
94
- - **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
95
- - **構造ディレクティブ** — `<template>` 要素による `for`, `if` / `elseif` / `else`
96
- - **組み込みフィルタ** — フォーマット、比較、算術、日付など 40 種類
97
- - **双方向バインディング** — `<input>`, `<select>`, `<textarea>` で自動有効
98
- - **Web Component バインディング** — Shadow DOM コンポーネントとの双方向状態バインディング
99
- - **パス getter** — ドットパスキー getter(`get "users.*.fullName"()`)によるデータツリーの任意の深さへのフラットな仮想プロパティ定義、自動依存追跡・キャッシュ
100
- - **Mustache 構文** — テキストノードでの `{{ path|filter }}`
101
- - **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
102
- - **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
103
- - **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`、Web Component 用 `$stateReadyCallback`
104
- - **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))
105
- - **サーバーサイドレンダリング** — `enable-ssr` 属性 + `@wcstack/server` でフル SSR と自動ハイドレーション
106
- - **依存ゼロ** — ランタイム依存なし
107
-
108
- ## インストール
109
-
110
- ### CDN(推奨)
111
-
112
- ```html
113
- <!-- 自動初期化 — これだけで動作します -->
114
- <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
115
- ```
116
-
117
- ### CDN(手動初期化)
118
-
119
- ```html
120
- <script type="module">
121
- import { bootstrapState } from 'https://esm.run/@wcstack/state';
122
- bootstrapState();
123
- </script>
124
- ```
125
-
126
- ## 基本的な使い方
127
-
128
- ```html
129
- <wcs-state>
130
- <script type="module">
131
- export default {
132
- count: 0,
133
- user: { id: 1, name: "Alice" },
134
- users: [
135
- { id: 1, name: "Alice" },
136
- { id: 2, name: "Bob" },
137
- { id: 3, name: "Charlie" }
138
- ],
139
- countUp() { this.count += 1; },
140
- clearCount() { this.count = 0; },
141
- get "users.*.displayName"() {
142
- return this["users.*.name"] + " (ID: " + this["users.*.id"] + ")";
143
- }
144
- };
145
- </script>
146
- </wcs-state>
147
-
148
- <!-- テキストバインディング -->
149
- <div data-wcs="textContent: count"></div>
150
- {{ count }}
151
-
152
- <!-- 双方向入力バインディング -->
153
- <input type="text" data-wcs="value: user.name">
154
-
155
- <!-- イベントバインディング -->
156
- <button data-wcs="onclick: countUp">Increment</button>
157
-
158
- <!-- 条件付きクラス -->
159
- <div data-wcs="textContent: count; class.over: count|gt(10)"></div>
160
-
161
- <!-- ループ -->
162
- <template data-wcs="for: users">
163
- <div>
164
- <span data-wcs="textContent: .id"></span>:
165
- <span data-wcs="textContent: .displayName"></span>
166
- </div>
167
- </template>
168
-
169
- <!-- 条件分岐レンダリング -->
170
- <template data-wcs="if: count|gt(0)">
171
- <p>カウントは正の値です。</p>
172
- </template>
173
- <template data-wcs="elseif: count|lt(0)">
174
- <p>カウントは負の値です。</p>
175
- </template>
176
- <template data-wcs="else:">
177
- <p>カウントはゼロです。</p>
178
- </template>
179
- ```
180
-
181
- ## 状態の初期化
182
-
183
- `<wcs-state>` は複数の方法で初期状態を読み込めます:
184
-
185
- ```html
186
- <!-- 1. <script type="application/json"> を id で参照 -->
187
- <script type="application/json" id="state">
188
- { "count": 0 }
189
- </script>
190
- <wcs-state state="state"></wcs-state>
191
-
192
- <!-- 2. インライン JSON 属性 -->
193
- <wcs-state json='{ "count": 0 }'></wcs-state>
194
-
195
- <!-- 3. 外部 JSON ファイル -->
196
- <wcs-state src="./data.json"></wcs-state>
197
-
198
- <!-- 4. 外部 JS モジュール (export default { ... }) -->
199
- <wcs-state src="./state.js"></wcs-state>
200
-
201
- <!-- 5. インラインスクリプトモジュール -->
202
- <wcs-state>
203
- <script type="module">
204
- export default { count: 0 };
205
- </script>
206
- </wcs-state>
207
-
208
- <!-- 6. プログラム API -->
209
- <script>
210
- const el = document.createElement('wcs-state');
211
- el.setInitialState({ count: 0 });
212
- document.body.appendChild(el);
213
- </script>
214
- ```
215
-
216
- 解決順序: `state` → `src` (.json / .js) → `json` → 内包 `<script>` → `setInitialState()` 待機。
217
-
218
- ### 名前付き状態
219
-
220
- 複数の状態要素を `name` 属性で共存できます。バインディングでは `@name` で参照します:
221
-
222
- ```html
223
- <wcs-state name="cart">...</wcs-state>
224
- <wcs-state name="user">...</wcs-state>
225
-
226
- <div data-wcs="textContent: total@cart"></div>
227
- <div data-wcs="textContent: name@user"></div>
228
- ```
229
-
230
- デフォルト名は `"default"`(`@` 不要)です。
231
-
232
- ## 状態の更新
233
-
234
- `@wcstack/state` では、すべての状態は**パス**を持ちます — `count`、`user.name`、`items` のように。状態をリアクティブに更新するには、**パスに代入**します:
235
-
236
- ```javascript
237
- this.count = 10; // パス "count"
238
- this["user.name"] = "Bob"; // パス "user.name"
239
- ```
240
-
241
- ルールは1つだけです。**「パスに直接代入する」ことで、関連するDOMが自動的に更新されます。**
242
-
243
- ### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
244
-
245
- これは単なる制約ではなく、**契約境界が見えている箇所**です。
246
-
247
- 通常のプロパティアクセスの書き方だと、まず `this.user` でプレーンな `user` オブジェクトを読み取り(パスの読み取り)、取得したオブジェクトの `.name` を直接書き換える挙動になります。これは「パスに対するプロパティ代入」という契約を通っていません。そのため、システム側は変更を検知しません:
248
-
249
- ```javascript
250
- // ✅ パスへの代入 — 変更が検知される
251
- this["user.name"] = "Bob";
252
-
253
- // ❌ パスへの代入ではない — 変更は検知されない
254
- this.user.name = "Bob";
255
- ```
256
-
257
- `this.user.name = "Bob"` も動くようにすると、一見便利にはなります。しかしその瞬間に「UI と状態はパスだけで結ばれる」という原理が崩れます。どこで依存を追跡し、どこで更新を確定するかが曖昧になり、契約境界が失われます。
258
-
259
- ### 配列
260
-
261
- 配列についても全く同じルールが適用されます。常に**パスに対して新しい配列を代入**してください。`push` や `splice`、`sort` などの破壊的な配列メソッドは、パスへの代入を介さずに状態をその場で(in-placeに)書き換えてしまうため、変更が検知されません。代わりに、新しい配列を返す非破壊的なメソッドを使用します:
262
-
263
- ```javascript
264
- // ✅ 新しい配列をパスに代入 — 変更が検知される
265
- this.items = this.items.concat({ id: 4, text: "New" });
266
- this.items = this.items.toSpliced(index, 1);
267
- this.items = this.items.filter(item => !item.done);
268
- this.items = this.items.toSorted((a, b) => a.id - b.id);
269
- this.items = this.items.toReversed();
270
- this.items = this.items.with(index, newValue);
271
-
272
- // ❌ その場での変更 — パスへの代入なし、変更は検知されない
273
- this.items.push({ id: 4, text: "New" });
274
- this.items.splice(index, 1);
275
- this.items.sort((a, b) => a.id - b.id);
276
- ```
277
-
278
- ## バインディング構文
279
-
280
- ### `data-wcs` 属性
281
-
282
- ```
283
- property[#modifier]: path[@state][|filter[|filter(args)...]]
284
- ```
285
-
286
- 複数バインディングは `;` で区切ります:
287
-
288
- ```html
289
- <div data-wcs="textContent: count; class.over: count|gt(10)"></div>
290
- ```
291
-
292
- | 要素 | 説明 | 例 |
293
- |---|---|---|
294
- | `property` | バインドする DOM プロパティ | `value`, `textContent`, `checked` |
295
- | `#modifier` | バインディング修飾子 | `#ro`, `#prevent`, `#stop`, `#onchange` |
296
- | `path` | 状態プロパティパス | `count`, `user.name`, `users.*.name` |
297
- | `@state` | 名前付き状態の参照 | `@cart`, `@user` |
298
- | `\|filter` | 変換フィルタチェーン | `\|gt(0)`, `\|round\|locale` |
299
-
300
- ### プロパティ種別
301
-
302
- | プロパティ | 説明 |
303
- |---|---|
304
- | `value` | 要素の値(input では双方向) |
305
- | `checked` | チェックボックス / ラジオボタンの選択状態(双方向) |
306
- | `textContent` | テキストコンテンツ |
307
- | `text` | textContent のエイリアス |
308
- | `html` | innerHTML |
309
- | `class.NAME` | CSS クラスの切り替え |
310
- | `style.PROP` | CSS スタイルプロパティの設定 |
311
- | `attr.NAME` | 属性の設定(SVG 名前空間対応) |
312
- | `radio` | ラジオボタングループバインディング(双方向) |
313
- | `checkbox` | チェックボックスグループの配列バインディング(双方向) |
314
- | `onclick`, `on*` | イベントハンドラバインディング |
315
-
316
- ### 修飾子
317
-
318
- | 修飾子 | 説明 |
319
- |---|---|
320
- | `#ro` | 読み取り専用 — 双方向バインディングを無効化 |
321
- | `#prevent` | イベントハンドラで `event.preventDefault()` を呼び出す |
322
- | `#stop` | イベントハンドラで `event.stopPropagation()` を呼び出す |
323
- | `#onchange` | 双方向バインディングで `input` の代わりに `change` イベントを使用 |
324
-
325
- ### 双方向バインディング
326
-
327
- 以下の要素で自動的に有効化されます:
328
-
329
- | 要素 | プロパティ | イベント |
330
- |---|---|---|
331
- | `<input type="checkbox/radio">` | `checked` | `input` |
332
- | `<input>`(その他の type) | `value`, `valueAsNumber`, `valueAsDate` | `input` |
333
- | `<select>` | `value` | `change` |
334
- | `<textarea>` | `value` | `input` |
335
-
336
- `<input type="button">` は除外されます。`#ro` で無効化、`#onchange` でイベントを変更できます。
337
-
338
- ### ラジオボタンバインディング
339
-
340
- `radio` でラジオボタングループを単一の状態値にバインドします:
341
-
342
- ```html
343
- <input type="radio" value="red" data-wcs="radio: selectedColor">
344
- <input type="radio" value="blue" data-wcs="radio: selectedColor">
345
- ```
346
-
347
- 状態値と一致する `value` を持つラジオボタンが自動的にチェックされます。ユーザーが別のラジオボタンを選択すると、状態が更新されます。`#ro` で読み取り専用にできます。
348
-
349
- `for` ループ内での使用:
350
-
351
- ```html
352
- <template data-wcs="for: branches">
353
- <label>
354
- <input type="radio" data-wcs="value: .; radio: currentBranch">
355
- {{ . }}
356
- </label>
357
- </template>
358
- ```
359
-
360
- ### チェックボックスバインディング
361
-
362
- `checkbox` でチェックボックスグループを状態配列にバインドします:
363
-
364
- ```html
365
- <input type="checkbox" value="apple" data-wcs="checkbox: selectedFruits">
366
- <input type="checkbox" value="banana" data-wcs="checkbox: selectedFruits">
367
- <input type="checkbox" value="orange" data-wcs="checkbox: selectedFruits">
368
- ```
369
-
370
- チェックボックスの `value` が状態配列に含まれている場合にチェック状態になります。チェックボックスの切り替えで配列への値の追加・削除が行われます。`|int` で文字列値を数値に変換、`#ro` で読み取り専用にできます。
371
-
372
- ### Mustache 構文
373
-
374
- `enableMustache` が `true`(デフォルト)の場合、テキストノードで `{{ expression }}` が使用できます:
375
-
376
- ```html
377
- <p>こんにちは、{{ user.name }}さん!</p>
378
- <p>カウント: {{ count|locale }}</p>
379
- ```
380
-
381
- 内部的にはコメントベースのバインディング(`<!--@@:expression-->`)に変換されます。
382
-
383
- ## 構造ディレクティブ
384
-
385
- 構造ディレクティブは `<template>` 要素で使用します:
386
-
387
- ### ループ (`for`)
388
-
389
- ```html
390
- <template data-wcs="for: users">
391
- <div>
392
- <!-- フルパス -->
393
- <span data-wcs="textContent: users.*.name"></span>
394
- <!-- 省略形(ループコンテキストからの相対パス) -->
395
- <span data-wcs="textContent: .name"></span>
396
- </div>
397
- </template>
398
- ```
399
-
400
- `for:` ディレクティブは**値ベースの差分アルゴリズム**を使用します。配列の各要素の値そのものが識別キーとして機能するため、React の `key` や Vue の `:key` のような明示的なキー属性は不要です。配列が再代入されると、差分アルゴリズムが新旧の要素を値で照合し、変更のない要素の DOM ノードを再利用しつつ、追加・削除・並び替えを効率的に処理します。
401
-
402
- #### ドット省略記法
403
-
404
- `for` ループ内では、`.` で始まるパスがループの配列パスを基準に展開されます:
405
-
406
- | 省略形 | 展開後 | 説明 |
407
- |---|---|---|
408
- | `.name` | `users.*.name` | 現在の要素のプロパティ |
409
- | `.` | `users.*` | 現在の要素そのもの |
410
- | `.name\|uc` | `users.*.name\|uc` | フィルタは保持される |
411
- | `.name@state` | `users.*.name@state` | 状態名は保持される |
412
-
413
- プリミティブ配列では、`.` が要素の値を直接参照します:
414
-
415
- ```html
416
- <template data-wcs="for: branches">
417
- <label>
418
- <input type="radio" data-wcs="value: .; radio: currentBranch">
419
- {{ . }}
420
- </label>
421
- </template>
422
- ```
423
-
424
- 多重ワイルドカードによるネストループに対応しています。ネストされた `for` ディレクティブの `.` 省略記法も親ループのパスを基準に展開されます:
425
-
426
- ```html
427
- <template data-wcs="for: regions">
428
- <!-- .states → regions.*.states -->
429
- <template data-wcs="for: .states">
430
- <!-- .name → regions.*.states.*.name -->
431
- <span data-wcs="textContent: .name"></span>
432
- </template>
433
- </template>
434
- ```
435
-
436
- ### 条件分岐 (`if` / `elseif` / `else`)
437
-
438
- ```html
439
- <template data-wcs="if: count|gt(0)">
440
- <p>正の値</p>
441
- </template>
442
- <template data-wcs="elseif: count|lt(0)">
443
- <p>負の値</p>
444
- </template>
445
- <template data-wcs="else:">
446
- <p>ゼロ</p>
447
- </template>
448
- ```
449
-
450
- 条件をチェーンできます。`elseif` は前の条件を自動的に反転します。
451
-
452
- ## パス getter(算出プロパティ)
453
-
454
- **パス getter** は `@wcstack/state` の中核機能です。JavaScript の getter に**ドットパス文字列キー**とワイルドカード(`*`)を使って定義します。**データツリーの任意の深さに仮想プロパティを追加でき、すべてを1箇所にフラットに定義できます**。データのネストがどれほど深くても、定義側は同じレベルに並び、ループ要素ごとの自動依存追跡が機能します。
455
-
456
- ### 基本的なパス getter
457
-
458
- ```html
459
- <wcs-state>
460
- <script type="module">
461
- export default {
462
- users: [
463
- { id: 1, firstName: "Alice", lastName: "Smith" },
464
- { id: 2, firstName: "Bob", lastName: "Jones" }
465
- ],
466
- // パス getter — ループ内で要素ごとに実行
467
- get "users.*.fullName"() {
468
- return this["users.*.firstName"] + " " + this["users.*.lastName"];
469
- },
470
- get "users.*.displayName"() {
471
- return this["users.*.fullName"] + " (ID: " + this["users.*.id"] + ")";
472
- }
473
- };
474
- </script>
475
- </wcs-state>
476
-
477
- <template data-wcs="for: users">
478
- <div data-wcs="textContent: .displayName"></div>
479
- </template>
480
- <!-- 出力:
481
- Alice Smith (ID: 1)
482
- Bob Jones (ID: 2)
483
- -->
484
- ```
485
-
486
- パス getter 内の `this["users.*.firstName"]` は、手動でインデックスを指定することなく、自動的に現在のループ要素に解決されます。
487
-
488
- ### トップレベル算出プロパティ
489
-
490
- ワイルドカードなしの getter は通常の算出プロパティとして動作します:
491
-
492
- ```javascript
493
- export default {
494
- price: 100,
495
- tax: 0.1,
496
- get total() {
497
- return this.price * (1 + this.tax);
498
- }
499
- };
500
- ```
501
-
502
- ### getter のチェーン
503
-
504
- パス getter は他のパス getter を参照でき、依存チェーンを形成します。上流の値が変更されると、キャッシュは自動的に無効化されます:
505
-
506
- ```html
507
- <wcs-state>
508
- <script type="module">
509
- export default {
510
- taxRate: 0.1,
511
- cart: {
512
- items: [
513
- { productId: "P001", quantity: 2, unitPrice: 500 },
514
- { productId: "P002", quantity: 1, unitPrice: 1200 }
515
- ]
516
- },
517
- // アイテムごとの小計
518
- get "cart.items.*.subtotal"() {
519
- return this["cart.items.*.unitPrice"] * this["cart.items.*.quantity"];
520
- },
521
- // 集計: 全小計の合計
522
- get "cart.totalPrice"() {
523
- return this.$getAll("cart.items.*.subtotal", []).reduce((sum, v) => sum + v, 0);
524
- },
525
- // チェーン: totalPrice から税を算出
526
- get "cart.tax"() {
527
- return this["cart.totalPrice"] * this.taxRate;
528
- },
529
- // チェーン: 合計金額
530
- get "cart.grandTotal"() {
531
- return this["cart.totalPrice"] + this["cart.tax"];
532
- }
533
- };
534
- </script>
535
- </wcs-state>
536
-
537
- <template data-wcs="for: cart.items">
538
- <div>
539
- <span data-wcs="textContent: .productId"></span>:
540
- <span data-wcs="textContent: .subtotal|locale"></span>
541
- </div>
542
- </template>
543
- <p>合計: <span data-wcs="textContent: cart.totalPrice|locale"></span></p>
544
- <p>税: <span data-wcs="textContent: cart.tax|locale"></span></p>
545
- <p>総合計: <span data-wcs="textContent: cart.grandTotal|locale"></span></p>
546
- ```
547
-
548
- 依存チェーン: `cart.grandTotal` → `cart.tax` → `cart.totalPrice` → `cart.items.*.subtotal` → `cart.items.*.unitPrice` / `cart.items.*.quantity`。アイテムの `unitPrice` や `quantity` を変更すると、チェーン全体が自動的に再計算されます。
549
-
550
- ### ネストされたワイルドカード getter
551
-
552
- ネストされた配列構造では複数のワイルドカードが使用できます:
553
-
554
- ```html
555
- <wcs-state>
556
- <script type="module">
557
- export default {
558
- categories: [
559
- {
560
- name: "果物",
561
- items: [
562
- { name: "りんご", price: 150 },
563
- { name: "バナナ", price: 100 }
564
- ]
565
- },
566
- {
567
- name: "野菜",
568
- items: [
569
- { name: "にんじん", price: 80 }
570
- ]
571
- }
572
- ],
573
- get "categories.*.items.*.label"() {
574
- return this["categories.*.name"] + " / " + this["categories.*.items.*.name"];
575
- }
576
- };
577
- </script>
578
- </wcs-state>
579
-
580
- <template data-wcs="for: categories">
581
- <h3 data-wcs="textContent: .name"></h3>
582
- <template data-wcs="for: .items">
583
- <div data-wcs="textContent: .label"></div>
584
- </template>
585
- </template>
586
- <!-- 出力:
587
- 果物
588
- 果物 / りんご
589
- 果物 / バナナ
590
- 野菜
591
- 野菜 / にんじん
592
- -->
593
- ```
594
-
595
- ### フラットな仮想プロパティ — ネストの深さに依存しない定義
596
-
597
- パス getter の重要な利点は、**データのネストがどれほど深くても、すべての仮想プロパティを1箇所にフラットに定義できる**ことです。各ネストレベルに算出プロパティを持たせるためだけにコンポーネントを分割する必要がありません。
598
-
599
- ```javascript
600
- export default {
601
- regions: [
602
- { name: "関東", prefectures: [
603
- { name: "東京", cities: [
604
- { name: "渋谷", population: 230000, area: 15.11 },
605
- { name: "新宿", population: 346000, area: 18.22 }
606
- ]},
607
- { name: "神奈川", cities: [
608
- { name: "横浜", population: 3750000, area: 437.56 }
609
- ]}
610
- ]}
611
- ],
612
-
613
- // --- ネストの深さに関係なく、すべてフラットに定義 ---
614
-
615
- // 市レベル — 仮想プロパティ
616
- get "regions.*.prefectures.*.cities.*.density"() {
617
- return this["regions.*.prefectures.*.cities.*.population"]
618
- / this["regions.*.prefectures.*.cities.*.area"];
619
- },
620
- get "regions.*.prefectures.*.cities.*.label"() {
621
- return this["regions.*.prefectures.*.name"] + " "
622
- + this["regions.*.prefectures.*.cities.*.name"];
623
- },
624
-
625
- // 県レベル — 市からの集約
626
- get "regions.*.prefectures.*.totalPopulation"() {
627
- return this.$getAll("regions.*.prefectures.*.cities.*.population", [])
628
- .reduce((a, b) => a + b, 0);
629
- },
630
-
631
- // 地方レベル — 県からの集約
632
- get "regions.*.totalPopulation"() {
633
- return this.$getAll("regions.*.prefectures.*.totalPopulation", [])
634
- .reduce((a, b) => a + b, 0);
635
- },
636
-
637
- // トップレベル — 地方からの集約
638
- get totalPopulation() {
639
- return this.$getAll("regions.*.totalPopulation", [])
640
- .reduce((a, b) => a + b, 0);
641
- }
642
- };
643
- ```
644
-
645
- 3階層のネスト、5つの仮想プロパティ — すべてが1つのフラットなオブジェクト内に並んで定義されています。各レベルは任意の深さの値を参照でき、`$getAll` による集約は下位から上位へ自然に流れます。コンポーネントベースのフレームワークでは、一般的に各ネストレベルに個別のコンポーネントを作成し、算出値をツリーの上位に渡す方法が採られます。パス getter は、すべての定義を1箇所にまとめるという異なるトレードオフを提供します。
646
-
647
- ### getter の戻り値のサブプロパティへのアクセス
648
-
649
- パス getter がオブジェクトを返す場合、ドットパスでそのサブプロパティにアクセスできます:
650
-
651
- ```javascript
652
- export default {
653
- products: [
654
- { id: "P001", name: "ウィジェット", price: 500, stock: 10 },
655
- { id: "P002", name: "ガジェット", price: 1200, stock: 3 }
656
- ],
657
- cart: {
658
- items: [
659
- { productId: "P001", quantity: 2 },
660
- { productId: "P002", quantity: 1 }
661
- ]
662
- },
663
- get productByProductId() {
664
- return new Map(this.products.map(p => [p.id, p]));
665
- },
666
- // 完全な product オブジェクトを返す
667
- get "cart.items.*.product"() {
668
- return this.productByProductId.get(this["cart.items.*.productId"]);
669
- },
670
- // 戻り値のサブプロパティにアクセス
671
- get "cart.items.*.total"() {
672
- return this["cart.items.*.product.price"] * this["cart.items.*.quantity"];
673
- }
674
- };
675
- ```
676
-
677
- `this["cart.items.*.product.price"]` は `cart.items.*.product` getter が返すオブジェクトを透過的にチェーンします。
678
-
679
- ### パス setter
680
-
681
- `set "path"()` でカスタム setter ロジックを定義できます:
682
-
683
- ```javascript
684
- export default {
685
- users: [
686
- { firstName: "Alice", lastName: "Smith" },
687
- { firstName: "Bob", lastName: "Jones" }
688
- ],
689
- get "users.*.fullName"() {
690
- return this["users.*.firstName"] + " " + this["users.*.lastName"];
691
- },
692
- set "users.*.fullName"(value) {
693
- const [first, ...rest] = value.split(" ");
694
- this["users.*.firstName"] = first;
695
- this["users.*.lastName"] = rest.join(" ");
696
- }
697
- };
698
- ```
699
-
700
- ```html
701
- <template data-wcs="for: users">
702
- <input type="text" data-wcs="value: .fullName">
703
- </template>
704
- ```
705
-
706
- パス setter は双方向バインディングと連携します — input を編集すると setter が呼ばれ、`firstName` / `lastName` に分割して書き戻します。
707
-
708
- ### 対応するパス getter パターン
709
-
710
- | パターン | 説明 | 例 |
711
- |---|---|---|
712
- | `get prop()` | トップレベル算出 | `get total()` |
713
- | `get "a.b"()` | ネスト算出(ワイルドカードなし) | `get "cart.totalPrice"()` |
714
- | `get "a.*.b"()` | 単一ワイルドカード | `get "users.*.fullName"()` |
715
- | `get "a.*.b.*.c"()` | 複数ワイルドカード | `get "categories.*.items.*.label"()` |
716
- | `set "a.*.b"(v)` | ワイルドカード setter | `set "users.*.fullName"(v)` |
717
-
718
- ### 仕組み
719
-
720
- 1. **コンテキスト解決** — `for:` ループのレンダリング時に、各イテレーションが `ListIndex` をアドレススタックにプッシュします。パス getter 内の `this["users.*.name"]` はこのスタックを使って `*` を解決するため、常に現在の要素を参照します。
721
-
722
- 2. **自動依存追跡** — getter が `this["users.*.name"]` にアクセスすると、`users.*.name` から getter のパスへの動的依存が登録されます。`users.*.name` が変更されると、getter のキャッシュが dirty になります。
723
-
724
- 3. **キャッシュ** — getter の結果は具体的なアドレス(パス + ループインデックス)ごとにキャッシュされます。`users.*.fullName` のインデックス 0 とインデックス 1 は別々のキャッシュエントリを持ちます。依存先が変更された場合のみキャッシュが無効化されます。
725
-
726
- 4. **直接インデックスアクセス** — 数値インデックスで特定の要素にアクセスすることもできます:`this["users.0.name"]` はループコンテキストなしで `users[0].name` に解決されます。
727
-
728
- ### ループインデックス変数(`$1`, `$2`, ...)
729
-
730
- getter やイベントハンドラ内で、`this.$1`、`this.$2` などで現在のループイテレーションのインデックスを取得できます(0始まりの値、1始まりの命名):
731
-
732
- ```javascript
733
- export default {
734
- users: ["Alice", "Bob", "Charlie"],
735
- get "users.*.rowLabel"() {
736
- return "#" + (this.$1 + 1) + ": " + this["users.*"];
737
- }
738
- };
739
- ```
740
-
741
- ```html
742
- <template data-wcs="for: users">
743
- <div data-wcs="textContent: .rowLabel"></div>
744
- </template>
745
- <!-- 出力:
746
- #1: Alice
747
- #2: Bob
748
- #3: Charlie
749
- -->
750
- ```
751
-
752
- ネストループでは、`$1` が外側のインデックス、`$2` が内側のインデックスです。
753
-
754
- テンプレート内でループインデックスを直接表示することもできます:
755
-
756
- ```html
757
- <template data-wcs="for: items">
758
- <td>{{ $1|inc(1) }}</td> <!-- 1始まりの行番号 -->
759
- </template>
760
- ```
761
-
762
- ### Proxy API
763
-
764
- 状態オブジェクト内(getter / メソッド)で `this` 経由で以下の API が利用できます:
765
-
766
- | API | 説明 |
767
- |---|---|
768
- | `this.$getAll(path, indexes?)` | ワイルドカードパスにマッチする全ての値を取得 |
769
- | `this.$resolve(path, indexes, value?)` | ワイルドカードパスを特定のインデックスで解決 |
770
- | `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行 |
771
- | `this.$trackDependency(path)` | キャッシュ無効化のための依存関係を手動で登録 |
772
- | `this.$stateElement` | `IStateElement` インスタンスへのアクセス |
773
- | `this.$1`, `this.$2`, ... | 現在のループインデックス(1始まりの命名、0始まりの値) |
774
-
775
- #### `$getAll` — 配列要素全体の集計
776
-
777
- `$getAll` はワイルドカードパスにマッチする全ての値を配列として収集します。集計パターンに不可欠です:
778
-
779
- ```javascript
780
- export default {
781
- scores: [85, 92, 78, 95, 88],
782
- get average() {
783
- const all = this.$getAll("scores.*", []);
784
- return all.reduce((sum, v) => sum + v, 0) / all.length;
785
- },
786
- get max() {
787
- return Math.max(...this.$getAll("scores.*", []));
788
- }
789
- };
790
- ```
791
-
792
- #### `$resolve` — 明示的なインデックスでのアクセス
793
-
794
- `$resolve` は特定のワイルドカードインデックスの値を読み書きします:
795
-
796
- ```javascript
797
- export default {
798
- items: ["A", "B", "C"],
799
- swapFirstTwo() {
800
- const a = this.$resolve("items.*", [0]);
801
- const b = this.$resolve("items.*", [1]);
802
- this.$resolve("items.*", [0], b);
803
- this.$resolve("items.*", [1], a);
804
- }
805
- };
806
- ```
807
-
808
- ## イベントハンドリング
809
-
810
- `on*` プロパティでイベントハンドラをバインドします:
811
-
812
- ```html
813
- <button data-wcs="onclick: handleClick">クリック</button>
814
- <form data-wcs="onsubmit#prevent: handleSubmit">...</form>
815
- ```
816
-
817
- ハンドラメソッドはイベントとループインデックスを受け取ります:
818
-
819
- ```javascript
820
- export default {
821
- items: ["A", "B", "C"],
822
- handleClick(event) {
823
- console.log("clicked");
824
- },
825
- removeItem(event, index) {
826
- // index はループコンテキスト ($1)
827
- this.items = this.items.toSpliced(index, 1);
828
- }
829
- };
830
- ```
831
-
832
- ```html
833
- <template data-wcs="for: items">
834
- <button data-wcs="onclick: removeItem">削除</button>
835
- </template>
836
- ```
837
-
838
- ## フィルタ
839
-
840
- 40 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます。
841
-
842
- ### 比較
843
-
844
- | フィルタ | 説明 | 例 |
845
- |---|---|---|
846
- | `eq(value)` | 等しい | `count\|eq(0)` → `true/false` |
847
- | `ne(value)` | 等しくない | `count\|ne(0)` |
848
- | `not` | 論理否定 | `isActive\|not` |
849
- | `lt(n)` | より小さい | `count\|lt(10)` |
850
- | `le(n)` | 以下 | `count\|le(10)` |
851
- | `gt(n)` | より大きい | `count\|gt(0)` |
852
- | `ge(n)` | 以上 | `count\|ge(0)` |
853
-
854
- ### 算術
855
-
856
- | フィルタ | 説明 | 例 |
857
- |---|---|---|
858
- | `inc(n)` | 加算 | `count\|inc(1)` |
859
- | `dec(n)` | 減算 | `count\|dec(1)` |
860
- | `mul(n)` | 乗算 | `price\|mul(1.1)` |
861
- | `div(n)` | 除算 | `total\|div(100)` |
862
- | `mod(n)` | 剰余 | `index\|mod(2)` |
863
-
864
- ### 数値フォーマット
865
-
866
- | フィルタ | 説明 | 例 |
867
- |---|---|---|
868
- | `fix(n)` | 固定小数点桁数 | `price\|fix(2)` → `"100.00"` |
869
- | `round(n?)` | 四捨五入 | `value\|round(2)` |
870
- | `floor(n?)` | 切り捨て | `value\|floor` |
871
- | `ceil(n?)` | 切り上げ | `value\|ceil` |
872
- | `locale(loc?)` | ロケール数値フォーマット | `count\|locale` / `count\|locale(ja-JP)` |
873
- | `percent(n?)` | パーセンテージフォーマット | `ratio\|percent(1)` |
874
-
875
- ### 文字列
876
-
877
- | フィルタ | 説明 | 例 |
878
- |---|---|---|
879
- | `uc` | 大文字変換 | `name\|uc` |
880
- | `lc` | 小文字変換 | `name\|lc` |
881
- | `cap` | 先頭大文字 | `name\|cap` |
882
- | `trim` | 空白除去 | `text\|trim` |
883
- | `slice(n)` | 文字列スライス | `text\|slice(5)` |
884
- | `substr(start, length)` | 部分文字列 | `text\|substr(0,10)` |
885
- | `pad(n, char?)` | 先頭パディング | `id\|pad(5,0)` → `"00001"` |
886
- | `rep(n)` | 繰り返し | `text\|rep(3)` |
887
- | `rev` | 反転 | `text\|rev` |
888
-
889
- ### 型変換
890
-
891
- | フィルタ | 説明 | 例 |
892
- |---|---|---|
893
- | `int` | 整数パース | `input\|int` |
894
- | `float` | 浮動小数点パース | `input\|float` |
895
- | `boolean` | 真偽値に変換 | `value\|boolean` |
896
- | `number` | 数値に変換 | `value\|number` |
897
- | `string` | 文字列に変換 | `value\|string` |
898
- | `null` | null に変換 | `value\|null` |
899
-
900
- ### 日付 / 時刻
901
-
902
- | フィルタ | 説明 | 例 |
903
- |---|---|---|
904
- | `date(loc?)` | 日付フォーマット | `timestamp\|date` / `timestamp\|date(ja-JP)` |
905
- | `time(loc?)` | 時刻フォーマット | `timestamp\|time` |
906
- | `datetime(loc?)` | 日付 + 時刻 | `timestamp\|datetime(en-US)` |
907
- | `ymd(sep?)` | YYYY-MM-DD | `timestamp\|ymd` / `timestamp\|ymd(/)` |
908
-
909
- ### 真偽値 / デフォルト
910
-
911
- | フィルタ | 説明 | 例 |
912
- |---|---|---|
913
- | `truthy` | truthy チェック | `value\|truthy` |
914
- | `falsy` | falsy チェック | `value\|falsy` |
915
- | `defaults(v)` | フォールバック値 | `name\|defaults(Anonymous)` |
916
-
917
- ### フィルタチェーン
918
-
919
- フィルタは `|` で連結できます:
920
-
921
- ```html
922
- <div data-wcs="textContent: price|mul(1.1)|round(2)|locale(ja-JP)"></div>
923
- ```
924
-
925
- ## Web Component バインディング
926
-
927
- `@wcstack/state` は Shadow DOM または Light DOM を使用したカスタム要素との双方向状態バインディングに対応しています。
928
-
929
- 多くのフレームワークでは、コンポーネント間の状態共有に props のバケツリレー、Context Provider、あるいは外部ストア(Redux, Pinia など)といったパターンが用いられます。`@wcstack/state` はこれらとは異なるアプローチを採ります。親コンポーネントと子コンポーネントは**パスの契約**によって結びつけられます。親は `data-wcs` 属性を使って外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態として通常通り読み書きを行うだけです:
930
-
931
- 1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
932
- 2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
933
- 3. 結合点は**パス名のみ**であるため、親と子は完全に疎結合な状態を保ち、それぞれ独立してテスト可能です。
934
- 4. 実行コストは、パスの解決(初回アクセス後はキャッシュされるため O(1) で動作します)と、依存グラフを通じた変更の伝播のみです。
935
-
936
- これは、コンポーネントレベルの複雑な抽象化ではなく、「パスの解決」に基づいたコンポーネント間状態管理への軽量なアプローチです。
937
-
938
- ### コンポーネント定義(Shadow DOM)
939
-
940
- ```javascript
941
- class MyComponent extends HTMLElement {
942
- state = { message: "" };
943
-
944
- constructor() {
945
- super();
946
- this.attachShadow({ mode: "open" });
947
- this.shadowRoot.innerHTML = `
948
- <wcs-state bind-component="state"></wcs-state>
949
- <div>{{ message }}</div>
950
- <input type="text" data-wcs="value: message" />
951
- `;
952
- }
953
- }
954
- customElements.define("my-component", MyComponent);
955
- ```
956
-
957
- ### コンポーネント定義(Light DOM)
958
-
959
- Light DOM コンポーネントは Shadow DOM を使用しません。CSS と同様に state の名前空間も上位スコープと共有されるため、`name` 属性が必須です。
960
-
961
- ```javascript
962
- class MyLightComponent extends HTMLElement {
963
- state = { message: "" };
964
-
965
- connectedCallback() {
966
- this.innerHTML = `
967
- <wcs-state bind-component="state" name="my-light"></wcs-state>
968
- <div data-wcs="text: message@my-light"></div>
969
- <input type="text" data-wcs="value: message@my-light" />
970
- `;
971
- }
972
- }
973
- customElements.define("my-light-component", MyLightComponent);
974
- ```
975
-
976
- - Light DOM コンポーネントでは `name` 属性が**必須**です(名前空間が上位スコープと共有されるため)
977
- - バインディングでは `@my-light` のように状態名を明示的に参照する必要があります
978
- - `<wcs-state>` はコンポーネント要素の直下に配置する必要があります
979
-
980
- ### ホスト側の使用方法
981
-
982
- ```html
983
- <wcs-state>
984
- <script type="module">
985
- export default {
986
- user: { name: "Alice" }
987
- };
988
- </script>
989
- </wcs-state>
990
-
991
- <!-- コンポーネントの state.message を外側の user.name にバインド -->
992
- <my-component data-wcs="state.message: user.name"></my-component>
993
- ```
994
-
995
- - `bind-component="state"` でコンポーネントの `state` プロパティを `<wcs-state>` にマッピング
996
- - `data-wcs="state.message: user.name"` でホスト要素上の外部状態パスを内部コンポーネント状態プロパティにバインド
997
- - 変更はコンポーネントと外部状態間で双方向に伝播
998
-
999
- ### 独立した Web Component への状態注入(`__e2e__/single-component`)
1000
-
1001
- ホストの外部状態に依存しないコンポーネントでも、`bind-component` で `state` を注入してリアクティブにできます。
1002
-
1003
- ```javascript
1004
- class MyComponent extends HTMLElement {
1005
- state = Object.freeze({
1006
- message: "Hello, World!"
1007
- });
1008
-
1009
- constructor() {
1010
- super();
1011
- this.attachShadow({ mode: "open" });
1012
- }
1013
-
1014
- connectedCallback() {
1015
- this.shadowRoot.innerHTML = `
1016
- <wcs-state bind-component="state"></wcs-state>
1017
- <div>{{ message }}</div>
1018
- `;
1019
- }
1020
-
1021
- async $stateReadyCallback(stateProp) {
1022
- console.log("state ready:", stateProp); // "state"
1023
- }
1024
- }
1025
- customElements.define("my-component", MyComponent);
1026
- ```
1027
-
1028
- - 初期 `state` は `Object.freeze(...)` で定義できます(注入後は書き換え可能なリアクティブ状態に置き換え)
1029
- - `bind-component="state"` により `this.state` が `@wcstack/state` の状態プロキシとして利用可能になります
1030
- - `this.state.message = "..."` のような代入で、Shadow DOM 内の `{{ message }}` が即時に更新されます
1031
- - `async $stateReadyCallback(stateProp)` は、Web Component 側で状態が利用可能になった直後に呼ばれます(`stateProp` は `bind-component` のプロパティ名)
1032
-
1033
- ### 制約事項
1034
-
1035
- - `bind-component` 付きの `<wcs-state>` はコンポーネント要素の**直下**(トップレベル)に配置すること
1036
- - 親要素は**カスタム要素**(ハイフンを含むタグ名)であること
1037
- - Light DOM コンポーネントでは `name` 属性が**必須**(上位スコープとの名前空間衝突を回避するため)
1038
- - Light DOM のバインディングでは状態名を明示的に参照すること(例: `@my-light`)
1039
-
1040
- ### ループ内でのコンポーネント使用
1041
-
1042
- ```html
1043
- <template data-wcs="for: users">
1044
- <my-component data-wcs="state.message: .name"></my-component>
1045
- </template>
1046
- ```
1047
-
1048
- ## 宣言的カスタムコンポーネント (DCC)
1049
-
1050
- JavaScript のクラス定義なしで、**HTML だけ**でカスタム要素を定義できます。`data-wc-definition` と Declarative Shadow DOM (`<template shadowrootmode>`) を使い、リアクティブな状態を持つ再利用可能なコンポーネントをインラインで宣言します。
1051
-
1052
- ### 基本的な定義
1053
-
1054
- ```html
1055
- <!-- 1. コンポーネントを定義(CSSで非表示) -->
1056
- <my-counter data-wc-definition>
1057
- <template shadowrootmode="open">
1058
- <p>{{ count }}</p>
1059
- <button data-wcs="onclick: increment">+1</button>
1060
- <wcs-state>
1061
- <script type="module">
1062
- export default {
1063
- count: 0,
1064
- increment() { this.count++; },
1065
- $bindables: ["count"]
1066
- };
1067
- </script>
1068
- </wcs-state>
1069
- </template>
1070
- </my-counter>
1071
-
1072
- <!-- 2. 使う — 各インスタンスが独自の状態を持つ -->
1073
- <my-counter></my-counter>
1074
- <my-counter></my-counter>
1075
- ```
1076
-
1077
- `<wcs-state>` が `data-wc-definition` 付きのホスト内にあることを検出すると:
1078
-
1079
- 1. 状態オブジェクトをロード(`<script type="module">` または `src="*.js"`)
1080
- 2. getter/setter/メソッドをプロトタイプに定義したカスタム要素クラスを生成
1081
- 3. `customElements.define()` で登録
1082
-
1083
- 定義要素は非表示になり、各インスタンスはテンプレートを自身の Shadow DOM にクローンして、独自の `<wcs-state>` を初期化します。
1084
-
1085
- ### 推奨 CSS
1086
-
1087
- ```css
1088
- :not(:defined) { display: none; }
1089
- [data-wc-definition] { display: none; }
1090
- ```
1091
-
1092
- ### `$bindables` と wc-bindable プロトコル
1093
-
1094
- `$bindables` 配列は、変更イベント付きのコンポーネントプロパティとして公開する状態プロパティを宣言します。[wc-bindable プロトコル](https://github.com/nicenemo/nicenemo/blob/main/docs/wc-bindable-protocol.md)に準拠しています:
1095
-
1096
- ```javascript
1097
- export default {
1098
- count: 0,
1099
- increment() { this.count++; },
1100
- $bindables: ["count"]
1101
- };
1102
- ```
1103
-
1104
- これにより以下が生成されます:
1105
-
1106
- - クラスの `static wcBindable` — フレームワークアダプタ用のプロトコルメタデータ
1107
- - プロトタイプの getter/setter — リアクティブプロキシ経由で読み書き
1108
- - `CustomEvent` のディスパッチ — 値が変更されるたびに `my-counter:count-changed` が発火
1109
-
1110
- ### DCC プロパティへのバインディング
1111
-
1112
- 他の `<wcs-state>` インスタンスから、通常の Web Component と同じように DCC プロパティにバインドできます:
1113
-
1114
- ```html
1115
- <my-counter data-wcs="count: parentCount"></my-counter>
1116
-
1117
- <wcs-state>
1118
- <script type="module">
1119
- export default { parentCount: 0 };
1120
- </script>
1121
- </wcs-state>
1122
- <div data-wcs="textContent: parentCount"></div>
1123
- ```
1124
-
1125
- ### Shadow Root モード
1126
-
1127
- `open` と `closed` の両モードに対応しています:
1128
-
1129
- ```html
1130
- <my-component data-wc-definition>
1131
- <template shadowrootmode="closed">
1132
- <!-- closed shadow DOM -->
1133
- </template>
1134
- </my-component>
1135
- ```
1136
-
1137
- ### 内部プロパティ
1138
-
1139
- `$` プレフィックス付きのプロパティは内部用で、コンポーネントのプロトタイプには公開されません:
1140
-
1141
- | プロパティ | 用途 |
1142
- |----------|---------|
1143
- | `$bindables` | 観測可能プロパティの宣言 |
1144
- | `$connectedCallback` | ライフサイクルフック(各インスタンスで実行) |
1145
- | `$disconnectedCallback` | クリーンアップフック |
1146
- | `$updatedCallback` | 状態変更後に呼ばれる |
1147
-
1148
- ## SVG サポート
1149
-
1150
- 全てのバインディングが `<svg>` 要素内で動作します。SVG 属性には `attr.*` を使用します:
1151
-
1152
- ```html
1153
- <svg width="200" height="100">
1154
- <template data-wcs="for: points">
1155
- <circle data-wcs="attr.cx: .x; attr.cy: .y; attr.fill: .color" r="5" />
1156
- </template>
1157
- </svg>
1158
- ```
1159
-
1160
- ## ライフサイクルフック
1161
-
1162
- 状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` を定義すると、初期化・クリーンアップ・更新時のフックとして利用できます。
1163
-
1164
- ```html
1165
- <wcs-state>
1166
- <script type="module">
1167
- export default {
1168
- timer: null,
1169
- count: 0,
1170
-
1171
- // <wcs-state> が DOM に接続された時に呼ばれる
1172
- async $connectedCallback() {
1173
- const res = await fetch("/api/initial-count");
1174
- this.count = await res.json();
1175
- this.timer = setInterval(() => { this.count++; }, 1000);
1176
- },
1177
-
1178
- // <wcs-state> が DOM から切断された時に呼ばれる(同期のみ)
1179
- $disconnectedCallback() {
1180
- clearInterval(this.timer);
1181
- }
1182
- };
1183
- </script>
1184
- </wcs-state>
1185
- ```
1186
-
1187
- | フック | タイミング | 非同期 |
1188
- |---|---|---|
1189
- | `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
1190
- | `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
1191
- | `$updatedCallback(paths, indexesListByPath)` | 状態変更が適用された後に呼び出し | 可(await されない) |
1192
-
1193
- `$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
1194
-
1195
- - フック内の `this` は読み書き可能な状態プロキシです。
1196
- - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
1197
- - `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
1198
- - `$updatedCallback(paths, indexesListByPath)` は更新された状態パスの一覧を受け取ります。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。`async` を使用できますが、戻り値は await されません。
1199
- - Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
1200
-
1201
- ## 設定
1202
-
1203
- `bootstrapState()` に部分的な設定オブジェクトを渡します:
1204
-
1205
- ```javascript
1206
- import { bootstrapState } from '@wcstack/state';
1207
-
1208
- bootstrapState({
1209
- locale: 'ja-JP',
1210
- debug: true,
1211
- enableMustache: false,
1212
- tagNames: { state: 'my-state' },
1213
- });
1214
- ```
1215
-
1216
- 全オプションとデフォルト値:
1217
-
1218
- | オプション | デフォルト | 説明 |
1219
- |---|---|---|
1220
- | `bindAttributeName` | `'data-wcs'` | バインディング属性名 |
1221
- | `tagNames.state` | `'wcs-state'` | 状態要素のタグ名 |
1222
- | `locale` | `'en'` | フィルタのデフォルトロケール |
1223
- | `debug` | `false` | デバッグモード |
1224
- | `enableMustache` | `true` | `{{ }}` 構文の有効化 |
1225
-
1226
- ## TypeScript サポート
1227
-
1228
- `defineState()` で状態オブジェクトをラップすると、メソッドや getter 内の `this` に型補完が効きます。ランタイムコストはゼロ(アイデンティティ関数)です。
1229
-
1230
- ```typescript
1231
- import { defineState } from '@wcstack/state';
1232
-
1233
- export default defineState({
1234
- count: 0,
1235
- users: [] as { name: string; age: number }[],
1236
-
1237
- increment() {
1238
- this.count++; // ✅ number
1239
- this["users.*.name"]; // ✅ string(ドットパス型解決)
1240
- this.$getAll("users.*.age", []); // ✅ API メソッド
1241
- },
1242
-
1243
- get "users.*.ageCategory"() {
1244
- return this["users.*.age"] < 25 ? "Young" : "Adult";
1245
- }
1246
- });
1247
- ```
1248
-
1249
- ユーティリティ型 `WcsPaths<T>` と `WcsPathValue<T, P>` もエクスポートされます。詳細は [docs/define-state.ja.md](docs/define-state.ja.md) を参照してください。
1250
-
1251
- ## API リファレンス
1252
-
1253
- ### `bootstrapState()`
1254
-
1255
- 状態システムを初期化します。`<wcs-state>` カスタム要素を登録し、DOM コンテンツ読み込みハンドラを設定します。
1256
-
1257
- ```javascript
1258
- import { bootstrapState } from '@wcstack/state';
1259
- bootstrapState();
1260
- ```
1261
-
1262
- ### `<wcs-state>` 要素
1263
-
1264
- | 属性 | 説明 |
1265
- |---|---|
1266
- | `name` | 状態名(デフォルト: `"default"`) |
1267
- | `state` | `<script type="application/json">` 要素の ID |
1268
- | `src` | `.json` または `.js` ファイルの URL |
1269
- | `json` | インライン JSON 文字列 |
1270
- | `bind-component` | Web Component バインディングのプロパティ名 |
1271
-
1272
- ### IStateElement
1273
-
1274
- | プロパティ / メソッド | 説明 |
1275
- |---|---|
1276
- | `name` | 状態名 |
1277
- | `initializePromise` | 状態の完全な初期化時に解決される Promise |
1278
- | `listPaths` | `for` ループで使用されるパスの Set |
1279
- | `getterPaths` | getter として定義されたパスの Set |
1280
- | `setterPaths` | setter として定義されたパスの Set |
1281
- | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
1282
- | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
1283
- | `setInitialState(state)` | プログラムから状態を設定(初期化前) |
1284
- | `bindProperty(prop, descriptor)` | 生の状態オブジェクトにプロパティを定義 |
1285
- | `nextVersion()` | バージョン番号をインクリメントして返す |
1286
-
1287
- ## アーキテクチャ
1288
-
1289
- ```
1290
- bootstrapState()
1291
- └── registerComponents() // <wcs-state> カスタム要素を登録
1292
-
1293
- <wcs-state> connectedCallback
1294
- ├── _initializeBindWebComponent() // bind-component: 親コンポーネントから状態を取得
1295
- ├── _initialize() // 状態をロード (state属性 / src / json / script / API)
1296
- │ └── setStateElementByName() // WeakMap<Node, Map<name, element>> に登録
1297
- │ └── (rootNode への初回登録時)
1298
- │ └── queueMicrotask → buildBindings()
1299
- ├── _callStateConnectedCallback() // $connectedCallback が定義されていれば呼び出し
1300
-
1301
- buildBindings(root)
1302
- ├── waitForStateInitialize() // 全 <wcs-state> の initializePromise を待機
1303
- ├── convertMustacheToComments() // {{ }} → コメントノードに変換
1304
- ├── collectStructuralFragments() // for/if テンプレートを収集
1305
- └── initializeBindings() // DOM 走査、data-wcs 解析、バインディング設定
1306
- ```
1307
-
1308
- ### リアクティビティフロー
1309
-
1310
- 1. Proxy の `set` トラップによる状態変更 → `setByAddress()`
1311
- 2. アドレス解決 → updater が絶対アドレスをキューに登録
1312
- 3. 依存関係ウォーカーが下流のキャッシュを無効化(dirty)
1313
- 4. updater が `applyChangeFromBindings()` によりバインド済み DOM ノードに変更を適用
1314
-
1315
- ### 状態アドレスシステム
1316
-
1317
- `users.*.name` のようなパスは以下に分解されます:
1318
-
1319
- - **PathInfo** — 静的パスメタデータ(セグメント、ワイルドカード数、親パス)
1320
- - **ListIndex** — ランタイムループインデックスチェーン
1321
- - **StateAddress** — PathInfo + ListIndex の組み合わせ
1322
- - **AbsoluteStateAddress** — 状態名 + StateAddress(クロス状態参照用)
1323
-
1324
- ## サーバーサイドレンダリング
1325
-
1326
- `@wcstack/state` は [`@wcstack/server`](../server/) パッケージと連携して SSR をサポートしています。クライアント用に書いたテンプレートがそのままサーバーでレンダリングされます — 変更不要。
1327
-
1328
- ### クイックセットアップ
1329
-
1330
- 1. `<wcs-state>` に `enable-ssr` を追加:
1331
-
1332
- ```html
1333
- <wcs-state enable-ssr>
1334
- <script type="module">
1335
- export default {
1336
- items: [],
1337
- async $connectedCallback() {
1338
- const res = await fetch("/api/items");
1339
- this.items = await res.json();
1340
- }
1341
- };
1342
- </script>
1343
- </wcs-state>
1344
- <template data-wcs="for: items">
1345
- <div data-wcs="textContent: items.*.name"></div>
1346
- </template>
1347
- ```
1348
-
1349
- 2. サーバーでレンダリング:
1350
-
1351
- ```javascript
1352
- import { renderToString } from "@wcstack/server";
1353
-
1354
- const html = await renderToString(template, {
1355
- baseUrl: "http://localhost:3000"
1356
- });
1357
- ```
1358
-
1359
- これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を自動検出し、JSON スナップショットから状態を復元し��再レンダリングなしでリアクティビティを再開します。
1360
-
1361
- ### 仕組み
1362
-
1363
- | フェーズ | 動作 |
1364
- |---------|------|
1365
- | **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用、ハイドレーションデータを含む `<wcs-ssr>` 要素付きのレンダリング済み HTML を出力 |
1366
- | **クライアント** | `<wcs-state enable-ssr>` が `<wcs-ssr>` の JSON から状態をロード、`$connectedCallback` をスキップ、`hydrateBindings()` が既存の DOM にリアクティビティを接続 |
1367
- | **フォールバック** | ���ーバー/クライアントのバージョン不一致時、SSR DOM をクリーンアップして `buildBindings()` でフルクライアントサイドレンダリングを実行 |
1368
-
1369
- ### `enable-ssr` の動作
1370
-
1371
- | コンテキスト | 動作 |
1372
- |------------|------|
1373
- | **サーバー**(`renderToString`) | 状態 JSON、テンプレートフラグメント、プロパティデータを含む `<wcs-ssr>` を生成 |
1374
- | **クラ��アント**(ハイドレーション) | `<wcs-ssr>` を読み取り、状態を復元、`$connectedCallback` をスキップ、既存 DOM のバイン���ィングをハイドレート |
1375
-
1376
- API の詳細は [`@wcstack/server` README](../server/README.ja.md) を参照してください。
1377
-
1378
- ## ライセンス
1379
-
1380
- MIT
1
+ # @wcstack/state
2
+
3
+ **これは便利な既存FWの別実装ではありません。フロントエンド開発の前提を組み替える、別系譜の試みです。**
4
+
5
+ 多くのライブラリは、UI・状態・コンポーネントの結合点を JavaScript の中に置きます。`@wcstack/state` はそこを選びません。仮想DOMも、コンパイルも、hook も、selector も前提にせず、HTML とパス文字列だけを契約として UI と状態を結びつけます。
6
+
7
+ それが `<wcs-state>` と `data-wcs` のアプローチです。CDNからの読み込みだけで動作し、依存パッケージはゼロ、構文はHTMLそのままです。CDNのスクリプトはカスタム要素の定義を登録するだけで、ロード時にはそれ以外の処理は走りません。`<wcs-state>` 要素がDOMに接続されたときにはじめて、状態ソースを読み取り、同一ルートノード(`document` または `ShadowRoot`)内の `data-wcs` バインディングを走査してリアクティビティを構築します。初期化プロセスはすべて要素のライフサイクルによって駆動されるため、独自の初期化コードを書く必要はありません。
8
+
9
+ ## ここには存在しないもの
10
+
11
+ 以下は未実装ではありません。**設計上、存在しません。**
12
+
13
+ - 変数を取り出す API
14
+ - 要素ごとに状態を束縛するオブジェクト
15
+ - hook
16
+ - selector
17
+ - reactive primitive をコンポーネントへ引き込むための glue code
18
+
19
+ None of these exist by design.
20
+
21
+ なぜなら、このライブラリでは UI と状態の結合点を JavaScript の中に置かないからです。状態を「取り出して」コンポーネントへ渡すのではなく、HTML 側がパス文字列によって状態を参照します。要素は状態を所有せず、状態も要素を知りません。両者が共有するのはパスだけです。
22
+
23
+ ## 既存FWとは比較しません
24
+
25
+ これは React / Vue / Solid と同じ問題を別の方法で解いているのではありません。**前提自体が違います。**
26
+
27
+ | 一般的なFWが前提にするもの | `@wcstack/state` が前提にするもの |
28
+ |---|---|
29
+ | コンポーネントが UI と状態の結合点 | パス文字列が UI と状態の結合点 |
30
+ | JavaScript が描画の中心 | HTML と DOM が中心 |
31
+ | state を取り出して component へ流し込む | path を宣言して DOM を状態へ接続する |
32
+ | hook / selector / signal で購読する | 属性とパスで束縛する |
33
+ | フレームワークの実行モデルにアプリ全体を載せる | ブラウザ標準の上に薄い reactive layer を足す |
34
+
35
+ 比較表を作るより先に、この前提差を理解してください。同じ棚に置いても、解いている問題の切り取り方が違います。
36
+
37
+ ## 第一原理: パスが唯一の契約
38
+
39
+ 既存の多くのフレームワークでは、**コンポーネント**がUIと状態の結合点になっています。状態ストアを外部に切り出しても、コンポーネント内にフックやセレクタ、リアクティブプリミティブといった**状態を引き込むためのコード**が必ず必要になります。つまり、UIと状態は常にJavaScriptの中で結びついているのです。
40
+
41
+ `@wcstack/state` はこの結合を完全に排除しました。UIと状態を結びつけているのは**パス文字列**だけです — `user.name` や `cart.items.*.subtotal` のようなドット区切りのアドレスのみが、2つのレイヤー間の唯一の契約(コントラクト)になります:
42
+
43
+ | レイヤー | 知っていること | 知らないこと |
44
+ |----------|----------------|--------------|
45
+ | **状態** (`<wcs-state>`) | データ構造とビジネスロジック | どのDOM要素がバインドされているか |
46
+ | **UI** (`data-wcs`) | パス文字列と表示意図 | 状態がどう保存・算出されているか |
47
+ | **コンポーネント** (`@name`) | 名前付き状態から必要なパス | 他コンポーネントの内部実装 |
48
+
49
+ 3つのレベルのパス契約が疎結合を実現しています:
50
+
51
+ 1. **UI ↔ 状態** — `data-wcs="textContent: user.name"` という属性がバインディングのすべてです。フックもセレクタもリアクティブプリミティブもありません。コンポーネントのJavaScriptには、状態を参照するコードが**一行も**必要ありません。
52
+
53
+ 2. **コンポーネント ↔ コンポーネント** — コンポーネント間の通信は、名前付き状態の参照(`@stateName`)で行われます。コンポーネント同士がお互いを直接インポートしたり参照したりすることはありません。共有するのはパスの命名規約だけです。
54
+
55
+ 3. **ループコンテキスト** — `for` ループ内では `*` が抽象インデックスとして機能します。`items.*.price` のようなバインディングは自動的に現在の要素へと解決されます。テンプレートは自身の具体的な位置(インデックス)を知る必要がなく、ワイルドカードがその契約となります。
56
+
57
+ ### なぜこれが重要なのか
58
+
59
+ これはUIと状態の完全な分離を、**JavaScriptのコードを介することなく**実現していることを意味します。つまり:
60
+
61
+ - UIをすべて作り直しても、状態のロジックに触れる必要がありません。
62
+ - 状態のデータ構造をリファクタリングしても、パス文字列の更新だけで済みます。
63
+ - HTMLを読むだけで、すべてのデータ依存関係を把握できます。
64
+
65
+ このパスによる契約は、REST APIのURLと同じ発想です — 両者が合意するシンプルな文字列だけが存在し、そこに共有するコードはありません。これはJavaScriptの上に独自のテンプレート言語を発明するのではなく、HTML本来の宣言的な性質をフルに活かした結果として生まれた設計です。
66
+
67
+ 以下の機能はすべて、この原理の帰結です。機能が先にあり、その説明として哲学を後付けしているのではありません。
68
+
69
+ ## わずか4ステップで動作
70
+
71
+ ```html
72
+ <!-- 1. CDN を読み込む -->
73
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
74
+
75
+ <!-- 2. <wcs-state> タグを書く -->
76
+ <wcs-state>
77
+ <!-- 3. 状態オブジェクトを定義する -->
78
+ <script type="module">
79
+ export default {
80
+ message: "Hello, World!"
81
+ };
82
+ </script>
83
+ </wcs-state>
84
+
85
+ <!-- 4. data-wcs 属性でバインドする -->
86
+ <div data-wcs="textContent: message"></div>
87
+ ```
88
+
89
+ これだけです。ビルドツールも、初期化コードも、重いフレームワークも必要ありません。
90
+
91
+ ## この原理から導かれる機能
92
+
93
+ - **宣言的データバインディング** — `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
94
+ - **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
95
+ - **構造ディレクティブ** — `<template>` 要素による `for`, `if` / `elseif` / `else`
96
+ - **組み込みフィルタ** — フォーマット、比較、算術、日付など 40 種類
97
+ - **双方向バインディング** — `<input>`, `<select>`, `<textarea>` で自動有効
98
+ - **Web Component バインディング** — Shadow DOM コンポーネントとの双方向状態バインディング
99
+ - **パス getter** — ドットパスキー getter(`get "users.*.fullName"()`)によるデータツリーの任意の深さへのフラットな仮想プロパティ定義、自動依存追跡・キャッシュ
100
+ - **Mustache 構文** — テキストノードでの `{{ path|filter }}`
101
+ - **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
102
+ - **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
103
+ - **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`、Web Component 用 `$stateReadyCallback`
104
+ - **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))
105
+ - **サーバーサイドレンダリング** — `enable-ssr` 属性 + `@wcstack/server` でフル SSR と自動ハイドレーション
106
+ - **依存ゼロ** — ランタイム依存なし
107
+
108
+ ## インストール
109
+
110
+ ### CDN(推奨)
111
+
112
+ ```html
113
+ <!-- 自動初期化 — これだけで動作します -->
114
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
115
+ ```
116
+
117
+ ### CDN(手動初期化)
118
+
119
+ ```html
120
+ <script type="module">
121
+ import { bootstrapState } from 'https://esm.run/@wcstack/state';
122
+ bootstrapState();
123
+ </script>
124
+ ```
125
+
126
+ ## 基本的な使い方
127
+
128
+ ```html
129
+ <wcs-state>
130
+ <script type="module">
131
+ export default {
132
+ count: 0,
133
+ user: { id: 1, name: "Alice" },
134
+ users: [
135
+ { id: 1, name: "Alice" },
136
+ { id: 2, name: "Bob" },
137
+ { id: 3, name: "Charlie" }
138
+ ],
139
+ countUp() { this.count += 1; },
140
+ clearCount() { this.count = 0; },
141
+ get "users.*.displayName"() {
142
+ return this["users.*.name"] + " (ID: " + this["users.*.id"] + ")";
143
+ }
144
+ };
145
+ </script>
146
+ </wcs-state>
147
+
148
+ <!-- テキストバインディング -->
149
+ <div data-wcs="textContent: count"></div>
150
+ {{ count }}
151
+
152
+ <!-- 双方向入力バインディング -->
153
+ <input type="text" data-wcs="value: user.name">
154
+
155
+ <!-- イベントバインディング -->
156
+ <button data-wcs="onclick: countUp">Increment</button>
157
+
158
+ <!-- 条件付きクラス -->
159
+ <div data-wcs="textContent: count; class.over: count|gt(10)"></div>
160
+
161
+ <!-- ループ -->
162
+ <template data-wcs="for: users">
163
+ <div>
164
+ <span data-wcs="textContent: .id"></span>:
165
+ <span data-wcs="textContent: .displayName"></span>
166
+ </div>
167
+ </template>
168
+
169
+ <!-- 条件分岐レンダリング -->
170
+ <template data-wcs="if: count|gt(0)">
171
+ <p>カウントは正の値です。</p>
172
+ </template>
173
+ <template data-wcs="elseif: count|lt(0)">
174
+ <p>カウントは負の値です。</p>
175
+ </template>
176
+ <template data-wcs="else:">
177
+ <p>カウントはゼロです。</p>
178
+ </template>
179
+ ```
180
+
181
+ ## 状態の初期化
182
+
183
+ `<wcs-state>` は複数の方法で初期状態を読み込めます:
184
+
185
+ ```html
186
+ <!-- 1. <script type="application/json"> を id で参照 -->
187
+ <script type="application/json" id="state">
188
+ { "count": 0 }
189
+ </script>
190
+ <wcs-state state="state"></wcs-state>
191
+
192
+ <!-- 2. インライン JSON 属性 -->
193
+ <wcs-state json='{ "count": 0 }'></wcs-state>
194
+
195
+ <!-- 3. 外部 JSON ファイル -->
196
+ <wcs-state src="./data.json"></wcs-state>
197
+
198
+ <!-- 4. 外部 JS モジュール (export default { ... }) -->
199
+ <wcs-state src="./state.js"></wcs-state>
200
+
201
+ <!-- 5. インラインスクリプトモジュール -->
202
+ <wcs-state>
203
+ <script type="module">
204
+ export default { count: 0 };
205
+ </script>
206
+ </wcs-state>
207
+
208
+ <!-- 6. プログラム API -->
209
+ <script>
210
+ const el = document.createElement('wcs-state');
211
+ el.setInitialState({ count: 0 });
212
+ document.body.appendChild(el);
213
+ </script>
214
+ ```
215
+
216
+ 解決順序: `state` → `src` (.json / .js) → `json` → 内包 `<script>` → `setInitialState()` 待機。
217
+
218
+ ### 名前付き状態
219
+
220
+ 複数の状態要素を `name` 属性で共存できます。バインディングでは `@name` で参照します:
221
+
222
+ ```html
223
+ <wcs-state name="cart">...</wcs-state>
224
+ <wcs-state name="user">...</wcs-state>
225
+
226
+ <div data-wcs="textContent: total@cart"></div>
227
+ <div data-wcs="textContent: name@user"></div>
228
+ ```
229
+
230
+ デフォルト名は `"default"`(`@` 不要)です。
231
+
232
+ ## 状態の更新
233
+
234
+ `@wcstack/state` では、すべての状態は**パス**を持ちます — `count`、`user.name`、`items` のように。状態をリアクティブに更新するには、**パスに代入**します:
235
+
236
+ ```javascript
237
+ this.count = 10; // パス "count"
238
+ this["user.name"] = "Bob"; // パス "user.name"
239
+ ```
240
+
241
+ ルールは1つだけです。**「パスに直接代入する」ことで、関連するDOMが自動的に更新されます。**
242
+
243
+ ### なぜ `this.user.name = "Bob"` ではDOMが更新されないのか
244
+
245
+ これは単なる制約ではなく、**契約境界が見えている箇所**です。
246
+
247
+ 通常のプロパティアクセスの書き方だと、まず `this.user` でプレーンな `user` オブジェクトを読み取り(パスの読み取り)、取得したオブジェクトの `.name` を直接書き換える挙動になります。これは「パスに対するプロパティ代入」という契約を通っていません。そのため、システム側は変更を検知しません:
248
+
249
+ ```javascript
250
+ // ✅ パスへの代入 — 変更が検知される
251
+ this["user.name"] = "Bob";
252
+
253
+ // ❌ パスへの代入ではない — 変更は検知されない
254
+ this.user.name = "Bob";
255
+ ```
256
+
257
+ `this.user.name = "Bob"` も動くようにすると、一見便利にはなります。しかしその瞬間に「UI と状態はパスだけで結ばれる」という原理が崩れます。どこで依存を追跡し、どこで更新を確定するかが曖昧になり、契約境界が失われます。
258
+
259
+ ### 配列
260
+
261
+ 配列についても全く同じルールが適用されます。常に**パスに対して新しい配列を代入**してください。`push` や `splice`、`sort` などの破壊的な配列メソッドは、パスへの代入を介さずに状態をその場で(in-placeに)書き換えてしまうため、変更が検知されません。代わりに、新しい配列を返す非破壊的なメソッドを使用します:
262
+
263
+ ```javascript
264
+ // ✅ 新しい配列をパスに代入 — 変更が検知される
265
+ this.items = this.items.concat({ id: 4, text: "New" });
266
+ this.items = this.items.toSpliced(index, 1);
267
+ this.items = this.items.filter(item => !item.done);
268
+ this.items = this.items.toSorted((a, b) => a.id - b.id);
269
+ this.items = this.items.toReversed();
270
+ this.items = this.items.with(index, newValue);
271
+
272
+ // ❌ その場での変更 — パスへの代入なし、変更は検知されない
273
+ this.items.push({ id: 4, text: "New" });
274
+ this.items.splice(index, 1);
275
+ this.items.sort((a, b) => a.id - b.id);
276
+ ```
277
+
278
+ ## バインディング構文
279
+
280
+ ### `data-wcs` 属性
281
+
282
+ ```
283
+ property[#modifier]: path[@state][|filter[|filter(args)...]]
284
+ ```
285
+
286
+ 複数バインディングは `;` で区切ります:
287
+
288
+ ```html
289
+ <div data-wcs="textContent: count; class.over: count|gt(10)"></div>
290
+ ```
291
+
292
+ | 要素 | 説明 | 例 |
293
+ |---|---|---|
294
+ | `property` | バインドする DOM プロパティ | `value`, `textContent`, `checked` |
295
+ | `#modifier` | バインディング修飾子 | `#ro`, `#prevent`, `#stop`, `#onchange` |
296
+ | `path` | 状態プロパティパス | `count`, `user.name`, `users.*.name` |
297
+ | `@state` | 名前付き状態の参照 | `@cart`, `@user` |
298
+ | `\|filter` | 変換フィルタチェーン | `\|gt(0)`, `\|round\|locale` |
299
+
300
+ ### プロパティ種別
301
+
302
+ | プロパティ | 説明 |
303
+ |---|---|
304
+ | `value` | 要素の値(input では双方向) |
305
+ | `checked` | チェックボックス / ラジオボタンの選択状態(双方向) |
306
+ | `textContent` | テキストコンテンツ |
307
+ | `text` | textContent のエイリアス |
308
+ | `html` | innerHTML |
309
+ | `class.NAME` | CSS クラスの切り替え |
310
+ | `style.PROP` | CSS スタイルプロパティの設定 |
311
+ | `attr.NAME` | 属性の設定(SVG 名前空間対応) |
312
+ | `radio` | ラジオボタングループバインディング(双方向) |
313
+ | `checkbox` | チェックボックスグループの配列バインディング(双方向) |
314
+ | `onclick`, `on*` | イベントハンドラバインディング |
315
+
316
+ ### 修飾子
317
+
318
+ | 修飾子 | 説明 |
319
+ |---|---|
320
+ | `#ro` | 読み取り専用 — 双方向バインディングを無効化 |
321
+ | `#prevent` | イベントハンドラで `event.preventDefault()` を呼び出す |
322
+ | `#stop` | イベントハンドラで `event.stopPropagation()` を呼び出す |
323
+ | `#onchange` | 双方向バインディングで `input` の代わりに `change` イベントを使用 |
324
+
325
+ ### 双方向バインディング
326
+
327
+ 以下の要素で自動的に有効化されます:
328
+
329
+ | 要素 | プロパティ | イベント |
330
+ |---|---|---|
331
+ | `<input type="checkbox/radio">` | `checked` | `input` |
332
+ | `<input>`(その他の type) | `value`, `valueAsNumber`, `valueAsDate` | `input` |
333
+ | `<select>` | `value` | `change` |
334
+ | `<textarea>` | `value` | `input` |
335
+
336
+ `<input type="button">` は除外されます。`#ro` で無効化、`#onchange` でイベントを変更できます。
337
+
338
+ ### ラジオボタンバインディング
339
+
340
+ `radio` でラジオボタングループを単一の状態値にバインドします:
341
+
342
+ ```html
343
+ <input type="radio" value="red" data-wcs="radio: selectedColor">
344
+ <input type="radio" value="blue" data-wcs="radio: selectedColor">
345
+ ```
346
+
347
+ 状態値と一致する `value` を持つラジオボタンが自動的にチェックされます。ユーザーが別のラジオボタンを選択すると、状態が更新されます。`#ro` で読み取り専用にできます。
348
+
349
+ `for` ループ内での使用:
350
+
351
+ ```html
352
+ <template data-wcs="for: branches">
353
+ <label>
354
+ <input type="radio" data-wcs="value: .; radio: currentBranch">
355
+ {{ . }}
356
+ </label>
357
+ </template>
358
+ ```
359
+
360
+ ### チェックボックスバインディング
361
+
362
+ `checkbox` でチェックボックスグループを状態配列にバインドします:
363
+
364
+ ```html
365
+ <input type="checkbox" value="apple" data-wcs="checkbox: selectedFruits">
366
+ <input type="checkbox" value="banana" data-wcs="checkbox: selectedFruits">
367
+ <input type="checkbox" value="orange" data-wcs="checkbox: selectedFruits">
368
+ ```
369
+
370
+ チェックボックスの `value` が状態配列に含まれている場合にチェック状態になります。チェックボックスの切り替えで配列への値の追加・削除が行われます。`|int` で文字列値を数値に変換、`#ro` で読み取り専用にできます。
371
+
372
+ ### Mustache 構文
373
+
374
+ `enableMustache` が `true`(デフォルト)の場合、テキストノードで `{{ expression }}` が使用できます:
375
+
376
+ ```html
377
+ <p>こんにちは、{{ user.name }}さん!</p>
378
+ <p>カウント: {{ count|locale }}</p>
379
+ ```
380
+
381
+ 内部的にはコメントベースのバインディング(`<!--@@:expression-->`)に変換されます。
382
+
383
+ ## 構造ディレクティブ
384
+
385
+ 構造ディレクティブは `<template>` 要素で使用します:
386
+
387
+ ### ループ (`for`)
388
+
389
+ ```html
390
+ <template data-wcs="for: users">
391
+ <div>
392
+ <!-- フルパス -->
393
+ <span data-wcs="textContent: users.*.name"></span>
394
+ <!-- 省略形(ループコンテキストからの相対パス) -->
395
+ <span data-wcs="textContent: .name"></span>
396
+ </div>
397
+ </template>
398
+ ```
399
+
400
+ `for:` ディレクティブは**値ベースの差分アルゴリズム**を使用します。配列の各要素の値そのものが識別キーとして機能するため、React の `key` や Vue の `:key` のような明示的なキー属性は不要です。配列が再代入されると、差分アルゴリズムが新旧の要素を値で照合し、変更のない要素の DOM ノードを再利用しつつ、追加・削除・並び替えを効率的に処理します。
401
+
402
+ #### ドット省略記法
403
+
404
+ `for` ループ内では、`.` で始まるパスがループの配列パスを基準に展開されます:
405
+
406
+ | 省略形 | 展開後 | 説明 |
407
+ |---|---|---|
408
+ | `.name` | `users.*.name` | 現在の要素のプロパティ |
409
+ | `.` | `users.*` | 現在の要素そのもの |
410
+ | `.name\|uc` | `users.*.name\|uc` | フィルタは保持される |
411
+ | `.name@state` | `users.*.name@state` | 状態名は保持される |
412
+
413
+ プリミティブ配列では、`.` が要素の値を直接参照します:
414
+
415
+ ```html
416
+ <template data-wcs="for: branches">
417
+ <label>
418
+ <input type="radio" data-wcs="value: .; radio: currentBranch">
419
+ {{ . }}
420
+ </label>
421
+ </template>
422
+ ```
423
+
424
+ 多重ワイルドカードによるネストループに対応しています。ネストされた `for` ディレクティブの `.` 省略記法も親ループのパスを基準に展開されます:
425
+
426
+ ```html
427
+ <template data-wcs="for: regions">
428
+ <!-- .states → regions.*.states -->
429
+ <template data-wcs="for: .states">
430
+ <!-- .name → regions.*.states.*.name -->
431
+ <span data-wcs="textContent: .name"></span>
432
+ </template>
433
+ </template>
434
+ ```
435
+
436
+ ### 条件分岐 (`if` / `elseif` / `else`)
437
+
438
+ ```html
439
+ <template data-wcs="if: count|gt(0)">
440
+ <p>正の値</p>
441
+ </template>
442
+ <template data-wcs="elseif: count|lt(0)">
443
+ <p>負の値</p>
444
+ </template>
445
+ <template data-wcs="else:">
446
+ <p>ゼロ</p>
447
+ </template>
448
+ ```
449
+
450
+ 条件をチェーンできます。`elseif` は前の条件を自動的に反転します。
451
+
452
+ ## パス getter(算出プロパティ)
453
+
454
+ **パス getter** は `@wcstack/state` の中核機能です。JavaScript の getter に**ドットパス文字列キー**とワイルドカード(`*`)を使って定義します。**データツリーの任意の深さに仮想プロパティを追加でき、すべてを1箇所にフラットに定義できます**。データのネストがどれほど深くても、定義側は同じレベルに並び、ループ要素ごとの自動依存追跡が機能します。
455
+
456
+ ### 基本的なパス getter
457
+
458
+ ```html
459
+ <wcs-state>
460
+ <script type="module">
461
+ export default {
462
+ users: [
463
+ { id: 1, firstName: "Alice", lastName: "Smith" },
464
+ { id: 2, firstName: "Bob", lastName: "Jones" }
465
+ ],
466
+ // パス getter — ループ内で要素ごとに実行
467
+ get "users.*.fullName"() {
468
+ return this["users.*.firstName"] + " " + this["users.*.lastName"];
469
+ },
470
+ get "users.*.displayName"() {
471
+ return this["users.*.fullName"] + " (ID: " + this["users.*.id"] + ")";
472
+ }
473
+ };
474
+ </script>
475
+ </wcs-state>
476
+
477
+ <template data-wcs="for: users">
478
+ <div data-wcs="textContent: .displayName"></div>
479
+ </template>
480
+ <!-- 出力:
481
+ Alice Smith (ID: 1)
482
+ Bob Jones (ID: 2)
483
+ -->
484
+ ```
485
+
486
+ パス getter 内の `this["users.*.firstName"]` は、手動でインデックスを指定することなく、自動的に現在のループ要素に解決されます。
487
+
488
+ ### トップレベル算出プロパティ
489
+
490
+ ワイルドカードなしの getter は通常の算出プロパティとして動作します:
491
+
492
+ ```javascript
493
+ export default {
494
+ price: 100,
495
+ tax: 0.1,
496
+ get total() {
497
+ return this.price * (1 + this.tax);
498
+ }
499
+ };
500
+ ```
501
+
502
+ ### getter のチェーン
503
+
504
+ パス getter は他のパス getter を参照でき、依存チェーンを形成します。上流の値が変更されると、キャッシュは自動的に無効化されます:
505
+
506
+ ```html
507
+ <wcs-state>
508
+ <script type="module">
509
+ export default {
510
+ taxRate: 0.1,
511
+ cart: {
512
+ items: [
513
+ { productId: "P001", quantity: 2, unitPrice: 500 },
514
+ { productId: "P002", quantity: 1, unitPrice: 1200 }
515
+ ]
516
+ },
517
+ // アイテムごとの小計
518
+ get "cart.items.*.subtotal"() {
519
+ return this["cart.items.*.unitPrice"] * this["cart.items.*.quantity"];
520
+ },
521
+ // 集計: 全小計の合計
522
+ get "cart.totalPrice"() {
523
+ return this.$getAll("cart.items.*.subtotal", []).reduce((sum, v) => sum + v, 0);
524
+ },
525
+ // チェーン: totalPrice から税を算出
526
+ get "cart.tax"() {
527
+ return this["cart.totalPrice"] * this.taxRate;
528
+ },
529
+ // チェーン: 合計金額
530
+ get "cart.grandTotal"() {
531
+ return this["cart.totalPrice"] + this["cart.tax"];
532
+ }
533
+ };
534
+ </script>
535
+ </wcs-state>
536
+
537
+ <template data-wcs="for: cart.items">
538
+ <div>
539
+ <span data-wcs="textContent: .productId"></span>:
540
+ <span data-wcs="textContent: .subtotal|locale"></span>
541
+ </div>
542
+ </template>
543
+ <p>合計: <span data-wcs="textContent: cart.totalPrice|locale"></span></p>
544
+ <p>税: <span data-wcs="textContent: cart.tax|locale"></span></p>
545
+ <p>総合計: <span data-wcs="textContent: cart.grandTotal|locale"></span></p>
546
+ ```
547
+
548
+ 依存チェーン: `cart.grandTotal` → `cart.tax` → `cart.totalPrice` → `cart.items.*.subtotal` → `cart.items.*.unitPrice` / `cart.items.*.quantity`。アイテムの `unitPrice` や `quantity` を変更すると、チェーン全体が自動的に再計算されます。
549
+
550
+ ### ネストされたワイルドカード getter
551
+
552
+ ネストされた配列構造では複数のワイルドカードが使用できます:
553
+
554
+ ```html
555
+ <wcs-state>
556
+ <script type="module">
557
+ export default {
558
+ categories: [
559
+ {
560
+ name: "果物",
561
+ items: [
562
+ { name: "りんご", price: 150 },
563
+ { name: "バナナ", price: 100 }
564
+ ]
565
+ },
566
+ {
567
+ name: "野菜",
568
+ items: [
569
+ { name: "にんじん", price: 80 }
570
+ ]
571
+ }
572
+ ],
573
+ get "categories.*.items.*.label"() {
574
+ return this["categories.*.name"] + " / " + this["categories.*.items.*.name"];
575
+ }
576
+ };
577
+ </script>
578
+ </wcs-state>
579
+
580
+ <template data-wcs="for: categories">
581
+ <h3 data-wcs="textContent: .name"></h3>
582
+ <template data-wcs="for: .items">
583
+ <div data-wcs="textContent: .label"></div>
584
+ </template>
585
+ </template>
586
+ <!-- 出力:
587
+ 果物
588
+ 果物 / りんご
589
+ 果物 / バナナ
590
+ 野菜
591
+ 野菜 / にんじん
592
+ -->
593
+ ```
594
+
595
+ ### フラットな仮想プロパティ — ネストの深さに依存しない定義
596
+
597
+ パス getter の重要な利点は、**データのネストがどれほど深くても、すべての仮想プロパティを1箇所にフラットに定義できる**ことです。各ネストレベルに算出プロパティを持たせるためだけにコンポーネントを分割する必要がありません。
598
+
599
+ ```javascript
600
+ export default {
601
+ regions: [
602
+ { name: "関東", prefectures: [
603
+ { name: "東京", cities: [
604
+ { name: "渋谷", population: 230000, area: 15.11 },
605
+ { name: "新宿", population: 346000, area: 18.22 }
606
+ ]},
607
+ { name: "神奈川", cities: [
608
+ { name: "横浜", population: 3750000, area: 437.56 }
609
+ ]}
610
+ ]}
611
+ ],
612
+
613
+ // --- ネストの深さに関係なく、すべてフラットに定義 ---
614
+
615
+ // 市レベル — 仮想プロパティ
616
+ get "regions.*.prefectures.*.cities.*.density"() {
617
+ return this["regions.*.prefectures.*.cities.*.population"]
618
+ / this["regions.*.prefectures.*.cities.*.area"];
619
+ },
620
+ get "regions.*.prefectures.*.cities.*.label"() {
621
+ return this["regions.*.prefectures.*.name"] + " "
622
+ + this["regions.*.prefectures.*.cities.*.name"];
623
+ },
624
+
625
+ // 県レベル — 市からの集約
626
+ get "regions.*.prefectures.*.totalPopulation"() {
627
+ return this.$getAll("regions.*.prefectures.*.cities.*.population", [])
628
+ .reduce((a, b) => a + b, 0);
629
+ },
630
+
631
+ // 地方レベル — 県からの集約
632
+ get "regions.*.totalPopulation"() {
633
+ return this.$getAll("regions.*.prefectures.*.totalPopulation", [])
634
+ .reduce((a, b) => a + b, 0);
635
+ },
636
+
637
+ // トップレベル — 地方からの集約
638
+ get totalPopulation() {
639
+ return this.$getAll("regions.*.totalPopulation", [])
640
+ .reduce((a, b) => a + b, 0);
641
+ }
642
+ };
643
+ ```
644
+
645
+ 3階層のネスト、5つの仮想プロパティ — すべてが1つのフラットなオブジェクト内に並んで定義されています。各レベルは任意の深さの値を参照でき、`$getAll` による集約は下位から上位へ自然に流れます。コンポーネントベースのフレームワークでは、一般的に各ネストレベルに個別のコンポーネントを作成し、算出値をツリーの上位に渡す方法が採られます。パス getter は、すべての定義を1箇所にまとめるという異なるトレードオフを提供します。
646
+
647
+ ### getter の戻り値のサブプロパティへのアクセス
648
+
649
+ パス getter がオブジェクトを返す場合、ドットパスでそのサブプロパティにアクセスできます:
650
+
651
+ ```javascript
652
+ export default {
653
+ products: [
654
+ { id: "P001", name: "ウィジェット", price: 500, stock: 10 },
655
+ { id: "P002", name: "ガジェット", price: 1200, stock: 3 }
656
+ ],
657
+ cart: {
658
+ items: [
659
+ { productId: "P001", quantity: 2 },
660
+ { productId: "P002", quantity: 1 }
661
+ ]
662
+ },
663
+ get productByProductId() {
664
+ return new Map(this.products.map(p => [p.id, p]));
665
+ },
666
+ // 完全な product オブジェクトを返す
667
+ get "cart.items.*.product"() {
668
+ return this.productByProductId.get(this["cart.items.*.productId"]);
669
+ },
670
+ // 戻り値のサブプロパティにアクセス
671
+ get "cart.items.*.total"() {
672
+ return this["cart.items.*.product.price"] * this["cart.items.*.quantity"];
673
+ }
674
+ };
675
+ ```
676
+
677
+ `this["cart.items.*.product.price"]` は `cart.items.*.product` getter が返すオブジェクトを透過的にチェーンします。
678
+
679
+ ### パス setter
680
+
681
+ `set "path"()` でカスタム setter ロジックを定義できます:
682
+
683
+ ```javascript
684
+ export default {
685
+ users: [
686
+ { firstName: "Alice", lastName: "Smith" },
687
+ { firstName: "Bob", lastName: "Jones" }
688
+ ],
689
+ get "users.*.fullName"() {
690
+ return this["users.*.firstName"] + " " + this["users.*.lastName"];
691
+ },
692
+ set "users.*.fullName"(value) {
693
+ const [first, ...rest] = value.split(" ");
694
+ this["users.*.firstName"] = first;
695
+ this["users.*.lastName"] = rest.join(" ");
696
+ }
697
+ };
698
+ ```
699
+
700
+ ```html
701
+ <template data-wcs="for: users">
702
+ <input type="text" data-wcs="value: .fullName">
703
+ </template>
704
+ ```
705
+
706
+ パス setter は双方向バインディングと連携します — input を編集すると setter が呼ばれ、`firstName` / `lastName` に分割して書き戻します。
707
+
708
+ ### 対応するパス getter パターン
709
+
710
+ | パターン | 説明 | 例 |
711
+ |---|---|---|
712
+ | `get prop()` | トップレベル算出 | `get total()` |
713
+ | `get "a.b"()` | ネスト算出(ワイルドカードなし) | `get "cart.totalPrice"()` |
714
+ | `get "a.*.b"()` | 単一ワイルドカード | `get "users.*.fullName"()` |
715
+ | `get "a.*.b.*.c"()` | 複数ワイルドカード | `get "categories.*.items.*.label"()` |
716
+ | `set "a.*.b"(v)` | ワイルドカード setter | `set "users.*.fullName"(v)` |
717
+
718
+ ### 仕組み
719
+
720
+ 1. **コンテキスト解決** — `for:` ループのレンダリング時に、各イテレーションが `ListIndex` をアドレススタックにプッシュします。パス getter 内の `this["users.*.name"]` はこのスタックを使って `*` を解決するため、常に現在の要素を参照します。
721
+
722
+ 2. **自動依存追跡** — getter が `this["users.*.name"]` にアクセスすると、`users.*.name` から getter のパスへの動的依存が登録されます。`users.*.name` が変更されると、getter のキャッシュが dirty になります。
723
+
724
+ 3. **キャッシュ** — getter の結果は具体的なアドレス(パス + ループインデックス)ごとにキャッシュされます。`users.*.fullName` のインデックス 0 とインデックス 1 は別々のキャッシュエントリを持ちます。依存先が変更された場合のみキャッシュが無効化されます。
725
+
726
+ 4. **直接インデックスアクセス** — 数値インデックスで特定の要素にアクセスすることもできます:`this["users.0.name"]` はループコンテキストなしで `users[0].name` に解決されます。
727
+
728
+ ### ループインデックス変数(`$1`, `$2`, ...)
729
+
730
+ getter やイベントハンドラ内で、`this.$1`、`this.$2` などで現在のループイテレーションのインデックスを取得できます(0始まりの値、1始まりの命名):
731
+
732
+ ```javascript
733
+ export default {
734
+ users: ["Alice", "Bob", "Charlie"],
735
+ get "users.*.rowLabel"() {
736
+ return "#" + (this.$1 + 1) + ": " + this["users.*"];
737
+ }
738
+ };
739
+ ```
740
+
741
+ ```html
742
+ <template data-wcs="for: users">
743
+ <div data-wcs="textContent: .rowLabel"></div>
744
+ </template>
745
+ <!-- 出力:
746
+ #1: Alice
747
+ #2: Bob
748
+ #3: Charlie
749
+ -->
750
+ ```
751
+
752
+ ネストループでは、`$1` が外側のインデックス、`$2` が内側のインデックスです。
753
+
754
+ テンプレート内でループインデックスを直接表示することもできます:
755
+
756
+ ```html
757
+ <template data-wcs="for: items">
758
+ <td>{{ $1|inc(1) }}</td> <!-- 1始まりの行番号 -->
759
+ </template>
760
+ ```
761
+
762
+ ### Proxy API
763
+
764
+ 状態オブジェクト内(getter / メソッド)で `this` 経由で以下の API が利用できます:
765
+
766
+ | API | 説明 |
767
+ |---|---|
768
+ | `this.$getAll(path, indexes?)` | ワイルドカードパスにマッチする全ての値を取得 |
769
+ | `this.$resolve(path, indexes, value?)` | ワイルドカードパスを特定のインデックスで解決 |
770
+ | `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行 |
771
+ | `this.$trackDependency(path)` | キャッシュ無効化のための依存関係を手動で登録 |
772
+ | `this.$stateElement` | `IStateElement` インスタンスへのアクセス |
773
+ | `this.$1`, `this.$2`, ... | 現在のループインデックス(1始まりの命名、0始まりの値) |
774
+
775
+ #### `$getAll` — 配列要素全体の集計
776
+
777
+ `$getAll` はワイルドカードパスにマッチする全ての値を配列として収集します。集計パターンに不可欠です:
778
+
779
+ ```javascript
780
+ export default {
781
+ scores: [85, 92, 78, 95, 88],
782
+ get average() {
783
+ const all = this.$getAll("scores.*", []);
784
+ return all.reduce((sum, v) => sum + v, 0) / all.length;
785
+ },
786
+ get max() {
787
+ return Math.max(...this.$getAll("scores.*", []));
788
+ }
789
+ };
790
+ ```
791
+
792
+ #### `$resolve` — 明示的なインデックスでのアクセス
793
+
794
+ `$resolve` は特定のワイルドカードインデックスの値を読み書きします:
795
+
796
+ ```javascript
797
+ export default {
798
+ items: ["A", "B", "C"],
799
+ swapFirstTwo() {
800
+ const a = this.$resolve("items.*", [0]);
801
+ const b = this.$resolve("items.*", [1]);
802
+ this.$resolve("items.*", [0], b);
803
+ this.$resolve("items.*", [1], a);
804
+ }
805
+ };
806
+ ```
807
+
808
+ ## イベントハンドリング
809
+
810
+ `on*` プロパティでイベントハンドラをバインドします:
811
+
812
+ ```html
813
+ <button data-wcs="onclick: handleClick">クリック</button>
814
+ <form data-wcs="onsubmit#prevent: handleSubmit">...</form>
815
+ ```
816
+
817
+ ハンドラメソッドはイベントとループインデックスを受け取ります:
818
+
819
+ ```javascript
820
+ export default {
821
+ items: ["A", "B", "C"],
822
+ handleClick(event) {
823
+ console.log("clicked");
824
+ },
825
+ removeItem(event, index) {
826
+ // index はループコンテキスト ($1)
827
+ this.items = this.items.toSpliced(index, 1);
828
+ }
829
+ };
830
+ ```
831
+
832
+ ```html
833
+ <template data-wcs="for: items">
834
+ <button data-wcs="onclick: removeItem">削除</button>
835
+ </template>
836
+ ```
837
+
838
+ ## フィルタ
839
+
840
+ 40 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます。
841
+
842
+ ### 比較
843
+
844
+ | フィルタ | 説明 | 例 |
845
+ |---|---|---|
846
+ | `eq(value)` | 等しい | `count\|eq(0)` → `true/false` |
847
+ | `ne(value)` | 等しくない | `count\|ne(0)` |
848
+ | `not` | 論理否定 | `isActive\|not` |
849
+ | `lt(n)` | より小さい | `count\|lt(10)` |
850
+ | `le(n)` | 以下 | `count\|le(10)` |
851
+ | `gt(n)` | より大きい | `count\|gt(0)` |
852
+ | `ge(n)` | 以上 | `count\|ge(0)` |
853
+
854
+ ### 算術
855
+
856
+ | フィルタ | 説明 | 例 |
857
+ |---|---|---|
858
+ | `inc(n)` | 加算 | `count\|inc(1)` |
859
+ | `dec(n)` | 減算 | `count\|dec(1)` |
860
+ | `mul(n)` | 乗算 | `price\|mul(1.1)` |
861
+ | `div(n)` | 除算 | `total\|div(100)` |
862
+ | `mod(n)` | 剰余 | `index\|mod(2)` |
863
+
864
+ ### 数値フォーマット
865
+
866
+ | フィルタ | 説明 | 例 |
867
+ |---|---|---|
868
+ | `fix(n)` | 固定小数点桁数 | `price\|fix(2)` → `"100.00"` |
869
+ | `round(n?)` | 四捨五入 | `value\|round(2)` |
870
+ | `floor(n?)` | 切り捨て | `value\|floor` |
871
+ | `ceil(n?)` | 切り上げ | `value\|ceil` |
872
+ | `locale(loc?)` | ロケール数値フォーマット | `count\|locale` / `count\|locale(ja-JP)` |
873
+ | `percent(n?)` | パーセンテージフォーマット | `ratio\|percent(1)` |
874
+
875
+ ### 文字列
876
+
877
+ | フィルタ | 説明 | 例 |
878
+ |---|---|---|
879
+ | `uc` | 大文字変換 | `name\|uc` |
880
+ | `lc` | 小文字変換 | `name\|lc` |
881
+ | `cap` | 先頭大文字 | `name\|cap` |
882
+ | `trim` | 空白除去 | `text\|trim` |
883
+ | `slice(n)` | 文字列スライス | `text\|slice(5)` |
884
+ | `substr(start, length)` | 部分文字列 | `text\|substr(0,10)` |
885
+ | `pad(n, char?)` | 先頭パディング | `id\|pad(5,0)` → `"00001"` |
886
+ | `rep(n)` | 繰り返し | `text\|rep(3)` |
887
+ | `rev` | 反転 | `text\|rev` |
888
+
889
+ ### 型変換
890
+
891
+ | フィルタ | 説明 | 例 |
892
+ |---|---|---|
893
+ | `int` | 整数パース | `input\|int` |
894
+ | `float` | 浮動小数点パース | `input\|float` |
895
+ | `boolean` | 真偽値に変換 | `value\|boolean` |
896
+ | `number` | 数値に変換 | `value\|number` |
897
+ | `string` | 文字列に変換 | `value\|string` |
898
+ | `null` | null に変換 | `value\|null` |
899
+
900
+ ### 日付 / 時刻
901
+
902
+ | フィルタ | 説明 | 例 |
903
+ |---|---|---|
904
+ | `date(loc?)` | 日付フォーマット | `timestamp\|date` / `timestamp\|date(ja-JP)` |
905
+ | `time(loc?)` | 時刻フォーマット | `timestamp\|time` |
906
+ | `datetime(loc?)` | 日付 + 時刻 | `timestamp\|datetime(en-US)` |
907
+ | `ymd(sep?)` | YYYY-MM-DD | `timestamp\|ymd` / `timestamp\|ymd(/)` |
908
+
909
+ ### 真偽値 / デフォルト
910
+
911
+ | フィルタ | 説明 | 例 |
912
+ |---|---|---|
913
+ | `truthy` | truthy チェック | `value\|truthy` |
914
+ | `falsy` | falsy チェック | `value\|falsy` |
915
+ | `defaults(v)` | フォールバック値 | `name\|defaults(Anonymous)` |
916
+
917
+ ### フィルタチェーン
918
+
919
+ フィルタは `|` で連結できます:
920
+
921
+ ```html
922
+ <div data-wcs="textContent: price|mul(1.1)|round(2)|locale(ja-JP)"></div>
923
+ ```
924
+
925
+ ## Web Component バインディング
926
+
927
+ `@wcstack/state` は Shadow DOM または Light DOM を使用したカスタム要素との双方向状態バインディングに対応しています。
928
+
929
+ 多くのフレームワークでは、コンポーネント間の状態共有に props のバケツリレー、Context Provider、あるいは外部ストア(Redux, Pinia など)といったパターンが用いられます。`@wcstack/state` はこれらとは異なるアプローチを採ります。親コンポーネントと子コンポーネントは**パスの契約**によって結びつけられます。親は `data-wcs` 属性を使って外部の状態パスを子コンポーネントのプロパティにバインドし、子は自身の状態として通常通り読み書きを行うだけです:
930
+
931
+ 1. 子コンポーネントは、自身の状態プロキシを通じて親の状態を参照・更新します。props の受け渡しやイベント発行など、親の存在を意識したコーディングは必要ありません。
932
+ 2. 親の状態が変更されると、Proxy の `set` トラップが影響するパスを参照している子のバインディングへ自動的に通知します。
933
+ 3. 結合点は**パス名のみ**であるため、親と子は完全に疎結合な状態を保ち、それぞれ独立してテスト可能です。
934
+ 4. 実行コストは、パスの解決(初回アクセス後はキャッシュされるため O(1) で動作します)と、依存グラフを通じた変更の伝播のみです。
935
+
936
+ これは、コンポーネントレベルの複雑な抽象化ではなく、「パスの解決」に基づいたコンポーネント間状態管理への軽量なアプローチです。
937
+
938
+ ### コンポーネント定義(Shadow DOM)
939
+
940
+ ```javascript
941
+ class MyComponent extends HTMLElement {
942
+ state = { message: "" };
943
+
944
+ constructor() {
945
+ super();
946
+ this.attachShadow({ mode: "open" });
947
+ this.shadowRoot.innerHTML = `
948
+ <wcs-state bind-component="state"></wcs-state>
949
+ <div>{{ message }}</div>
950
+ <input type="text" data-wcs="value: message" />
951
+ `;
952
+ }
953
+ }
954
+ customElements.define("my-component", MyComponent);
955
+ ```
956
+
957
+ ### コンポーネント定義(Light DOM)
958
+
959
+ Light DOM コンポーネントは Shadow DOM を使用しません。CSS と同様に state の名前空間も上位スコープと共有されるため、`name` 属性が必須です。
960
+
961
+ ```javascript
962
+ class MyLightComponent extends HTMLElement {
963
+ state = { message: "" };
964
+
965
+ connectedCallback() {
966
+ this.innerHTML = `
967
+ <wcs-state bind-component="state" name="my-light"></wcs-state>
968
+ <div data-wcs="text: message@my-light"></div>
969
+ <input type="text" data-wcs="value: message@my-light" />
970
+ `;
971
+ }
972
+ }
973
+ customElements.define("my-light-component", MyLightComponent);
974
+ ```
975
+
976
+ - Light DOM コンポーネントでは `name` 属性が**必須**です(名前空間が上位スコープと共有されるため)
977
+ - バインディングでは `@my-light` のように状態名を明示的に参照する必要があります
978
+ - `<wcs-state>` はコンポーネント要素の直下に配置する必要があります
979
+
980
+ ### ホスト側の使用方法
981
+
982
+ ```html
983
+ <wcs-state>
984
+ <script type="module">
985
+ export default {
986
+ user: { name: "Alice" }
987
+ };
988
+ </script>
989
+ </wcs-state>
990
+
991
+ <!-- コンポーネントの state.message を外側の user.name にバインド -->
992
+ <my-component data-wcs="state.message: user.name"></my-component>
993
+ ```
994
+
995
+ - `bind-component="state"` でコンポーネントの `state` プロパティを `<wcs-state>` にマッピング
996
+ - `data-wcs="state.message: user.name"` でホスト要素上の外部状態パスを内部コンポーネント状態プロパティにバインド
997
+ - 変更はコンポーネントと外部状態間で双方向に伝播
998
+
999
+ ### 独立した Web Component への状態注入(`__e2e__/single-component`)
1000
+
1001
+ ホストの外部状態に依存しないコンポーネントでも、`bind-component` で `state` を注入してリアクティブにできます。
1002
+
1003
+ ```javascript
1004
+ class MyComponent extends HTMLElement {
1005
+ state = Object.freeze({
1006
+ message: "Hello, World!"
1007
+ });
1008
+
1009
+ constructor() {
1010
+ super();
1011
+ this.attachShadow({ mode: "open" });
1012
+ }
1013
+
1014
+ connectedCallback() {
1015
+ this.shadowRoot.innerHTML = `
1016
+ <wcs-state bind-component="state"></wcs-state>
1017
+ <div>{{ message }}</div>
1018
+ `;
1019
+ }
1020
+
1021
+ async $stateReadyCallback(stateProp) {
1022
+ console.log("state ready:", stateProp); // "state"
1023
+ }
1024
+ }
1025
+ customElements.define("my-component", MyComponent);
1026
+ ```
1027
+
1028
+ - 初期 `state` は `Object.freeze(...)` で定義できます(注入後は書き換え可能なリアクティブ状態に置き換え)
1029
+ - `bind-component="state"` により `this.state` が `@wcstack/state` の状態プロキシとして利用可能になります
1030
+ - `this.state.message = "..."` のような代入で、Shadow DOM 内の `{{ message }}` が即時に更新されます
1031
+ - `async $stateReadyCallback(stateProp)` は、Web Component 側で状態が利用可能になった直後に呼ばれます(`stateProp` は `bind-component` のプロパティ名)
1032
+
1033
+ ### 制約事項
1034
+
1035
+ - `bind-component` 付きの `<wcs-state>` はコンポーネント要素の**直下**(トップレベル)に配置すること
1036
+ - 親要素は**カスタム要素**(ハイフンを含むタグ名)であること
1037
+ - Light DOM コンポーネントでは `name` 属性が**必須**(上位スコープとの名前空間衝突を回避するため)
1038
+ - Light DOM のバインディングでは状態名を明示的に参照すること(例: `@my-light`)
1039
+
1040
+ ### ループ内でのコンポーネント使用
1041
+
1042
+ ```html
1043
+ <template data-wcs="for: users">
1044
+ <my-component data-wcs="state.message: .name"></my-component>
1045
+ </template>
1046
+ ```
1047
+
1048
+ ## 宣言的カスタムコンポーネント (DCC)
1049
+
1050
+ JavaScript のクラス定義なしで、**HTML だけ**でカスタム要素を定義できます。`data-wc-definition` と Declarative Shadow DOM (`<template shadowrootmode>`) を使い、リアクティブな状態を持つ再利用可能なコンポーネントをインラインで宣言します。
1051
+
1052
+ ### 基本的な定義
1053
+
1054
+ ```html
1055
+ <!-- 1. コンポーネントを定義(CSSで非表示) -->
1056
+ <my-counter data-wc-definition>
1057
+ <template shadowrootmode="open">
1058
+ <p>{{ count }}</p>
1059
+ <button data-wcs="onclick: increment">+1</button>
1060
+ <wcs-state>
1061
+ <script type="module">
1062
+ export default {
1063
+ count: 0,
1064
+ increment() { this.count++; },
1065
+ $bindables: ["count"]
1066
+ };
1067
+ </script>
1068
+ </wcs-state>
1069
+ </template>
1070
+ </my-counter>
1071
+
1072
+ <!-- 2. 使う — 各インスタンスが独自の状態を持つ -->
1073
+ <my-counter></my-counter>
1074
+ <my-counter></my-counter>
1075
+ ```
1076
+
1077
+ `<wcs-state>` が `data-wc-definition` 付きのホスト内にあることを検出すると:
1078
+
1079
+ 1. 状態オブジェクトをロード(`<script type="module">` または `src="*.js"`)
1080
+ 2. getter/setter/メソッドをプロトタイプに定義したカスタム要素クラスを生成
1081
+ 3. `customElements.define()` で登録
1082
+
1083
+ 定義要素は非表示になり、各インスタンスはテンプレートを自身の Shadow DOM にクローンして、独自の `<wcs-state>` を初期化します。
1084
+
1085
+ ### 推奨 CSS
1086
+
1087
+ ```css
1088
+ :not(:defined) { display: none; }
1089
+ [data-wc-definition] { display: none; }
1090
+ ```
1091
+
1092
+ ### `$bindables` と wc-bindable プロトコル
1093
+
1094
+ `$bindables` 配列は、変更イベント付きのコンポーネントプロパティとして公開する状態プロパティを宣言します。[wc-bindable プロトコル](https://github.com/nicenemo/nicenemo/blob/main/docs/wc-bindable-protocol.md)に準拠しています:
1095
+
1096
+ ```javascript
1097
+ export default {
1098
+ count: 0,
1099
+ increment() { this.count++; },
1100
+ $bindables: ["count"]
1101
+ };
1102
+ ```
1103
+
1104
+ これにより以下が生成されます:
1105
+
1106
+ - クラスの `static wcBindable` — フレームワークアダプタ用のプロトコルメタデータ
1107
+ - プロトタイプの getter/setter — リアクティブプロキシ経由で読み書き
1108
+ - `CustomEvent` のディスパッチ — 値が変更されるたびに `my-counter:count-changed` が発火
1109
+
1110
+ ### DCC プロパティへのバインディング
1111
+
1112
+ 他の `<wcs-state>` インスタンスから、通常の Web Component と同じように DCC プロパティにバインドできます:
1113
+
1114
+ ```html
1115
+ <my-counter data-wcs="count: parentCount"></my-counter>
1116
+
1117
+ <wcs-state>
1118
+ <script type="module">
1119
+ export default { parentCount: 0 };
1120
+ </script>
1121
+ </wcs-state>
1122
+ <div data-wcs="textContent: parentCount"></div>
1123
+ ```
1124
+
1125
+ ### Shadow Root モード
1126
+
1127
+ `open` と `closed` の両モードに対応しています:
1128
+
1129
+ ```html
1130
+ <my-component data-wc-definition>
1131
+ <template shadowrootmode="closed">
1132
+ <!-- closed shadow DOM -->
1133
+ </template>
1134
+ </my-component>
1135
+ ```
1136
+
1137
+ ### 内部プロパティ
1138
+
1139
+ `$` プレフィックス付きのプロパティは内部用で、コンポーネントのプロトタイプには公開されません:
1140
+
1141
+ | プロパティ | 用途 |
1142
+ |----------|---------|
1143
+ | `$bindables` | 観測可能プロパティの宣言 |
1144
+ | `$connectedCallback` | ライフサイクルフック(各インスタンスで実行) |
1145
+ | `$disconnectedCallback` | クリーンアップフック |
1146
+ | `$updatedCallback` | 状態変更後に呼ばれる |
1147
+
1148
+ ## SVG サポート
1149
+
1150
+ 全てのバインディングが `<svg>` 要素内で動作します。SVG 属性には `attr.*` を使用します:
1151
+
1152
+ ```html
1153
+ <svg width="200" height="100">
1154
+ <template data-wcs="for: points">
1155
+ <circle data-wcs="attr.cx: .x; attr.cy: .y; attr.fill: .color" r="5" />
1156
+ </template>
1157
+ </svg>
1158
+ ```
1159
+
1160
+ ## ライフサイクルフック
1161
+
1162
+ 状態オブジェクトに `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback` を定義すると、初期化・クリーンアップ・更新時のフックとして利用できます。
1163
+
1164
+ ```html
1165
+ <wcs-state>
1166
+ <script type="module">
1167
+ export default {
1168
+ timer: null,
1169
+ count: 0,
1170
+
1171
+ // <wcs-state> が DOM に接続された時に呼ばれる
1172
+ async $connectedCallback() {
1173
+ const res = await fetch("/api/initial-count");
1174
+ this.count = await res.json();
1175
+ this.timer = setInterval(() => { this.count++; }, 1000);
1176
+ },
1177
+
1178
+ // <wcs-state> が DOM から切断された時に呼ばれる(同期のみ)
1179
+ $disconnectedCallback() {
1180
+ clearInterval(this.timer);
1181
+ }
1182
+ };
1183
+ </script>
1184
+ </wcs-state>
1185
+ ```
1186
+
1187
+ | フック | タイミング | 非同期 |
1188
+ |---|---|---|
1189
+ | `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
1190
+ | `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
1191
+ | `$updatedCallback(paths, indexesListByPath)` | 状態変更が適用された後に呼び出し | 可(await されない) |
1192
+
1193
+ `$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
1194
+
1195
+ - フック内の `this` は読み書き可能な状態プロキシです。
1196
+ - `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
1197
+ - `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
1198
+ - `$updatedCallback(paths, indexesListByPath)` は更新された状態パスの一覧を受け取ります。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。`async` を使用できますが、戻り値は await されません。
1199
+ - Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
1200
+
1201
+ ## 設定
1202
+
1203
+ `bootstrapState()` に部分的な設定オブジェクトを渡します:
1204
+
1205
+ ```javascript
1206
+ import { bootstrapState } from '@wcstack/state';
1207
+
1208
+ bootstrapState({
1209
+ locale: 'ja-JP',
1210
+ debug: true,
1211
+ enableMustache: false,
1212
+ tagNames: { state: 'my-state' },
1213
+ });
1214
+ ```
1215
+
1216
+ 全オプションとデフォルト値:
1217
+
1218
+ | オプション | デフォルト | 説明 |
1219
+ |---|---|---|
1220
+ | `bindAttributeName` | `'data-wcs'` | バインディング属性名 |
1221
+ | `tagNames.state` | `'wcs-state'` | 状態要素のタグ名 |
1222
+ | `locale` | `'en'` | フィルタのデフォルトロケール |
1223
+ | `debug` | `false` | デバッグモード |
1224
+ | `enableMustache` | `true` | `{{ }}` 構文の有効化 |
1225
+
1226
+ ## TypeScript サポート
1227
+
1228
+ `defineState()` で状態オブジェクトをラップすると、メソッドや getter 内の `this` に型補完が効きます。ランタイムコストはゼロ(アイデンティティ関数)です。
1229
+
1230
+ ```typescript
1231
+ import { defineState } from '@wcstack/state';
1232
+
1233
+ export default defineState({
1234
+ count: 0,
1235
+ users: [] as { name: string; age: number }[],
1236
+
1237
+ increment() {
1238
+ this.count++; // ✅ number
1239
+ this["users.*.name"]; // ✅ string(ドットパス型解決)
1240
+ this.$getAll("users.*.age", []); // ✅ API メソッド
1241
+ },
1242
+
1243
+ get "users.*.ageCategory"() {
1244
+ return this["users.*.age"] < 25 ? "Young" : "Adult";
1245
+ }
1246
+ });
1247
+ ```
1248
+
1249
+ ユーティリティ型 `WcsPaths<T>` と `WcsPathValue<T, P>` もエクスポートされます。詳細は [docs/define-state.ja.md](docs/define-state.ja.md) を参照してください。
1250
+
1251
+ ## API リファレンス
1252
+
1253
+ ### `bootstrapState()`
1254
+
1255
+ 状態システムを初期化します。`<wcs-state>` カスタム要素を登録し、DOM コンテンツ読み込みハンドラを設定します。
1256
+
1257
+ ```javascript
1258
+ import { bootstrapState } from '@wcstack/state';
1259
+ bootstrapState();
1260
+ ```
1261
+
1262
+ ### `<wcs-state>` 要素
1263
+
1264
+ | 属性 | 説明 |
1265
+ |---|---|
1266
+ | `name` | 状態名(デフォルト: `"default"`) |
1267
+ | `state` | `<script type="application/json">` 要素の ID |
1268
+ | `src` | `.json` または `.js` ファイルの URL |
1269
+ | `json` | インライン JSON 文字列 |
1270
+ | `bind-component` | Web Component バインディングのプロパティ名 |
1271
+
1272
+ ### IStateElement
1273
+
1274
+ | プロパティ / メソッド | 説明 |
1275
+ |---|---|
1276
+ | `name` | 状態名 |
1277
+ | `initializePromise` | 状態の完全な初期化時に解決される Promise |
1278
+ | `listPaths` | `for` ループで使用されるパスの Set |
1279
+ | `getterPaths` | getter として定義されたパスの Set |
1280
+ | `setterPaths` | setter として定義されたパスの Set |
1281
+ | `createState(mutability, callback)` | 状態プロキシを作成(`"readonly"` または `"writable"`) |
1282
+ | `createStateAsync(mutability, callback)` | `createState` の非同期版 |
1283
+ | `setInitialState(state)` | プログラムから状態を設定(初期化前) |
1284
+ | `bindProperty(prop, descriptor)` | 生の状態オブジェクトにプロパティを定義 |
1285
+ | `nextVersion()` | バージョン番号をインクリメントして返す |
1286
+
1287
+ ## アーキテクチャ
1288
+
1289
+ ```
1290
+ bootstrapState()
1291
+ └── registerComponents() // <wcs-state> カスタム要素を登録
1292
+
1293
+ <wcs-state> connectedCallback
1294
+ ├── _initializeBindWebComponent() // bind-component: 親コンポーネントから状態を取得
1295
+ ├── _initialize() // 状態をロード (state属性 / src / json / script / API)
1296
+ │ └── setStateElementByName() // WeakMap<Node, Map<name, element>> に登録
1297
+ │ └── (rootNode への初回登録時)
1298
+ │ └── queueMicrotask → buildBindings()
1299
+ ├── _callStateConnectedCallback() // $connectedCallback が定義されていれば呼び出し
1300
+
1301
+ buildBindings(root)
1302
+ ├── waitForStateInitialize() // 全 <wcs-state> の initializePromise を待機
1303
+ ├── convertMustacheToComments() // {{ }} → コメントノードに変換
1304
+ ├── collectStructuralFragments() // for/if テンプレートを収集
1305
+ └── initializeBindings() // DOM 走査、data-wcs 解析、バインディング設定
1306
+ ```
1307
+
1308
+ ### リアクティビティフロー
1309
+
1310
+ 1. Proxy の `set` トラップによる状態変更 → `setByAddress()`
1311
+ 2. アドレス解決 → updater が絶対アドレスをキューに登録
1312
+ 3. 依存関係ウォーカーが下流のキャッシュを無効化(dirty)
1313
+ 4. updater が `applyChangeFromBindings()` によりバインド済み DOM ノードに変更を適用
1314
+
1315
+ ### 状態アドレスシステム
1316
+
1317
+ `users.*.name` のようなパスは以下に分解されます:
1318
+
1319
+ - **PathInfo** — 静的パスメタデータ(セグメント、ワイルドカード数、親パス)
1320
+ - **ListIndex** — ランタイムループインデックスチェーン
1321
+ - **StateAddress** — PathInfo + ListIndex の組み合わせ
1322
+ - **AbsoluteStateAddress** — 状態名 + StateAddress(クロス状態参照用)
1323
+
1324
+ ## サーバーサイドレンダリング
1325
+
1326
+ `@wcstack/state` は [`@wcstack/server`](../server/) パッケージと連携して SSR をサポートしています。クライアント用に書いたテンプレートがそのままサーバーでレンダリングされます — 変更不要。
1327
+
1328
+ ### クイックセットアップ
1329
+
1330
+ 1. `<wcs-state>` に `enable-ssr` を追加:
1331
+
1332
+ ```html
1333
+ <wcs-state enable-ssr>
1334
+ <script type="module">
1335
+ export default {
1336
+ items: [],
1337
+ async $connectedCallback() {
1338
+ const res = await fetch("/api/items");
1339
+ this.items = await res.json();
1340
+ }
1341
+ };
1342
+ </script>
1343
+ </wcs-state>
1344
+ <template data-wcs="for: items">
1345
+ <div data-wcs="textContent: items.*.name"></div>
1346
+ </template>
1347
+ ```
1348
+
1349
+ 2. サーバーでレンダリング:
1350
+
1351
+ ```javascript
1352
+ import { renderToString } from "@wcstack/server";
1353
+
1354
+ const html = await renderToString(template, {
1355
+ baseUrl: "http://localhost:3000"
1356
+ });
1357
+ ```
1358
+
1359
+ これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を自動検出し、JSON スナップショットから状態を復元し��再レンダリングなしでリアクティビティを再開します。
1360
+
1361
+ ### 仕組み
1362
+
1363
+ | フェーズ | 動作 |
1364
+ |---------|------|
1365
+ | **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用、ハイドレーションデータを含む `<wcs-ssr>` 要素付きのレンダリング済み HTML を出力 |
1366
+ | **クライアント** | `<wcs-state enable-ssr>` が `<wcs-ssr>` の JSON から状態をロード、`$connectedCallback` をスキップ、`hydrateBindings()` が既存の DOM にリアクティビティを接続 |
1367
+ | **フォールバック** | ���ーバー/クライアントのバージョン不一致時、SSR DOM をクリーンアップして `buildBindings()` でフルクライアントサイドレンダリングを実行 |
1368
+
1369
+ ### `enable-ssr` の動作
1370
+
1371
+ | コンテキスト | 動作 |
1372
+ |------------|------|
1373
+ | **サーバー**(`renderToString`) | 状態 JSON、テンプレートフラグメント、プロパティデータを含む `<wcs-ssr>` を生成 |
1374
+ | **クラ��アント**(ハイドレーション) | `<wcs-ssr>` を読み取り、状態を復元、`$connectedCallback` をスキップ、既存 DOM のバイン���ィングをハイドレート |
1375
+
1376
+ API の詳細は [`@wcstack/server` README](../server/README.ja.md) を参照してください。
1377
+
1378
+ ## ライセンス
1379
+
1380
+ MIT