@wcstack/state 1.25.0 → 1.27.0
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 +223 -13
- package/README.md +227 -13
- package/dist/auto.min.js +2 -3
- package/dist/auto.min.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.esm.js +2828 -556
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +163 -1
- package/dist/wcs-manifest.json +85 -0
- package/package.json +1 -1
- package/dist/auto.js +0 -3
- package/dist/index.esm.min.js +0 -2
- package/dist/index.esm.min.js.map +0 -1
package/README.ja.md
CHANGED
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
- **宣言的データバインディング** — `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
|
|
96
96
|
- **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
|
|
97
97
|
- **構造ディレクティブ** — `<template>` 要素による `for`, `if` / `elseif` / `else`
|
|
98
|
-
- **組み込みフィルタ** — フォーマット、比較、算術、日付など
|
|
98
|
+
- **組み込みフィルタ** — フォーマット、比較、算術、日付など 46 種類
|
|
99
99
|
- **双方向バインディング** — `<input>`, `<select>`, `<textarea>` で自動有効
|
|
100
100
|
- **Web Component バインディング** — Shadow DOM コンポーネントとの双方向状態バインディング
|
|
101
101
|
- **command token** — pub/sub チャネル(`command.<method>: tokenName`)で state から wc-bindable カスタム要素のメソッドを起動
|
|
@@ -220,6 +220,8 @@
|
|
|
220
220
|
|
|
221
221
|
解決順序: `state` → `src` (.json / .js) → `json` → 内包 `<script>` → `setInitialState()` 待機。
|
|
222
222
|
|
|
223
|
+
> **Content-Security-Policy 下では:** 5 番(内包 `<script type="module">`)は `blob:` URL 経由で評価されるため `script-src blob:` が必要です。ページの nonce では救えません。厳格な CSP を敷く場合は 4 番(`src="./state.js"`)を使ってください。追加ディレクティブは不要です。詳細は [docs/csp.ja.md](../../docs/csp.ja.md)。
|
|
224
|
+
|
|
223
225
|
### 名前付き状態
|
|
224
226
|
|
|
225
227
|
複数の状態要素を `name` 属性で共存できます。バインディングでは `@name` で参照します:
|
|
@@ -392,7 +394,7 @@ authority はバインディング単位で `#init=` により上書きできま
|
|
|
392
394
|
|
|
393
395
|
- `enableDirectionalInitialSync: false`(opt-out)のとき `#init=`/`#sync=` を書くと throw します。
|
|
394
396
|
- **1.20 以前からの移行:** output-only メンバに対して state 側に都合のよい初期値(`value: []`、`query: ""` 等)をシードしないでください — 要素側の実初期値(多くは `null`/`undefined`)がシードを置き換えます。シードは要素の実初期値に合わせ、表示用の値は派生 getter で null ガードしてください。
|
|
395
|
-
- **1.21.x まで**、`init=element` / `init=auto` / `init=none` はバインディングの生存期間全体で state→element 書き込みを抑止しており、真に双方向なメンバには使えませんでした。現在は authority は初期同期のみを支配します(`docs/architecture-hardening/09-remediation-design.md` §3.6)。
|
|
397
|
+
- **1.21.x まで**、`init=element` / `init=auto` / `init=none` はバインディングの生存期間全体で state→element 書き込みを抑止しており、真に双方向なメンバには使えませんでした。現在は authority は初期同期のみを支配します(`docs/architecture-hardening/09-remediation-design.ja.md` §3.6)。
|
|
396
398
|
|
|
397
399
|
### ラジオボタンバインディング
|
|
398
400
|
|
|
@@ -507,7 +509,42 @@ export default {
|
|
|
507
509
|
</template>
|
|
508
510
|
```
|
|
509
511
|
|
|
510
|
-
`for:`
|
|
512
|
+
`for:` ディレクティブは**値ベースの差分アルゴリズム**を使用します。配列の各要素の値そのものが識別キーとして機能します。配列が再代入されると、差分アルゴリズムが新旧の要素を値で照合し、変更のない要素の DOM ノードを再利用しつつ、追加・削除・並び替えを効率的に処理します。
|
|
513
|
+
|
|
514
|
+
つまり、**行オブジェクトの参照さえ保たれていれば、行の追加・削除・並べ替えに明示的なキー属性(React の `key` や Vue の `:key`)は不要です**。非破壊の配列メソッド(`toSorted` / `toReversed` / `filter` / `with` / `toSpliced`)はいずれも要素の参照を保つため、ソートやフィルタは構造的に keyed に動作し、「キーの付け方を間違える」種類のバグが原理的に発生しません。
|
|
515
|
+
|
|
516
|
+
例外は、**新しく生成されたオブジェクトとして届くデータ**です。`fetch(...).json()`、ストレージからの `JSON.parse`、WebSocket / SSE の全件スナップショット、Worker の `postMessage` などがこれにあたります。これらの行は参照で照合できないため、全行が破棄・再構築されます。次の [`$listKeys`](#listkeys--再取得された行の同一性) を参照してください。
|
|
517
|
+
|
|
518
|
+
#### `$listKeys` — 再取得された行の同一性
|
|
519
|
+
|
|
520
|
+
行がバインディングの管理外の DOM 状態(フォーカス、IME 変換中の文字列、`<details>` の開閉、行内スクロール位置、`<canvas>` の描画内容、`<video>` の再生位置)を持つ場合、行の再構築でそれらは失われます。キーを宣言すると、リフレッシュをまたいで行を同定できます:
|
|
521
|
+
|
|
522
|
+
```js
|
|
523
|
+
{
|
|
524
|
+
items: [],
|
|
525
|
+
$listKeys: {
|
|
526
|
+
"items": "id", // フィールド名
|
|
527
|
+
"items.*.children": (row) => row.uid, // 複合キー用の関数も可
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
キーを宣言すると、新しい配列を代入しても**既存の行オブジェクトが据え置かれ**、実際に変化したフィールドだけがそこへ書き込まれます。行の DOM は再構築されず再利用されます:
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
// 行オブジェクトはすべて新しいが id で照合されるため、DOM・フォーカス・
|
|
536
|
+
// <details> の状態は保たれ、差分のあるフィールドだけが書き込まれる
|
|
537
|
+
this.items = await (await fetch("/api/items")).json();
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
補足:
|
|
541
|
+
|
|
542
|
+
- **opt-in かつパス単位**です。宣言していないリストは従来どおりの挙動で、追加コストもありません。
|
|
543
|
+
- **ネストも opt-in** です。宣言されたパスだけがキー照合され、未宣言のネスト配列は従来どおり参照置換されます。リスト単位で段階的に導入できます。
|
|
544
|
+
- **無変化のリフレッシュはゼロコスト**です。何も変わっていなければフィールド書き込みも DOM 操作も一切発生しません。
|
|
545
|
+
- **行は plain object** である必要があり、キーは存在かつ一意でなければなりません。キーの重複・欠落・クラスインスタンスは、静かに劣化させず即座にエラーになります。
|
|
546
|
+
- **行から消えたフィールドは `null` でクリア**されます。`null` はこのパッケージにおける明示的なクリアの語彙です(`undefined` は「状態が値を持たない」を意味し、書き込み自体がスキップされます)。
|
|
547
|
+
- 格納される配列は照合済みの行オブジェクトから組み直されるため、代入後は `this.items !== 代入した配列` になります。
|
|
511
548
|
|
|
512
549
|
#### ドット省略記法
|
|
513
550
|
|
|
@@ -948,7 +985,7 @@ export default {
|
|
|
948
985
|
|
|
949
986
|
## フィルタ
|
|
950
987
|
|
|
951
|
-
|
|
988
|
+
46 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます。
|
|
952
989
|
|
|
953
990
|
### 比較
|
|
954
991
|
|
|
@@ -971,6 +1008,8 @@ export default {
|
|
|
971
1008
|
| `mul(n)` | 乗算 | `price\|mul(1.1)` |
|
|
972
1009
|
| `div(n)` | 除算 | `total\|div(100)` |
|
|
973
1010
|
| `mod(n)` | 剰余 | `index\|mod(2)` |
|
|
1011
|
+
| `abs` | 絶対値 | `delta\|abs` |
|
|
1012
|
+
| `clamp(min, max)` | 範囲内に丸める | `ratio\|clamp(0,100)` |
|
|
974
1013
|
|
|
975
1014
|
### 数値フォーマット
|
|
976
1015
|
|
|
@@ -982,6 +1021,7 @@ export default {
|
|
|
982
1021
|
| `ceil(n?)` | 切り上げ | `value\|ceil` |
|
|
983
1022
|
| `locale(loc?)` | ロケール数値フォーマット | `count\|locale` / `count\|locale(ja-JP)` |
|
|
984
1023
|
| `percent(n?)` | パーセンテージフォーマット | `ratio\|percent(1)` |
|
|
1024
|
+
| `unit(u)` | 単位(任意の接尾辞)を付加 | `width\|unit(px)` → `"40px"` |
|
|
985
1025
|
|
|
986
1026
|
### 文字列
|
|
987
1027
|
|
|
@@ -996,6 +1036,8 @@ export default {
|
|
|
996
1036
|
| `pad(n, char?)` | 先頭パディング | `id\|pad(5,0)` → `"00001"` |
|
|
997
1037
|
| `rep(n)` | 繰り返し | `text\|rep(3)` |
|
|
998
1038
|
| `rev` | 反転 | `text\|rev` |
|
|
1039
|
+
| `truncate(n, suffix?)` | 切り詰めて省略記号を付加 | `title\|truncate(20)` → `"…"` 付き |
|
|
1040
|
+
| `join(sep?)` | 配列を連結(既定 `", "`) | `tags\|join` / `tags\|join(/)` |
|
|
999
1041
|
|
|
1000
1042
|
### 型変換
|
|
1001
1043
|
|
|
@@ -1016,6 +1058,7 @@ export default {
|
|
|
1016
1058
|
| `time(loc?)` | 時刻フォーマット | `timestamp\|time` |
|
|
1017
1059
|
| `datetime(loc?)` | 日付 + 時刻 | `timestamp\|datetime(en-US)` |
|
|
1018
1060
|
| `ymd(sep?)` | YYYY-MM-DD | `timestamp\|ymd` / `timestamp\|ymd(/)` |
|
|
1061
|
+
| `hms(sep?)` | HH:MM:SS | `timestamp\|hms` / `timestamp\|hms(-)` |
|
|
1019
1062
|
|
|
1020
1063
|
### 真偽値 / デフォルト
|
|
1021
1064
|
|
|
@@ -1088,6 +1131,18 @@ customElements.define("my-light-component", MyLightComponent);
|
|
|
1088
1131
|
- バインディングでは `@my-light` のように状態名を明示的に参照する必要があります
|
|
1089
1132
|
- `<wcs-state>` はコンポーネント要素の直下に配置する必要があります
|
|
1090
1133
|
|
|
1134
|
+
- ホスト側からのバインド(`<my-light-component data-wcs="state.message: user.name">`)も
|
|
1135
|
+
Shadow DOM 形と同様に使えます。コンポーネントのサブツリーは**独立したバインディングスコープ**
|
|
1136
|
+
として扱われ、コンポーネント側の状態が名前登録を終えてから配線されます
|
|
1137
|
+
|
|
1138
|
+
> **注意**: Light DOM は名前空間を上位スコープと共有するため、**同じ `name` を持つインスタンスを
|
|
1139
|
+
> 同一スコープに複数置くことはできません**。リストの行ごとにコンポーネントを配置するような形は
|
|
1140
|
+
> Shadow DOM を使ってください。
|
|
1141
|
+
>
|
|
1142
|
+
> また `State.getBindingsReady(root)` はコンポーネントのスコープを含みません(Shadow DOM 形で
|
|
1143
|
+
> 子が別 rootNode にあるのと同じ扱いです)。コンポーネント内部の描画完了まで待ちたい場合は、
|
|
1144
|
+
> コンポーネント側の `<wcs-state>` の初期化を待ってください。
|
|
1145
|
+
|
|
1091
1146
|
### ホスト側の使用方法
|
|
1092
1147
|
|
|
1093
1148
|
```html
|
|
@@ -1156,6 +1211,46 @@ customElements.define("my-component", MyComponent);
|
|
|
1156
1211
|
</template>
|
|
1157
1212
|
```
|
|
1158
1213
|
|
|
1214
|
+
### コンポーネント側でリストを描画する
|
|
1215
|
+
|
|
1216
|
+
配列をコンポーネントに束ね、`for:` をコンポーネントの内側で回すこともできます。
|
|
1217
|
+
値の正本は外側の状態のままで、行の追加・削除・並べ替え・行フィールドの書き込みは双方向に届きます。
|
|
1218
|
+
|
|
1219
|
+
```html
|
|
1220
|
+
<!-- ホスト側 -->
|
|
1221
|
+
<wcs-state json='{"rows":[{"name":"Alice"},{"name":"Bob"}]}'></wcs-state>
|
|
1222
|
+
<my-list data-wcs="state.items: rows"></my-list>
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
```javascript
|
|
1226
|
+
// コンポーネント側(Shadow DOM)
|
|
1227
|
+
this.shadowRoot.innerHTML = `
|
|
1228
|
+
<wcs-state bind-component="state"></wcs-state>
|
|
1229
|
+
<ul>
|
|
1230
|
+
<template data-wcs="for: items">
|
|
1231
|
+
<li data-wcs="textContent: .name"></li>
|
|
1232
|
+
</template>
|
|
1233
|
+
</ul>
|
|
1234
|
+
`;
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
- ホストが `rows` を差し替えても、行フィールド(`rows.0.name`)だけを書いても、コンポーネント内の行に反映されます
|
|
1238
|
+
- コンポーネント側から `items.*.name` を書き戻すと、ホストの `rows` に届きます
|
|
1239
|
+
|
|
1240
|
+
#### 入れ子とスコープの重ね方
|
|
1241
|
+
|
|
1242
|
+
コンポーネント自身がホスト側の `for` の中にあり、**さらにコンポーネント内でも `for` を回す**入れ子構成にも対応しています。外側の行と内側の行の対応はフレームワークが保ちます:
|
|
1243
|
+
|
|
1244
|
+
```html
|
|
1245
|
+
<template data-wcs="for: groups">
|
|
1246
|
+
<my-list data-wcs="state.items: groups.*.children"></my-list>
|
|
1247
|
+
</template>
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
コンポーネントの中にさらにコンポーネントを置いて、**スコープを重ねる**こともできます。途中のコンポーネントが配列を素通しするだけで自分では `for` を回さなくても、正本スコープ起点の行フィールド書き込みは最下層の行まで届きます。
|
|
1251
|
+
|
|
1252
|
+
コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$updatedCallback` / `$getAll` はいずれも**自分のスコープ内の位置**を報告します。
|
|
1253
|
+
|
|
1159
1254
|
## Command Token(メソッドバインディング)
|
|
1160
1255
|
|
|
1161
1256
|
プロパティバインディング(`state.message: user.name`)はコンポーネントへ流れ込むデータを扱いますが、**state からコンポーネントのメソッドを起動する**こと —— `<wcs-fetch>.fetch()`、`<wcs-dialog>.open()` など —— はカバーしません。**command token** は型付きの pub/sub チャネルでこの隙間を埋めます:
|
|
@@ -1455,6 +1550,8 @@ event token は command token と同じ `Token` pub/sub プリミティブを共
|
|
|
1455
1550
|
|
|
1456
1551
|
command token / event token が運ぶのは離散的なやり取りです。**`$streams`** は残る形 —— 連続的なフローをカバーします。非同期 producer(async iterable / async generator / `ReadableStream`)を宣言すると、フレームワークがそれを **fold して単一の reactive プロパティに畳み込みます** —— 各チャンクは通常のパス代入を通るため、バインディング・パス getter・`$updatedCallback` は自分で値を代入した場合とまったく同じように反応します。`args` 関数が読んだ state パスが変化すると、実行中の producer は abort され、新しい引数で source が張り直されます(switchMap 型の依存駆動 restart)。stream は `$connectedCallback` 完了後に eager に起動し、要素の disconnect で abort されます。
|
|
1457
1552
|
|
|
1553
|
+
`$updatedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watch-watch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
|
|
1554
|
+
|
|
1458
1555
|
```html
|
|
1459
1556
|
<wcs-state>
|
|
1460
1557
|
<script type="module">
|
|
@@ -1530,6 +1627,68 @@ $streams: {
|
|
|
1530
1627
|
|
|
1531
1628
|
完全な契約 —— ライフサイクルと所有権・restart セマンティクス・flush 粒度・スコープ外リスト —— は [docs/streams.ja.md](docs/streams.ja.md) を参照してください。
|
|
1532
1629
|
|
|
1630
|
+
## Watch(`$watch`)
|
|
1631
|
+
|
|
1632
|
+
`$updatedCallback` は **binding 駆動** です。その更新で live DOM binding が実際に適用された path だけを報告するため、**描画していない値の変化は見えません**。**`$watch`** はその headless 版で、ページ上でそのパスがバインドされているかどうかに関わらず、state の変化で発火します(**ワイルドカードの行パスだけは例外**で、headless に成立させるには `$listKeys` が要ります。後述)。
|
|
1633
|
+
|
|
1634
|
+
```html
|
|
1635
|
+
<wcs-state>
|
|
1636
|
+
<script type="module">
|
|
1637
|
+
export default {
|
|
1638
|
+
isLoading: false,
|
|
1639
|
+
items: [],
|
|
1640
|
+
startedAt: 0,
|
|
1641
|
+
|
|
1642
|
+
$watch: {
|
|
1643
|
+
// 立ち上がり検出は cur/prev を自分で比較する
|
|
1644
|
+
isLoading(cur, prev) {
|
|
1645
|
+
if (cur === true && prev === false) { this.startedAt = Date.now(); }
|
|
1646
|
+
},
|
|
1647
|
+
|
|
1648
|
+
// ワイルドカードパスは変化した行ごとに 1 回発火する
|
|
1649
|
+
//(リストを `for` で描画しているか、`$listKeys` の宣言が要る。後述)
|
|
1650
|
+
"items.*.price"(cur, prev, index) {
|
|
1651
|
+
this.lastPriceChange = `#${index}: ${prev} → ${cur}`;
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
};
|
|
1655
|
+
</script>
|
|
1656
|
+
</wcs-state>
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
ハンドラの `this` は **writable** な state proxy なので書き戻せます。その書き込みは次の更新バッチに乗ります。戻り値は無視され、await もされません。
|
|
1660
|
+
|
|
1661
|
+
| 引数 | 契約 |
|
|
1662
|
+
|---|---|
|
|
1663
|
+
| `cur` | drain 時点の値(そのバッチの確定値) |
|
|
1664
|
+
| `prev` | **バッチ開始時点**の値(first-write-wins)。意味を持つのは**スカラのときだけ**(下記) |
|
|
1665
|
+
| `...indexes` | ワイルドカードパスのときのみ。そのスコープ自身のループ添字(`$1` / `$2` と同じ規約) |
|
|
1666
|
+
|
|
1667
|
+
**`prev` はスカラ限定です。** same-value guard が既に読んでいる旧値を再利用するため watch のための追加読みは発生せず、その帰結として参照型(in-place 変異では同じ参照になるため)・`$postUpdate` 経由・`config.sameValueGuard` オフのときは `undefined` になります。
|
|
1668
|
+
|
|
1669
|
+
**`$watch` は独自の発火条件を持ちません。** 更新バッチに載ったものをそのまま発火します。これはうまく噛み合っていて、同値の primitive 書き込みは enqueue 前に落ちている(=実質的に変化時のみ発火)一方、occurrence(`semantics: "event"` の property)は**意図的に**落とされないので `cur === prev` で発火します。エッジ検出が要るならハンドラ内で `cur` と `prev` を比較してください。
|
|
1670
|
+
|
|
1671
|
+
**getter を watch すると eager になります。** computed は本来 lazy で、依存は評価時にしか記録されません —— つまり描画していない getter は本来一度も発火しません。`$watch` に宣言すると接続時に 1 回評価され、以後は依存に触れたバッチの終端で毎回評価されます。`prev` は前回の評価値です。重い computed を watch すればその評価コストが毎バッチ乗り、getter 内の例外は watch 経由で表面化します。ワイルドカード getter(`items.*.tax`)は eager 化**しません**(初回評価がリスト全体を舐めることになるため)。この形は DOM にバインドされている場合にのみ発火し、`prev` は常に `undefined` です(行ごとの評価値を保持しないため)。
|
|
1672
|
+
|
|
1673
|
+
発火順序は 3 層に分かれ、利用者が意思を持てるのは真ん中の層だけです。
|
|
1674
|
+
|
|
1675
|
+
| 層 | 順序 | 制御 |
|
|
1676
|
+
|---|---|---|
|
|
1677
|
+
| 機構間 | `$updatedCallback` → `$watch` → `$streams` restart | 固定 |
|
|
1678
|
+
| ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
|
|
1679
|
+
| 同一パスの行間 | `indexes` 昇順 | 固定 |
|
|
1680
|
+
|
|
1681
|
+
主なルール:
|
|
1682
|
+
|
|
1683
|
+
- **自 state のみ** —— パスに `@stateName` は書けません。他の state 要素の watch は宣言時に拒否されます。
|
|
1684
|
+
- **中間値は観測できません** —— 1 バッチ内の `a → b → c` は `cur = c` / `prev = a` で 1 回だけ発火します(binding 更新と同じ契約)。
|
|
1685
|
+
- **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
|
|
1686
|
+
- **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。
|
|
1687
|
+
- **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$updatedCallback` とは異なる扱いです。
|
|
1688
|
+
- **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。32 段で打ち切り、コンソールに報告します(値と DOM は巻き戻しません)。
|
|
1689
|
+
- **mapped な `bind-component` の子では使えません** —— 子の state は `$` 始まりのプロパティを遮る proxy に包まれるため、宣言が届きません(`$streams` も同様)。plain な(マップされていない)子では宣言できます。
|
|
1690
|
+
- **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
|
|
1691
|
+
|
|
1533
1692
|
## Inputs と属性ミラー
|
|
1534
1693
|
|
|
1535
1694
|
`wcBindable.inputs` は一方向のプロパティ入力(state → 要素)を宣言します。エントリに `attribute` を設定すると、フレームワークはプロパティを書き込むたびにその値を当該 HTML 属性へも書き込むため、`attributeChangedCallback`・CSS の属性セレクタ・DevTools がすべてプロパティ値と同期し続けます。
|
|
@@ -1576,6 +1735,24 @@ chip.payload = null → element.data = null かつ removeAttribute("dat
|
|
|
1576
1735
|
- ミラーはベストエフォート: `setAttribute` の失敗は握りつぶされ(`debug` 警告付き)、プロパティ書き込みをブロックしない
|
|
1577
1736
|
- ネイティブ HTML 要素は `inputs` を完全に無視する —— ミラーは `static wcBindable` を公開するカスタム要素でのみ有効になる
|
|
1578
1737
|
|
|
1738
|
+
## コンポーネント機構の選び方
|
|
1739
|
+
|
|
1740
|
+
カスタム要素に独自の状態を持たせる機構は 2 つあり、**排他**です。コンポーネントごとにどちらか一方を選びます。
|
|
1741
|
+
|
|
1742
|
+
| | [DCC](#宣言的カスタムコンポーネント-dcc) | [`bind-component`](#web-component-バインディング) |
|
|
1743
|
+
|---|---|---|
|
|
1744
|
+
| 要素の定義方法 | HTML だけ(`data-wc-definition` + Declarative Shadow DOM) | 自分で書く `class extends HTMLElement` |
|
|
1745
|
+
| 状態の在処 | テンプレート内のインライン `<script type="module">`(インスタンスごとにロード) | コンポーネントインスタンスのプロパティ(`this.state`) |
|
|
1746
|
+
| `static wcBindable` | `$bindables` / `$commands` から生成 | **無し** — wc-bindable の producer ではない |
|
|
1747
|
+
| 親から値をバインド | `count: parentCount`(双方向・変更イベントあり) | `state.msg: user.name`(パスマッピング) |
|
|
1748
|
+
| 親からメソッド起動 | `command.bumpBy: $command.bump` | 不可 — クラス側に公開して自分で呼ぶ |
|
|
1749
|
+
| spread(`...: obj`) | 使える | 使えない(`wcBindable` 宣言が必要) |
|
|
1750
|
+
| コンポーネント自身の読み書き | 要素の `this.count` | `this.state.msg` |
|
|
1751
|
+
|
|
1752
|
+
判断の目安は「**JavaScript クラスが無いなら DCC、既にクラスを書いているなら `bind-component`**」です。併用はエラーになります。`data-wc-definition` ホストの中に `<wcs-state bind-component>` を置くのは設定ミスです — DCC の状態はテンプレートに属し、インスタンスごとにロードされるためです。
|
|
1753
|
+
|
|
1754
|
+
`bind-component` のコンポーネントが wc-bindable プロトコルの外に留まっているのは意図的です。宣言されたプロパティ面ではなく**パス**で配線するのがこの機構だからで、`wcBindable` 宣言を必要とする spread と command token が使えないのはその帰結です。
|
|
1755
|
+
|
|
1579
1756
|
## 宣言的カスタムコンポーネント (DCC)
|
|
1580
1757
|
|
|
1581
1758
|
JavaScript のクラス定義なしで、**HTML だけ**でカスタム要素を定義できます。`data-wc-definition` と Declarative Shadow DOM (`<template shadowrootmode>`) を使い、リアクティブな状態を持つ再利用可能なコンポーネントをインラインで宣言します。
|
|
@@ -1620,23 +1797,55 @@ JavaScript のクラス定義なしで、**HTML だけ**でカスタム要素を
|
|
|
1620
1797
|
[data-wc-definition] { display: none; }
|
|
1621
1798
|
```
|
|
1622
1799
|
|
|
1623
|
-
### `$bindables` と wc-bindable プロトコル
|
|
1800
|
+
### `$bindables` / `$commands` と wc-bindable プロトコル
|
|
1624
1801
|
|
|
1625
|
-
`$bindables`
|
|
1802
|
+
`$bindables` は変更イベント付きのコンポーネント**プロパティ**として公開する状態プロパティを、`$commands` は起動可能な**コマンド**として公開する状態メソッドを宣言します。この 2 つで [wc-bindable プロトコル](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/README.md)の宣言が組み立てられます:
|
|
1626
1803
|
|
|
1627
1804
|
```javascript
|
|
1628
1805
|
export default {
|
|
1629
1806
|
count: 0,
|
|
1630
|
-
|
|
1631
|
-
$bindables: ["count"]
|
|
1807
|
+
bumpBy(step) { this.count += step; },
|
|
1808
|
+
$bindables: ["count"],
|
|
1809
|
+
$commands: ["bumpBy"]
|
|
1632
1810
|
};
|
|
1633
1811
|
```
|
|
1634
1812
|
|
|
1635
1813
|
これにより以下が生成されます:
|
|
1636
1814
|
|
|
1637
|
-
- クラスの `static wcBindable` — フレームワークアダプタ用のプロトコルメタデータ。各 `$bindables` メンバは `properties` と `inputs` の両方に宣言され(双方向)、方向認識初期同期の下でも親 state → DCC の書き込みが機能します — [バインディング authority](#バインディング-authority-init--sync)
|
|
1638
|
-
-
|
|
1639
|
-
- `CustomEvent` のディスパッチ —
|
|
1815
|
+
- クラスの `static wcBindable` — フレームワークアダプタ用のプロトコルメタデータ。各 `$bindables` メンバは `properties` と `inputs` の両方に宣言され(双方向)、方向認識初期同期の下でも親 state → DCC の書き込みが機能します — [バインディング authority](#バインディング-authority-init--sync) 参照。各 `$commands` メンバは `commands` エントリになります
|
|
1816
|
+
- `$bindables` はプロトタイプの getter/setter、`$commands` はメソッド — いずれもリアクティブプロキシ経由
|
|
1817
|
+
- `CustomEvent` のディスパッチ — `$bindables` メンバが変更されるたびに `my-counter:count-changed` が発火
|
|
1818
|
+
|
|
1819
|
+
`commands` エントリは常に `async: true` です。DCC のメソッドは内側の `<wcs-state>` の初期化に chain するため、状態側のメソッドが `async` でなくても戻り値は Promise になります。
|
|
1820
|
+
|
|
1821
|
+
どちらの宣言もコンポーネント定義時に `$commandTokens` と同じ強度で検証されます。次はいずれもエラーになります。
|
|
1822
|
+
|
|
1823
|
+
- 配列でない
|
|
1824
|
+
- 非空文字列でないエントリ
|
|
1825
|
+
- `$` 始まりのエントリ(内部プロパティはコンポーネントの prototype に公開されません)
|
|
1826
|
+
- 重複したエントリ — これは従来サイレントに壊れていました。重複名があると `wcBindable` 宣言全体が読み取り不能になり、その要素が黙って双方向バインド不可になります
|
|
1827
|
+
- 状態に存在しないエントリ(自身とプロトタイプチェーンの両方を探索します。`$streams` の名前は値プロパティがインスタンスごとに実体化されるため「存在する」と見なされます)
|
|
1828
|
+
- `$bindables` にメソッドを書いた場合、または `$commands` に値プロパティを書いた場合
|
|
1829
|
+
|
|
1830
|
+
### DCC のメソッドを起動する
|
|
1831
|
+
|
|
1832
|
+
`$commands` メンバは、I/O ノードと同じように親 state から [command token](#command-tokenメソッドバインディング) で起動できます:
|
|
1833
|
+
|
|
1834
|
+
```html
|
|
1835
|
+
<wcs-state>
|
|
1836
|
+
<script type="module">
|
|
1837
|
+
export default {
|
|
1838
|
+
$commandTokens: ["bump"],
|
|
1839
|
+
fire() { this.$command.bump.emit(3); }
|
|
1840
|
+
};
|
|
1841
|
+
</script>
|
|
1842
|
+
</wcs-state>
|
|
1843
|
+
|
|
1844
|
+
<button data-wcs="onclick: fire">bump</button>
|
|
1845
|
+
<my-counter data-wcs="command.bumpBy: $command.bump"></my-counter>
|
|
1846
|
+
```
|
|
1847
|
+
|
|
1848
|
+
位置引数はそのまま素通しされるので、`emit(3)` はコンポーネント側の状態の `bumpBy(3)` を呼びます。
|
|
1640
1849
|
|
|
1641
1850
|
### DCC プロパティへのバインディング
|
|
1642
1851
|
|
|
@@ -1672,6 +1881,7 @@ export default {
|
|
|
1672
1881
|
| プロパティ | 用途 |
|
|
1673
1882
|
|----------|---------|
|
|
1674
1883
|
| `$bindables` | 観測可能プロパティの宣言 |
|
|
1884
|
+
| `$commands` | 起動可能メソッドの宣言 |
|
|
1675
1885
|
| `$connectedCallback` | ライフサイクルフック(各インスタンスで実行) |
|
|
1676
1886
|
| `$disconnectedCallback` | クリーンアップフック |
|
|
1677
1887
|
| `$updatedCallback` | 状態変更後に呼ばれる |
|
|
@@ -1719,14 +1929,14 @@ export default {
|
|
|
1719
1929
|
|---|---|---|
|
|
1720
1930
|
| `$connectedCallback` | 初回接続時は状態初期化後、再接続時は毎回呼び出し | 可(await される) |
|
|
1721
1931
|
| `$disconnectedCallback` | 要素が DOM から削除された時 | 不可(同期のみ) |
|
|
1722
|
-
| `$updatedCallback(paths, indexesListByPath)` |
|
|
1932
|
+
| `$updatedCallback(paths, indexesListByPath)` | live binding に更新が適用された後に呼び出し | 可(await されない) |
|
|
1723
1933
|
|
|
1724
1934
|
`$disconnectedCallback` を除くすべてのフックで `async` を使用できます。リアクティブ Proxy はすべてのプロパティへの代入を変更として検知します。そのため、標準の `async/await` による処理とプロパティへの直接代入だけで非同期ロジックが完結します。ローディングフラグの切り替え、取得したデータの格納、エラーメッセージの更新といった処理もすべて単なるプロパティ代入で行えるため、非同期状態を管理するための複雑な抽象化機能は必要ありません。
|
|
1725
1935
|
|
|
1726
1936
|
- フック内の `this` は読み書き可能な状態プロキシです。
|
|
1727
1937
|
- `$connectedCallback` は要素が接続される**たびに**呼ばれます(一度削除された後の再接続も含みます)。再確立が必要なセットアップ処理に適しています。
|
|
1728
1938
|
- `$disconnectedCallback` は同期的に呼び出されます。タイマーのクリア、イベントリスナーの削除、リソースの解放といったクリーンアップ処理に使用してください。
|
|
1729
|
-
- `$updatedCallback(paths, indexesListByPath)`
|
|
1939
|
+
- `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。`async` を使用できますが、戻り値は await されません。
|
|
1730
1940
|
- Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
|
|
1731
1941
|
|
|
1732
1942
|
## 設定
|