@wcstack/state 2.6.1 → 3.1.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 +112 -45
- package/README.md +114 -45
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/define.d.ts +295 -0
- package/dist/define.js +1 -0
- package/dist/index.d.ts +254 -94
- package/dist/index.esm.js +1 -23477
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.esm.js +22 -37
- package/dist/parser.d.ts +37 -39
- package/dist/parser.esm.js +172 -1153
- package/dist/split/chunks/binder.js +2 -0
- package/dist/split/chunks/binder.js.map +1 -0
- package/dist/split/chunks/checkDependency.js +2 -0
- package/dist/split/chunks/checkDependency.js.map +1 -0
- package/dist/split/chunks/diagnosticsHooks.js +2 -0
- package/dist/split/chunks/diagnosticsHooks.js.map +1 -0
- package/dist/split/chunks/errorGuidance.js +2 -0
- package/dist/split/chunks/errorGuidance.js.map +1 -0
- package/dist/split/chunks/features.d.ts +29 -0
- package/dist/split/chunks/filterRegistry.js +2 -0
- package/dist/split/chunks/filterRegistry.js.map +1 -0
- package/dist/split/chunks/getAllPropertyDescriptors.js +2 -0
- package/dist/split/chunks/getAllPropertyDescriptors.js.map +1 -0
- package/dist/split/chunks/lifecycleHooks.js +2 -0
- package/dist/split/chunks/lifecycleHooks.js.map +1 -0
- package/dist/split/chunks/mount.js +2 -0
- package/dist/split/chunks/mount.js.map +1 -0
- package/dist/split/chunks/pathDiagnostics.js +2 -0
- package/dist/split/chunks/pathDiagnostics.js.map +1 -0
- package/dist/split/chunks/registerComponents.js +2 -0
- package/dist/split/chunks/registerComponents.js.map +1 -0
- package/dist/split/chunks/setAll.js +2 -0
- package/dist/split/chunks/setAll.js.map +1 -0
- package/dist/split/chunks/version.js +2 -0
- package/dist/split/chunks/version.js.map +1 -0
- package/dist/split/chunks/watchRuntime.js +2 -0
- package/dist/split/chunks/watchRuntime.js.map +1 -0
- package/dist/split/core.d.ts +1417 -0
- package/dist/split/core.js +2 -0
- package/dist/split/core.js.map +1 -0
- package/dist/split/features/devtools.d.ts +9 -0
- package/dist/split/features/devtools.js +2 -0
- package/dist/split/features/devtools.js.map +1 -0
- package/dist/split/features/diagnostics.d.ts +11 -0
- package/dist/split/features/diagnostics.js +2 -0
- package/dist/split/features/diagnostics.js.map +1 -0
- package/dist/split/features/formats.d.ts +10 -0
- package/dist/split/features/formats.js +2 -0
- package/dist/split/features/formats.js.map +1 -0
- package/dist/split/features/recursion.d.ts +9 -0
- package/dist/split/features/recursion.js +2 -0
- package/dist/split/features/recursion.js.map +1 -0
- package/dist/split/features/scopes.d.ts +10 -0
- package/dist/split/features/scopes.js +2 -0
- package/dist/split/features/scopes.js.map +1 -0
- package/dist/split/features/ssr.d.ts +9 -0
- package/dist/split/features/ssr.js +2 -0
- package/dist/split/features/ssr.js.map +1 -0
- package/dist/split/features/temporal.d.ts +9 -0
- package/dist/split/features/temporal.js +2 -0
- package/dist/split/features/temporal.js.map +1 -0
- package/dist/wcs-manifest.json +1 -0
- package/package.json +13 -1
package/README.ja.md
CHANGED
|
@@ -177,6 +177,57 @@
|
|
|
177
177
|
</script>
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
### 分割エントリ(使う機能だけを入れる)
|
|
181
|
+
|
|
182
|
+
`@wcstack/state` と `/auto` は全機能入りで、こちらが引き続き推奨です(全部使うページなら、core +
|
|
183
|
+
機能に分けるより 1 ファイルのほうが小さい)。機能を意図的に落とすページは、組み合わせて入れられます。
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<script type="importmap">
|
|
187
|
+
{
|
|
188
|
+
"imports": {
|
|
189
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/core.js",
|
|
190
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/features/temporal.js",
|
|
191
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/features/scopes.js"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
<script type="module">
|
|
196
|
+
import { bootstrapState, installFeatures } from '@wcstack/state/core';
|
|
197
|
+
import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $streams
|
|
198
|
+
import scopes from '@wcstack/state/features/scopes'; // bind-component・mount=・DCC
|
|
199
|
+
|
|
200
|
+
installFeatures([temporal, scopes]);
|
|
201
|
+
bootstrapState();
|
|
202
|
+
</script>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
分割の形は、パッケージのファイルそのものから読みます — 上の例のように jsDelivr の素の `/npm/` パスで
|
|
206
|
+
版を固定するか(素のパスは `exports` を読まないので、`dist/split/` 以下のファイル名を書く)、バンドラを
|
|
207
|
+
通します。**`esm.run` からは読まないでください**。`+esm` はエントリごとにサーバー側で再バンドルし、
|
|
208
|
+
共有の core チャンクをそれぞれに取り込むため、エントリごとに別のエンジンを抱えます。機能は core から
|
|
209
|
+
見えないコピーへ install され、`installFeatures` を呼んでも `[wcs/feature-not-installed]` で落ちます。
|
|
210
|
+
素のファイルなら、どのエントリの相対 import も同じチャンクの URL に解決され、エンジンは 1 回だけ評価
|
|
211
|
+
されます。この形の integrity は [docs/sri.ja.md §5.1](../../docs/sri.ja.md#51-wcstackstate-の分割エントリ) を参照。
|
|
212
|
+
|
|
213
|
+
| エントリ | 足されるもの |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `@wcstack/state/core` | バインディングの本体: `data-wcs`・`for` / `if`・パス getter・フィルタ・イベント・`$command` / `$on`・`bootstrapState`・`installFeatures` |
|
|
216
|
+
| `@wcstack/state/features/temporal` | `$watch`・`$scan`・`$streams` |
|
|
217
|
+
| `@wcstack/state/features/scopes` | `bind-component`・`mount=` のボリューム・オーバーレイの公開 getter・DCC(`data-wc-definition`) |
|
|
218
|
+
| `@wcstack/state/features/recursion` | `$recursion` と `**` パス |
|
|
219
|
+
| `@wcstack/state/features/ssr` | `enable-ssr`: サーバー描画とハイドレーション |
|
|
220
|
+
| `@wcstack/state/features/formats` | 書式フィルタ群(`uc`・`date`・`round`・`truncate` …)。core が答えるのは `if` / `else` が要る `not` だけ |
|
|
221
|
+
| `@wcstack/state/features/devtools` | DevTools Hook Protocol への source 登録 |
|
|
222
|
+
| `@wcstack/state/features/diagnostics` | 開発時の警告: 束縛・`$watch`・`$scan` のパスが state 上で解決できないとき、did-you-mean 付きで知らせる。入れなければ静か(本番向け)。throw するエラーの文言はどちらでも変わらない |
|
|
223
|
+
| `@wcstack/state/define` | `defineState` と型だけ — ランタイムは 0 |
|
|
224
|
+
|
|
225
|
+
機能が入っていない宣言は黙って無視されません。state の定義時に
|
|
226
|
+
`[wcs/feature-not-installed] … install it with installFeatures([...]) from "@wcstack/state/features/…"`
|
|
227
|
+
で落ち、実装の無いフィルタは束縛計画の段で `[wcs/filter-unknown]` で落ちます。install は冪等で、
|
|
228
|
+
どのエントリも core のチャンクを 1 つだけ共有します(機能側がエンジンの 2 つ目のコピーを抱える
|
|
229
|
+
ことはありません)。
|
|
230
|
+
|
|
180
231
|
## 基本的な使い方
|
|
181
232
|
|
|
182
233
|
```html
|
|
@@ -284,6 +335,33 @@
|
|
|
284
335
|
|
|
285
336
|
ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。`$errorCallback` はルート専用です(バインディングの失敗はツリーの所有者へ 1 回だけ報告されます)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。ルートの `<wcs-state>` が初期化に失敗した場合、その時点で待機していたボリュームは永久に待たずに自分の報告を出して決着します。その報告が終点です —— 孤児として報告されたボリュームは後から自分で接ぎ木し直さないので、あとから修正版のルートを接続しても復帰しません。接ぎ木しないまま決着したボリューム(孤児・ロード失敗・接ぎ木失敗)はマウントの枠を返します。ロード中やルート待ちのあいだに外れたボリュームも枠を返します。そうしたボリュームは、同じ root へ付け直したときはその場で、それ以外は接ぎ木の直前に枠を取り直し、枠が空いていれば従来どおり(外れたままでも)接ぎ木します。外れている間に別のボリュームが枠を取っていた場合は、それを報告して接ぎ木しません。ボリュームの `$connectedCallback` が同期で投げた場合も非同期の失敗と同じく報告に留まり、接ぎ木は済んだものとして扱います。ページを読み直さずに復旧するには、壊れたルートと孤児のボリュームを取り除いて新しい要素を追加してください。接ぎ木済みのボリュームは、外してもデータがツリーに残るので枠を握ったままです。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。初期化後に `mount` 属性を変更することはできません — 変更は console 警告付きで無視されます。要素を取り除き、望むパスで新しい要素を追加してください。
|
|
286
337
|
|
|
338
|
+
**ルートのパスをボリュームへ注入する(3.1)。** ボリュームのコードが自分の外のパスを読むには、ボリューム要素の `data-wcs` にコンポーネントの部分マウントと同じ形で書きます:
|
|
339
|
+
|
|
340
|
+
```html
|
|
341
|
+
<wcs-state mount="cart" src="./cart.js" data-wcs="state.taxRate: settings.taxRate"></wcs-state>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`cart.js` の中(getter・メソッド・`$watch`・`$listKeys`・ライフサイクル)では `this.taxRate` がルートの `settings.taxRate` を読み書きし、依存も張られるので、`settings.taxRate` が変われば `get total()` も再評価されます。`$watch: { taxRate() {…} }` は `settings.taxRate` の変化で発火し、`$updatedCallback` はその更新を内側の名前 `taxRate` で受けます。注入はボリュームのコードの中の名前です — ページは今までどおり `settings.taxRate` を読み、`cart.taxRate` はツリーにありません。
|
|
345
|
+
|
|
346
|
+
- **1 キーずつ**: 左辺は `state.<キー>` の 1 段です(`state: …` でボリューム全体を動かすことはできません — `mount` を変えてください)。
|
|
347
|
+
- **注入が自前のキーに勝つ**: ボリュームが同名のデータキー(`taxRate: 0`)を宣言していても、その値は接ぎ木されません。同名の getter・setter・メソッドはどちらが `this.taxRate` か曖昧なので、そのボリュームは接ぎ木せずにエラーを報告します。
|
|
348
|
+
- **`#ro`**: `state.taxRate#ro: settings.taxRate` なら読めても書けません。ボリュームのコードからの代入・`$setAll`・書きの `$resolve` は `[wcs/mount-readonly]` を投げます(ルート自身は書けます)。
|
|
349
|
+
- **静的なパスだけ**: 右辺にワイルドカード(`items.*.x`)や `$` 始まりのパスは使えません(ボリュームにはループ文脈がありません)。フィルタも受けません — 変換は getter で行ってください。形の誤りは `[wcs/mount-path-invalid]` で、ロードの前に報告されます。
|
|
350
|
+
|
|
351
|
+
**スコープごとに動くもの**(3.0 で 1 つの表にしました — 要件 B11。ここに無言で無視されるものはありません):
|
|
352
|
+
|
|
353
|
+
| 宣言 | ルートの `<wcs-state>` | ボリューム `<wcs-state mount="p">` | マウントされたコンポーネント(`state: …` 付きの `bind-component`) |
|
|
354
|
+
|---|---|---|---|
|
|
355
|
+
| データキー・getter・setter・メソッド | 動く | 動く(`p` 相対) | 動く — 自前のキーはホストがマップしない限り私有、getter は公開される |
|
|
356
|
+
| `$connectedCallback` / `$disconnectedCallback` | 動く | 動く(`p` 相対) | 動く |
|
|
357
|
+
| `$watch`・`$listKeys`・`$updatedCallback` | 動く | 動く(`p` 相対) | 動かない — `wcs/mount-dollar-declaration` を 1 回警告 |
|
|
358
|
+
| `$streams`・`$scan`・`$recursion`・`**` の getter | 動く | 接ぎ木の前にエラーで拒否 | 動かない — 警告 |
|
|
359
|
+
| `$commandTokens`・`$eventTokens`・`$on` | 動く | 動かない — 警告 | 動かない — 警告 |
|
|
360
|
+
| `$errorCallback` | 動く | 動かない — 警告(3.0 より前は無言) | 動かない — 警告(3.0 より前は無言) |
|
|
361
|
+
| 初期化の失敗 | 1 回報告し、`connectedCallbackPromise` を reject | 接ぎ木せずに決着して枠を返す。`connectedCallbackPromise` は resolve(`scopes` 機能が無いときは reject) | 1 回報告し、コンポーネントの `connectedCallbackPromise` を reject |
|
|
362
|
+
|
|
363
|
+
マウントされていないコンポーネント(自前の `<wcs-state>` を持つ素の Shadow DOM の子)はそれ自体がルートで、1 列目のすべてが動きます。
|
|
364
|
+
|
|
287
365
|
> **v1 の名前付き状態からの移行:** `<wcs-state name="cart">` + `total@cart` は `<wcs-state mount="cart">` + `cart.total` になります。v2 では `name` 属性は fail-fast し、パス中の `@` は parse error です(どちらもこの誘導文付き)。移行の対応表: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9。
|
|
288
366
|
|
|
289
367
|
## 状態の更新
|
|
@@ -346,6 +424,8 @@ property[#modifier]: path[|filter[|filter(args)...]]
|
|
|
346
424
|
<div data-wcs="textContent: count; class.over: count|gt(10)"></div>
|
|
347
425
|
```
|
|
348
426
|
|
|
427
|
+
区切りの `;` と `|` は引用符の外でだけ区切るので(3.0)、引用符付きのフィルタ引数に含められます: `textContent: tags|join('; ')`、`title: parts|join(' | ')`。3.0 より前はどちらも束縛が壊れていました。
|
|
428
|
+
|
|
349
429
|
| 要素 | 説明 | 例 |
|
|
350
430
|
|---|---|---|
|
|
351
431
|
| `property` | バインドする DOM プロパティ | `value`, `textContent`, `checked` |
|
|
@@ -368,8 +448,15 @@ property[#modifier]: path[|filter[|filter(args)...]]
|
|
|
368
448
|
| `radio` | ラジオボタングループバインディング(双方向) |
|
|
369
449
|
| `checkbox` | チェックボックスグループの配列バインディング(双方向) |
|
|
370
450
|
| `onclick`, `on*` | イベントハンドラバインディング |
|
|
451
|
+
| `.NAME` | 明示のプロパティ(3.1)— `on` で始まってもイベントにしない |
|
|
371
452
|
|
|
372
|
-
`
|
|
453
|
+
**`on` で始まるプロパティ(3.1)。** 名前が `on` で始まる束縛はイベントになります(`onclick:`)。そのため `online: x` は "line" イベントを待ち、要素の `online` プロパティには書きません。`on` で始まるプロパティに値を書くには、左辺の先頭にドットを付けます:
|
|
454
|
+
|
|
455
|
+
```html
|
|
456
|
+
<my-status data-wcs=".online: isOnline; onclick: refresh"></my-status>
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
ドットの付いた形はドットの無い形と同じ束縛で、イベントにならない点だけが違います(`.value:` は `value:` と同じく双方向、修飾子と入力フィルタもそのまま使えます)。名前空間の語(`.class` / `.attr` / `.style` / `.command` / `.eventToken`)と空の名前は `[wcs/binding-syntax]` で拒否します。
|
|
373
460
|
|
|
374
461
|
### 修飾子
|
|
375
462
|
|
|
@@ -384,6 +471,10 @@ property[#modifier]: path[|filter[|filter(args)...]]
|
|
|
384
471
|
|
|
385
472
|
複数の修飾子は 1 つの `#` の後にカンマ区切りで書きます: `value#ro,init=none: path`
|
|
386
473
|
|
|
474
|
+
修飾子は束縛の種別を変えません: `radio#ro:` と `checkbox#ro:` は radio / checkbox の束縛のままです(3.0)。3.0 より前は修飾子を付けると `radio` という名前の素のプロパティ束縛になり、radio グループの `#ro` が効いていませんでした。
|
|
475
|
+
|
|
476
|
+
3.0 からパーサは、以前黙って丸めていた書き方を `[wcs/binding-syntax]` で拒否します: 2 つ目の `#`(`value#ro#wo` は `ro` だけ残して後ろを捨てていた — `value#ro,wo` と書く)、`else:` の後ろの値(`else:` と書く)、`for` / `if` / `elseif` / `else` / `...` の左辺の修飾子やフィルタ、フィルタ引数の閉じていない引用符。
|
|
477
|
+
|
|
387
478
|
### 双方向バインディング
|
|
388
479
|
|
|
389
480
|
以下の要素で自動的に有効化されます:
|
|
@@ -561,7 +652,7 @@ export default {
|
|
|
561
652
|
<wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
|
|
562
653
|
```
|
|
563
654
|
|
|
564
|
-
**`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null`
|
|
655
|
+
**`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。**表示の表面は別です(3.0):** `textContent` / `innerText` / `innerHTML`・mustache のテキスト・`attr.*`・`style.*` には生かすべき要素側の既定値が無いので、`undefined` も `null` も「値が無い」— テキストは空になり、属性やスタイルは削除されます。3.0 より前は `textContent:` も `undefined` をスキップしていたため使い回したリストの行に前の行の文字が残り、属性には文字列 `"undefined"` / `"null"` が入っていました。このスキップは spread に限らずすべてのプロパティバインディングに適用され、`config.debug` 時はスキップごとに `console.debug` でログが出ます。
|
|
565
656
|
|
|
566
657
|
**制約事項**:
|
|
567
658
|
|
|
@@ -1021,13 +1112,13 @@ export default {
|
|
|
1021
1112
|
};
|
|
1022
1113
|
```
|
|
1023
1114
|
|
|
1024
|
-
規則:
|
|
1115
|
+
規則: `$eq` と `$eqPath` は getter の中で評価されれば購読を張ります — 行の getter でも、`get isAdmin() { return this.$eq("role", "admin"); }` のようなトップレベルの getter でも同じです。メソッドやハンドラから呼ぶと比較結果を返すだけです。`$eqIndex` にはリスト行が要ります: 行の外では `$eqIndex("…") needs a list row scope.` を投げ、その深さにリストの index が無い `level` でも投げます。行の購読はリスト差分がその行を外した時点で落ちます。鍵の比較は `Object.is` ですが、Map の意味論により `+0` と `-0`、`NaN` 同士は同じ鍵になります。`$eqPath` は鍵を依存なしで読むので、行の鍵がその場で書き換わっても行は再評価されません — 変わらない同一性(id)に使い、鍵自体が動く場合は追跡付きの読みと `$eq` を組み合わせてください。
|
|
1025
1116
|
|
|
1026
1117
|
行に届くもの:
|
|
1027
1118
|
|
|
1028
1119
|
- **`path` への書き込み** — オブジェクトを含むどんな値でも(`$eq("selected", this["items.*"])` に対する `this.selected = row`): 選ばれていた行と、新たに選ばれる行。
|
|
1029
1120
|
- **`path` より上のオブジェクトへの書き込み** — `$eq("sel.id", …)` に対する `this.sel = { id: 2 }`: 同じく 2 行。旧いオブジェクトの下と新しいオブジェクトの下で `path` が持つ値を鍵にします。
|
|
1030
|
-
- **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId
|
|
1121
|
+
- **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。`@wcstack/devtools` の State ペインは、path ごとの購読の数を **Keyed selection** に出し、この形に落ちた path には `tracked` バッジを付けます(3.0)。
|
|
1031
1122
|
- **型変換はしません:** `"2"` は id `2` に一致しません。`<input>` や `<select>` は文字列を書くので、入口で変換する(`value|number: selectedId`)か、id を文字列で持ってください。
|
|
1032
1123
|
|
|
1033
1124
|
### ループインデックス変数(`$1`, `$2`, ...)
|
|
@@ -1159,7 +1250,7 @@ this.$setAll("users.*", [], rows, { spread: true }); // 配列を保ったま
|
|
|
1159
1250
|
|
|
1160
1251
|
#### `$resolve` — 明示的なインデックスでのアクセス
|
|
1161
1252
|
|
|
1162
|
-
`$resolve`
|
|
1253
|
+
`$resolve` は特定のワイルドカードインデックスの値を読み書きします。**読みか書きかは引数の個数で決まります**(3.0): `$resolve(path, indexes)` は読み、`$resolve(path, indexes, value)` は `value` の書き込みで、`undefined` も書きます(3.0 より前は第 3 引数が `undefined` なら読みでした)。readonly のプロキシでは、この書き込みは直接代入と同じく `This state is readonly.` で throw し、`$setAll` も同じです(3.0 より前はどちらも readonly のプロキシから書けていました):
|
|
1163
1254
|
|
|
1164
1255
|
```javascript
|
|
1165
1256
|
export default {
|
|
@@ -1422,8 +1513,6 @@ export default {
|
|
|
1422
1513
|
| `gt(n)` | より大きい | `count\|gt(0)` |
|
|
1423
1514
|
| `ge(n)` | 以上 | `count\|ge(0)` |
|
|
1424
1515
|
|
|
1425
|
-
`eq` / `ne` は数値の値を数値として、それ以外を引数の**文字列**と比べます。そのため 2.x では真偽値が `eq(true)` に一致することはなく、`done|eq(true)` は常に `false` です。真偽値そのものをバインドする(`class.done: .done`)か `not` を使ってください。3.0 は引用符の無い `true` / `false` / `null` を型付きの値として読みます([3.0 への準備](#30-への準備wcsv3-migration))。
|
|
1426
|
-
|
|
1427
1516
|
### 算術
|
|
1428
1517
|
|
|
1429
1518
|
| フィルタ | 説明 | 例 |
|
|
@@ -1489,8 +1578,8 @@ export default {
|
|
|
1489
1578
|
|
|
1490
1579
|
| フィルタ | 説明 | 例 |
|
|
1491
1580
|
|---|---|---|
|
|
1492
|
-
| `truthy` | truthy チェック | `value\|truthy` |
|
|
1493
|
-
| `falsy` | falsy
|
|
1581
|
+
| `truthy` | truthy チェック — JavaScript の真偽判定そのもので `boolean` と同じ(3.0 から `0n` は偽) | `value\|truthy` |
|
|
1582
|
+
| `falsy` | falsy チェック(JavaScript の真偽判定。`defaults` も同じ判定を使う) | `value\|falsy` |
|
|
1494
1583
|
| `defaults(v)` | フォールバック値 | `name\|defaults(Anonymous)` |
|
|
1495
1584
|
|
|
1496
1585
|
### フィルタチェーン
|
|
@@ -1501,6 +1590,10 @@ export default {
|
|
|
1501
1590
|
<div data-wcs="textContent: price|mul(1.1)|round(2)|locale(ja-JP)"></div>
|
|
1502
1591
|
```
|
|
1503
1592
|
|
|
1593
|
+
フィルタは束縛計画の段で解決されます。未知の名前は `[wcs/filter-unknown]`(did-you-mean 付き)で、3.0 からは受け付ける個数の外の引数も `[wcs/filter-arity]`(`join(a,b)`: "accepts at most 1 argument(s) (2 given)")で落ちます — lint と同じコード・同じ範囲です。引数は構造のままキャッシュされるので、`join('a,b')` と `join(a)` を取り違えません。
|
|
1594
|
+
|
|
1595
|
+
**引数のリテラルは型を持ちます(3.0)。** 引用符の無い `true` / `false` / `null` / 数値はその値、引用符付きの引数は文字列です。比較フィルタは真偽値と `null` の比較にこれを使います: `done|eq(true)` は `true` に一致し(3.0 より前は文字列 `"true"` と比べていて一致しなかった)、`eq('true')` は一致せず、`eq(null)` は `null` に一致します。数値と文字列の比べ方は変わりません — 数値の値は数と、文字列の値は原文と比べるので、フォームの値 `"1"` は今も `eq(1)` に一致します。`defaults(v)` は型付きの値を返します: `defaults(0)` は `0`、`defaults('0')` は `"0"`、`defaults(null)` は `null`。
|
|
1596
|
+
|
|
1504
1597
|
## Web Component バインディング
|
|
1505
1598
|
|
|
1506
1599
|
`@wcstack/state` は Shadow DOM または Light DOM を使用したカスタム要素との双方向状態バインディングに対応しています。
|
|
@@ -1618,10 +1711,16 @@ customElements.define("user-card", UserCard);
|
|
|
1618
1711
|
- 配列そのものをルートにマウントする形(`state: rows` + 中で `for`)は非対応です。行をマウントする(`state: .`)か、配列を持つオブジェクトをマウントして中で `for` を回してください(`state: group` + `for: children`)。どちらも契約テストで固定されており、マウントがツリー拡張の唯一の手段です
|
|
1619
1712
|
|
|
1620
1713
|
> プロパティ単位の形(`state.message: user.name`)はそのまま動きます — 同じ機構の上の部分マウントです。
|
|
1621
|
-
>
|
|
1622
|
-
> 宣言しているコンポーネント(`state = { message: "" }` + `state.message:
|
|
1623
|
-
>
|
|
1624
|
-
>
|
|
1714
|
+
> **明示した部分マウントはコンポーネント自前のキーに勝ちます(3.0):** マップされるキーに既定値を
|
|
1715
|
+
> 宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)もホストの値を読み、
|
|
1716
|
+
> 既定値は使われません(2.x では R1 によって自前のキーが私有になり、ホストの値を隠していました —
|
|
1717
|
+
> `wcs/mount-own-key-shadow` の警告付き)。マップしていない自前のキーは今も R1 で私有です。
|
|
1718
|
+
>
|
|
1719
|
+
> **マウントの `#ro` を尊重します(3.0):** `state#ro: user` や `state.title#ro: doc.title` では、
|
|
1720
|
+
> コンポーネントはそのエントリを読めても書けません — `element.state.title = …`・メソッド内の
|
|
1721
|
+
> `this.title = …`・`$setAll` / `$resolve` の書き込みは `[wcs/mount-readonly]` で落ち、コンポーネント内の
|
|
1722
|
+
> 双方向束縛(`value: title`)は書き戻しません。ホストはそのパスに書けます(2.x は修飾子を受け付けて
|
|
1723
|
+
> 無視していました)。Light DOM のマウントに `name` は不要で、
|
|
1625
1724
|
> `element.state`(および getter / メソッド内の `this`)の `$getAll` / `$setAll` / `$resolve` /
|
|
1626
1725
|
> `$postUpdate` はコンポーネント自身の語彙で書けます — パスはマウント先へ翻訳され、ホスト行の
|
|
1627
1726
|
> 添字は自動で前置されます。
|
|
@@ -2679,38 +2778,6 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
2679
2778
|
| `$watch` の書き込み連鎖 | 32 | そのバッチの watch 発火をスキップ |
|
|
2680
2779
|
| バインディングの適用失敗 | — | その 1 本のみスキップ |
|
|
2681
2780
|
|
|
2682
|
-
### 3.0 への準備(`wcs/v3-migration`)
|
|
2683
|
-
|
|
2684
|
-
3.0 は互換層を持ちません。代わりにこのリリースは、3.0 が拒否する書き方・読み方が変わる書き方を 2.x のまま動かしつつ名指しで知らせます。警告は書き方とサイトごとに 1 回だけ、コード `wcs/v3-migration` で出し、3.0 での扱いと今の書き換え先を示します:
|
|
2685
|
-
|
|
2686
|
-
```
|
|
2687
|
-
[@wcstack/state] [wcs/v3-migration] "value#ro#wo": 3.0 rejects a second "#". Write "value#ro,wo".
|
|
2688
|
-
See "Preparing for 3.0" in the @wcstack/state README.
|
|
2689
|
-
```
|
|
2690
|
-
|
|
2691
|
-
| 書き方 | 2.x | 3.0 | 今の書き方 |
|
|
2692
|
-
|---|---|---|---|
|
|
2693
|
-
| 2 つ目の `#`(`value#ro#wo:`) | 最初の修飾子列だけ残す | `[wcs/binding-syntax]` | `value#ro,wo:` |
|
|
2694
|
-
| `else:` の後ろの値 | 無視 | `[wcs/binding-syntax]` | `else:` |
|
|
2695
|
-
| `for` / `if` / `elseif` / `else` / `...` の修飾子・フィルタ | ただのプロパティのバインディングになる | `[wcs/binding-syntax]` | キーワードだけ |
|
|
2696
|
-
| `radio#ro:` / `checkbox#ro:` | `radio` という名前のプロパティになり効かない | 修飾子を守る radio / checkbox のバインディング | —(意図どおりか確かめる) |
|
|
2697
|
-
| フィルタ引数の閉じていない引用符 | 黙って閉じる | `[wcs/binding-syntax]` | 引用符を閉じる |
|
|
2698
|
-
| フィルタが受け取る数を超える引数 | 余りは無視 | `[wcs/filter-arity]` | 余りを消す |
|
|
2699
|
-
| `eq` / `ne` の引用符の無い `true` / `false` / `null`(`eq(true)`) | 真偽値・`null` の値を文字列と比べる | 型付きの値と比べる | 文字列で比べ続けるなら `eq('true')` |
|
|
2700
|
-
| `defaults` の引用符の無い `true` / `false` / `null` | 文字列(`"null"`)を既定値にする | 型付きの値を既定値にする | 文字列のままなら `defaults('null')` |
|
|
2701
|
-
| `truthy` / `falsy` / `defaults` に来た `0n` | 真 | 偽(JavaScript の真偽判定) | — |
|
|
2702
|
-
| `textContent` / `innerText` / `innerHTML` への `undefined` | 前のテキストを残す(使い回した行では前の行のもの) | 空にする | 「値なし」は `""` か `null` を返す |
|
|
2703
|
-
| `attr.*` への `undefined` / `null` | `"undefined"` / `"null"` を書く | 属性を削除する | — |
|
|
2704
|
-
| `style.*` への `undefined` | 前の値を残す | 消す | — |
|
|
2705
|
-
| `$resolve(path, indexes, undefined)` | 読む | `undefined` を書く(引数の個数で決める) | 読むなら `$resolve(path, indexes)` |
|
|
2706
|
-
| readonly のプロキシからの `$resolve(path, indexes, value)` / `$setAll` | 書く | `This state is readonly.` を投げる | `createState("writable", …)` から書く |
|
|
2707
|
-
| `#ro` のマウント(`state#ro: user`)を通るコンポーネントの書き込み | ホストのツリーに書く | `[wcs/mount-readonly]` | ホストで書くか、`#ro` を外す |
|
|
2708
|
-
| 部分マウントに隠された自前の既定値(コンポーネントが `name` を宣言し、`state.name: user.name`) | 既定値が勝つ(警告あり) | 明示したマウントが勝つ | 既定値を消す |
|
|
2709
|
-
|
|
2710
|
-
もう 1 つは予告でなく先に入れています:ボリュームやマウントされたコンポーネントの `$errorCallback` は黙って無視していましたが、ルート専用のキー(`$commandTokens`、`$on` など)を挙げる既存の警告で名指しするようにしました(3.0 と同じ)。動くのは今もルートの state だけです。
|
|
2711
|
-
|
|
2712
|
-
値の警告はその値が実際に来たときにだけ出ます。コンソールが静かでも、テストが通らなかった経路については何も分かりません。文法の行はバインディングを最初に解析したときに調べます。`npx @wcstack/lint <file>`(と VS Code 拡張)も同じ判定で `wcs/v3-migration`(info)として報告するので、ページを動かさずに見つけられます。
|
|
2713
|
-
|
|
2714
2781
|
## 設定
|
|
2715
2782
|
|
|
2716
2783
|
`bootstrapState()` に部分的な設定オブジェクトを渡します:
|
package/README.md
CHANGED
|
@@ -177,6 +177,58 @@ Every row is a section of this README. Unless it appears under [Where the neighb
|
|
|
177
177
|
</script>
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
### Split entries (only the features you use)
|
|
181
|
+
|
|
182
|
+
`@wcstack/state` and `/auto` ship everything, and they stay the recommended way in: a page that
|
|
183
|
+
uses every feature is smaller as one file than as core plus features. When a page deliberately
|
|
184
|
+
leaves features out, it can compose them instead:
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
<script type="importmap">
|
|
188
|
+
{
|
|
189
|
+
"imports": {
|
|
190
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/core.js",
|
|
191
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/features/temporal.js",
|
|
192
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@3.1.0/dist/split/features/scopes.js"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
</script>
|
|
196
|
+
<script type="module">
|
|
197
|
+
import { bootstrapState, installFeatures } from '@wcstack/state/core';
|
|
198
|
+
import temporal from '@wcstack/state/features/temporal'; // $watch / $scan / $streams
|
|
199
|
+
import scopes from '@wcstack/state/features/scopes'; // bind-component, mount=, DCC
|
|
200
|
+
|
|
201
|
+
installFeatures([temporal, scopes]);
|
|
202
|
+
bootstrapState();
|
|
203
|
+
</script>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Load the split form from the package's own files — jsDelivr's plain, version-pinned `/npm/` path as
|
|
207
|
+
above (it does not read `exports`, so name the file under `dist/split/`), or a bundler — and **never
|
|
208
|
+
through `esm.run`**. Its `+esm` endpoint re-bundles every entry on the server and inlines the shared
|
|
209
|
+
core chunk into each one, so every entry would carry its own engine: a feature would install into a
|
|
210
|
+
copy the core never sees, and the page throws `[wcs/feature-not-installed]` in spite of
|
|
211
|
+
`installFeatures`. From the plain files, every entry's relative import resolves to the same chunk URL,
|
|
212
|
+
so the browser evaluates the engine once. Integrity for this form: [docs/sri.md §5.1](../../docs/sri.md#51-the-split-entries-of-wcstackstate).
|
|
213
|
+
|
|
214
|
+
| Entry | What it adds |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `@wcstack/state/core` | The binding engine: `data-wcs`, `for` / `if`, path getters, filters, events, `$command` / `$on`, `bootstrapState`, `installFeatures` |
|
|
217
|
+
| `@wcstack/state/features/temporal` | `$watch`, `$scan`, `$streams` |
|
|
218
|
+
| `@wcstack/state/features/scopes` | `bind-component`, `mount=` volumes, overlay exports, DCC (`data-wc-definition`) |
|
|
219
|
+
| `@wcstack/state/features/recursion` | `$recursion` and `**` paths |
|
|
220
|
+
| `@wcstack/state/features/ssr` | `enable-ssr`: server rendering and hydration |
|
|
221
|
+
| `@wcstack/state/features/formats` | The formatting filters (`uc`, `date`, `round`, `truncate`, …). The core answers only `not`, which `if` / `else` need |
|
|
222
|
+
| `@wcstack/state/features/devtools` | The DevTools hook protocol source |
|
|
223
|
+
| `@wcstack/state/features/diagnostics` | Development-time warnings: a bound / `$watch` / `$scan` path that does not resolve on the state is reported with a did-you-mean. Without it the page stays silent — thrown errors keep their full messages either way |
|
|
224
|
+
| `@wcstack/state/define` | `defineState` and the types only — no runtime at all |
|
|
225
|
+
|
|
226
|
+
A declaration whose feature is missing does not fail quietly: it throws
|
|
227
|
+
`[wcs/feature-not-installed] … install it with installFeatures([...]) from "@wcstack/state/features/…"`
|
|
228
|
+
when the state is defined, and a filter with no implementation throws `[wcs/filter-unknown]` when the
|
|
229
|
+
bindings are planned. Installing is idempotent, and every entry shares one core chunk (a feature
|
|
230
|
+
never carries a second copy of the engine).
|
|
231
|
+
|
|
180
232
|
## Basic Usage
|
|
181
233
|
|
|
182
234
|
```html
|
|
@@ -284,6 +336,33 @@ There is **one state tree per root**. To split state across modules, mount a vol
|
|
|
284
336
|
|
|
285
337
|
A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. `$errorCallback` is root-only (a binding failure is reported once, to the tree's owner). Load order does not matter (a volume connected before the root is grafted when the root registers). If the root `<wcs-state>` fails to initialize, the volumes already waiting for it settle with a report of their own instead of waiting forever. That report is the end of the line for those volumes: a volume reported as an orphan does not graft itself later, so connecting a corrected root afterwards does not bring it back. A volume that settles without grafting — orphaned, failed to load, or failed to graft — releases its mount slot, and so does a volume detached while it is still loading or waiting for its root. Such a volume takes the slot back when it is re-attached to the same root, or otherwise just before it grafts, and still grafts as before when the slot is free — even while detached; if another volume took the slot in the meantime, it reports that and does not graft. A synchronous throw from a volume's `$connectedCallback` is reported like an asynchronous one, and the volume counts as grafted. To recover without reloading the page, remove the broken root and the orphaned volumes and add new elements. A grafted volume keeps its slot even when detached, because its data stays in the tree. Mount paths must be static (`*`, `$`, `#`, `@` are rejected). Changing `mount` after the element has initialized is not supported: the change is ignored with a console warning — remove the element and add a new one with the desired path.
|
|
286
338
|
|
|
339
|
+
**Injecting root paths into a volume (3.1).** When a volume's code needs a path outside its own subtree, write the injection on the volume element's `data-wcs`, in the same form as a component's partial mount:
|
|
340
|
+
|
|
341
|
+
```html
|
|
342
|
+
<wcs-state mount="cart" src="./cart.js" data-wcs="state.taxRate: settings.taxRate"></wcs-state>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Inside `cart.js` — getters, methods, `$watch`, `$listKeys` and the lifecycle callbacks — `this.taxRate` reads and writes the root's `settings.taxRate`. The read records a dependency, so `get total()` re-evaluates when `settings.taxRate` changes. `$watch: { taxRate() {…} }` fires on changes to `settings.taxRate`, and `$updatedCallback` receives that update under the inner name `taxRate`. The injected name exists only inside the volume's code: the page keeps reading `settings.taxRate`, and the tree has no `cart.taxRate`.
|
|
346
|
+
|
|
347
|
+
- **One key at a time.** The left side is a single `state.<key>`. `state: …` cannot move the whole volume; change `mount` for that.
|
|
348
|
+
- **The injection wins over the volume's own key.** A data key of the same name (`taxRate: 0`) is not grafted. An accessor or method of the same name would make `this.taxRate` ambiguous, so that volume reports an error and does not graft.
|
|
349
|
+
- **`#ro`.** With `state.taxRate#ro: settings.taxRate`, the value can be read but not written. An assignment from the volume's code, `$setAll`, or a writing `$resolve` throws `[wcs/mount-readonly]`; the root itself can still write the path.
|
|
350
|
+
- **Static paths only.** The right side cannot contain a wildcard (`items.*.x`) or start with `$`, because a volume has no loop context. Filters are not accepted either; derive the value in a getter. A malformed injection is reported as `[wcs/mount-path-invalid]` before the volume loads.
|
|
351
|
+
|
|
352
|
+
**What each scope runs** (3.0 states it as one table — requirement B11; nothing here is ignored silently):
|
|
353
|
+
|
|
354
|
+
| Declaration | Root `<wcs-state>` | Volume `<wcs-state mount="p">` | Mounted component (`bind-component` with `state: …`) |
|
|
355
|
+
|---|---|---|---|
|
|
356
|
+
| Data keys, getters, setters, methods | yes | yes, relative to `p` | yes — own keys are private unless the host maps them; getters are exported |
|
|
357
|
+
| `$connectedCallback` / `$disconnectedCallback` | yes | yes, relative to `p` | yes |
|
|
358
|
+
| `$watch`, `$listKeys`, `$updatedCallback` | yes | yes, relative to `p` | not run — one `wcs/mount-dollar-declaration` warning |
|
|
359
|
+
| `$streams`, `$scan`, `$recursion`, `**` getters | yes | rejected with an error before grafting | not run — warning |
|
|
360
|
+
| `$commandTokens`, `$eventTokens`, `$on` | yes | not run — warning | not run — warning |
|
|
361
|
+
| `$errorCallback` | yes | not run — warning (silent before 3.0) | not run — warning (silent before 3.0) |
|
|
362
|
+
| An initialization failure | reported once; `connectedCallbackPromise` rejects | settles without grafting and releases its slot; `connectedCallbackPromise` resolves (a missing `scopes` feature rejects it) | reported once; the component's `connectedCallbackPromise` rejects |
|
|
363
|
+
|
|
364
|
+
A component that is not mounted (a plain Shadow DOM child with its own `<wcs-state>`) is a root of its own and runs everything in the first column.
|
|
365
|
+
|
|
287
366
|
> **Migrating from v1's named states:** `<wcs-state name="cart">` + `total@cart` becomes `<wcs-state mount="cart">` + `cart.total`. In v2 the `name` attribute fails fast and `@` in a path is a parse error, each with this exact guidance. Migration table: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9.
|
|
288
367
|
|
|
289
368
|
## Updating State
|
|
@@ -346,6 +425,8 @@ Multiple bindings separated by `;`:
|
|
|
346
425
|
<div data-wcs="textContent: count; class.over: count|gt(10)"></div>
|
|
347
426
|
```
|
|
348
427
|
|
|
428
|
+
The separators `;` and `|` split only outside quotes (3.0), so a quoted filter argument may contain them: `textContent: tags|join('; ')`, `title: parts|join(' | ')`. Before 3.0 both broke the binding.
|
|
429
|
+
|
|
349
430
|
| Part | Description | Example |
|
|
350
431
|
|---|---|---|
|
|
351
432
|
| `property` | DOM property to bind | `value`, `textContent`, `checked` |
|
|
@@ -368,8 +449,15 @@ Multiple bindings separated by `;`:
|
|
|
368
449
|
| `radio` | Radio button group binding (two-way) |
|
|
369
450
|
| `checkbox` | Checkbox group binding to array (two-way) |
|
|
370
451
|
| `onclick`, `on*` | Event handler binding |
|
|
452
|
+
| `.NAME` | Explicit property (3.1): never an event, even when the name starts with `on` |
|
|
371
453
|
|
|
372
|
-
|
|
454
|
+
**Properties whose names start with `on` (3.1).** A binding whose name starts with `on` is an event binding (`onclick:`). So `online: x` listens for a `"line"` event and never writes the element's `online` property. To bind such a property, put a dot in front of the name:
|
|
455
|
+
|
|
456
|
+
```html
|
|
457
|
+
<my-status data-wcs=".online: isOnline; onclick: refresh"></my-status>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
The dotted form is the same binding as the undotted one, except that it is never an event. `.value:` is two-way like `value:`, and modifiers and input filters work as usual. A namespace word (`.class`, `.attr`, `.style`, `.command`, `.eventToken`) or an empty name after the dot is rejected with `[wcs/binding-syntax]`.
|
|
373
461
|
|
|
374
462
|
### Modifiers
|
|
375
463
|
|
|
@@ -384,6 +472,10 @@ Multiple bindings separated by `;`:
|
|
|
384
472
|
|
|
385
473
|
Multiple modifiers are comma-separated after a single `#`: `value#ro,init=none: path`.
|
|
386
474
|
|
|
475
|
+
A modifier never changes the kind of binding: `radio#ro:` and `checkbox#ro:` stay radio / checkbox bindings (3.0). Before 3.0 a modifier turned them into a plain property named `radio`, so `#ro` on a radio group did not take effect.
|
|
476
|
+
|
|
477
|
+
As of 3.0 the parser rejects what it used to round off silently, with `[wcs/binding-syntax]`: a second `#` (`value#ro#wo` kept `ro` and dropped the rest — write `value#ro,wo`), a value after `else:` (write `else:`), modifiers or left-side filters on `for` / `if` / `elseif` / `else` / `...`, and an unterminated quote in filter arguments.
|
|
478
|
+
|
|
387
479
|
### Two-Way Binding
|
|
388
480
|
|
|
389
481
|
Automatically enabled for:
|
|
@@ -561,7 +653,7 @@ Runtime reads `customClass.wcBindable.properties + inputs` and expands each name
|
|
|
561
653
|
<wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
|
|
562
654
|
```
|
|
563
655
|
|
|
564
|
-
**`undefined` is "no opinion"** — when an expanded state path resolves to `undefined` (e.g. the slot object doesn't initialize that input), the property write is **skipped** and the element keeps its own default. You only need to initialize the paths you actually use; `usersFetch: { value: null, loading: false }` is enough even though `<wcs-fetch>` also declares `method` / `manual` / `body`. To explicitly clear a value, assign `null` — `null` is always written. (This skip applies to every property binding, not just spread; with `config.debug` each skipped write is logged via `console.debug`.)
|
|
656
|
+
**`undefined` is "no opinion"** — when an expanded state path resolves to `undefined` (e.g. the slot object doesn't initialize that input), the property write is **skipped** and the element keeps its own default. You only need to initialize the paths you actually use; `usersFetch: { value: null, loading: false }` is enough even though `<wcs-fetch>` also declares `method` / `manual` / `body`. To explicitly clear a value, assign `null` — `null` is always written. (This skip applies to every property binding that feeds an element input, not just spread; with `config.debug` each skipped write is logged via `console.debug`.) **Display surfaces are different (3.0):** `textContent` / `innerText` / `innerHTML`, mustache text, `attr.*` and `style.*` have no element default worth keeping, so `undefined` and `null` both mean "no value" there — the text becomes empty and the attribute or style is removed. Before 3.0 a `textContent:` binding skipped `undefined` too, which left the previous row's text in a reused list row, and an attribute got the string `"undefined"` / `"null"`.
|
|
565
657
|
|
|
566
658
|
**Constraints**:
|
|
567
659
|
|
|
@@ -1021,13 +1113,13 @@ export default {
|
|
|
1021
1113
|
};
|
|
1022
1114
|
```
|
|
1023
1115
|
|
|
1024
|
-
Rules:
|
|
1116
|
+
Rules: `$eq` and `$eqPath` subscribe when evaluated inside any getter — a row getter, or a top-level one such as `get isAdmin() { return this.$eq("role", "admin"); }` — and called from a method or a handler they just return the comparison. `$eqIndex` needs a list row: outside one it throws `$eqIndex("…") needs a list row scope.`, and a `level` with no list index at that depth throws too. A row's subscription is dropped when the list diff removes the row; keys compare with `Object.is` except that Map semantics treat `+0`/`-0` and `NaN`/`NaN` as equal. `$eqPath` reads the key without a dependency, so a row whose key changes in place is not re-evaluated by that change — use it for identities that do not change (ids), and `$eq` with a tracked read when the key itself is live.
|
|
1025
1117
|
|
|
1026
1118
|
What reaches the rows:
|
|
1027
1119
|
|
|
1028
1120
|
- **A write to `path`** — of any value, objects included (`this.selected = row` with `$eq("selected", this["items.*"])`): the row that was selected and the row that becomes selected.
|
|
1029
1121
|
- **A write to an object above `path`** — `this.sel = { id: 2 }` for `$eq("sel.id", …)`: the same two rows, keyed by the value `path` had under the old object and has under the new one.
|
|
1030
|
-
- **A `path` that is a getter, or sits under one** (`$eq("current.id", …)` with `get current()`): its value changes without a write to it, so the calls fall back to an ordinary tracked read. The selection stays correct, but a change re-evaluates every row, as without the keyed form. Point `path` at the written state (`selectedId`) to keep the two-row cost.
|
|
1122
|
+
- **A `path` that is a getter, or sits under one** (`$eq("current.id", …)` with `get current()`): its value changes without a write to it, so the calls fall back to an ordinary tracked read. The selection stays correct, but a change re-evaluates every row, as without the keyed form. Point `path` at the written state (`selectedId`) to keep the two-row cost. The State pane of `@wcstack/devtools` counts the subscriptions per path under **Keyed selection** and marks a path that fell back this way with a `tracked` badge (3.0).
|
|
1031
1123
|
- **No type conversion:** `"2"` does not match the id `2`. An `<input>` or `<select>` writes strings, so convert on the way in (`value|number: selectedId`) or keep the ids as strings.
|
|
1032
1124
|
|
|
1033
1125
|
### Loop Index Variables (`$1`, `$2`, ...)
|
|
@@ -1160,7 +1252,7 @@ One thing `$setAll` is not: a shortcut for the dependency walk. Rendering still
|
|
|
1160
1252
|
|
|
1161
1253
|
#### `$resolve` — Access by Explicit Index
|
|
1162
1254
|
|
|
1163
|
-
`$resolve` reads or writes a value at a specific wildcard index:
|
|
1255
|
+
`$resolve` reads or writes a value at a specific wildcard index. **The argument count decides which** (3.0): `$resolve(path, indexes)` reads, and `$resolve(path, indexes, value)` writes `value` — `undefined` included (before 3.0 an `undefined` third argument was a read). On a readonly proxy the write throws `This state is readonly.`, exactly as a direct assignment does, and so does `$setAll` (before 3.0 both helpers wrote through a readonly proxy):
|
|
1164
1256
|
|
|
1165
1257
|
```javascript
|
|
1166
1258
|
export default {
|
|
@@ -1423,8 +1515,6 @@ export default {
|
|
|
1423
1515
|
| `gt(n)` | Greater than | `count\|gt(0)` |
|
|
1424
1516
|
| `ge(n)` | Greater than or equal | `count\|ge(0)` |
|
|
1425
1517
|
|
|
1426
|
-
`eq` / `ne` compare a number value numerically and anything else with the argument **as text**, so a boolean never equals `eq(true)` in 2.x: `done|eq(true)` is always `false`. Bind the boolean itself (`class.done: .done`) or use `not`. 3.0 reads an unquoted `true` / `false` / `null` as the typed value ([Preparing for 3.0](#preparing-for-30-wcsv3-migration)).
|
|
1427
|
-
|
|
1428
1518
|
### Arithmetic
|
|
1429
1519
|
|
|
1430
1520
|
| Filter | Description | Example |
|
|
@@ -1490,8 +1580,8 @@ export default {
|
|
|
1490
1580
|
|
|
1491
1581
|
| Filter | Description | Example |
|
|
1492
1582
|
|---|---|---|
|
|
1493
|
-
| `truthy` | Truthy check | `value\|truthy` |
|
|
1494
|
-
| `falsy` | Falsy check | `value\|falsy` |
|
|
1583
|
+
| `truthy` | Truthy check — JavaScript's own truthiness, the same as `boolean` (so `0n` is falsy since 3.0) | `value\|truthy` |
|
|
1584
|
+
| `falsy` | Falsy check (JavaScript's truthiness; `defaults` uses the same test) | `value\|falsy` |
|
|
1495
1585
|
| `defaults(v)` | Fallback value | `name\|defaults(Anonymous)` |
|
|
1496
1586
|
|
|
1497
1587
|
### Filter Chaining
|
|
@@ -1502,6 +1592,10 @@ Filters can be chained with `|`:
|
|
|
1502
1592
|
<div data-wcs="textContent: price|mul(1.1)|round(2)|locale(ja-JP)"></div>
|
|
1503
1593
|
```
|
|
1504
1594
|
|
|
1595
|
+
A filter is resolved when the bindings are planned. An unknown name throws `[wcs/filter-unknown]` (with a did-you-mean), and — as of 3.0 — an argument count outside what the filter accepts throws `[wcs/filter-arity]` (`join(a,b)`: "accepts at most 1 argument(s) (2 given)"), the same code and bounds lint reports. Arguments are cached by their structure, so `join('a,b')` and `join(a)` are never confused.
|
|
1596
|
+
|
|
1597
|
+
**Argument literals are typed (3.0).** An unquoted `true`, `false`, `null` or number is that value; a quoted argument is a string. The comparison filters use it for booleans and `null`: `done|eq(true)` matches `true` (before 3.0 it compared with the string `"true"` and never matched), `eq('true')` does not, and `eq(null)` matches `null`. Numbers and strings compare as before — a numeric value against the number, a string value against the text — so a form value `"1"` still matches `eq(1)`. `defaults(v)` returns the typed value: `defaults(0)` gives `0`, `defaults('0')` gives `"0"`, `defaults(null)` gives `null`.
|
|
1598
|
+
|
|
1505
1599
|
## Web Component Binding
|
|
1506
1600
|
|
|
1507
1601
|
`@wcstack/state` supports bidirectional state binding with custom elements using Shadow DOM or Light DOM.
|
|
@@ -1618,10 +1712,17 @@ customElements.define("user-card", UserCard);
|
|
|
1618
1712
|
- Mounting an array as the root (`state: rows` with `for` over it inside) is not supported; mount the row (`state: .`) or the object that holds the array (`state: group` with `for: children` inside). Both forms are contract-tested; mounts are the only way to extend the tree.
|
|
1619
1713
|
|
|
1620
1714
|
> The per-property form (`state.message: user.name`) keeps working — it is a partial mount on
|
|
1621
|
-
> the same machinery.
|
|
1622
|
-
> for a mapped key (`state = { message: "" }` together with
|
|
1623
|
-
>
|
|
1624
|
-
>
|
|
1715
|
+
> the same machinery. **An explicit partial mount wins over the component's own key (3.0):** a
|
|
1716
|
+
> component that declares a default for a mapped key (`state = { message: "" }` together with
|
|
1717
|
+
> `state.message: ...`) reads the host value; the default is simply not used. (In 2.x R1 made
|
|
1718
|
+
> that own key private and it hid the host value, with a `wcs/mount-own-key-shadow` warning.)
|
|
1719
|
+
> R1 still keeps every *unmapped* own key private.
|
|
1720
|
+
>
|
|
1721
|
+
> **`#ro` on a mount is honoured (3.0):** `state#ro: user` or `state.title#ro: doc.title`
|
|
1722
|
+
> lets the component read the entry but not write it — `element.state.title = …`, `this.title = …`
|
|
1723
|
+
> in a method and `$setAll` / `$resolve` writes throw `[wcs/mount-readonly]`, and a two-way
|
|
1724
|
+
> binding inside the component (`value: title`) does not write back. The host can still write the
|
|
1725
|
+
> path. (2.x accepted the modifier and ignored it.) The mounted `<wcs-state>` needs no `name`
|
|
1625
1726
|
> in Light DOM, and `$getAll` / `$setAll` / `$resolve` / `$postUpdate` on `element.state`
|
|
1626
1727
|
> (and on `this` inside getters/methods) speak the component's own vocabulary — paths are
|
|
1627
1728
|
> translated onto the mount and the host row's indexes are prepended automatically.
|
|
@@ -2683,38 +2784,6 @@ Every failure mode reports and continues; nothing already applied is reverted:
|
|
|
2683
2784
|
| `$watch` write chain | 32 | Skip watch firing for that batch |
|
|
2684
2785
|
| Binding apply failure | — | Skip that one binding |
|
|
2685
2786
|
|
|
2686
|
-
### Preparing for 3.0 (`wcs/v3-migration`)
|
|
2687
|
-
|
|
2688
|
-
3.0 has no compatibility layer. Instead, this release names each form that 3.0 rejects or reads differently, while still running it the 2.x way. Each warning is printed once per form and site, under the code `wcs/v3-migration`, and says what 3.0 does and what to write now:
|
|
2689
|
-
|
|
2690
|
-
```
|
|
2691
|
-
[@wcstack/state] [wcs/v3-migration] "value#ro#wo": 3.0 rejects a second "#". Write "value#ro,wo".
|
|
2692
|
-
See "Preparing for 3.0" in the @wcstack/state README.
|
|
2693
|
-
```
|
|
2694
|
-
|
|
2695
|
-
| Form | 2.x | 3.0 | Write now |
|
|
2696
|
-
|---|---|---|---|
|
|
2697
|
-
| A second `#` (`value#ro#wo:`) | Keeps the first modifier list | `[wcs/binding-syntax]` | `value#ro,wo:` |
|
|
2698
|
-
| A value after `else:` | Ignored | `[wcs/binding-syntax]` | `else:` |
|
|
2699
|
-
| Modifiers or filters on `for` / `if` / `elseif` / `else` / `...` | A plain property binding | `[wcs/binding-syntax]` | The bare keyword |
|
|
2700
|
-
| `radio#ro:` / `checkbox#ro:` | A property named `radio`, no effect | A radio / checkbox binding that honours the modifiers | — (check the binding still does what you meant) |
|
|
2701
|
-
| An unterminated quote in filter arguments | Closed silently | `[wcs/binding-syntax]` | Close the quote |
|
|
2702
|
-
| More filter arguments than the filter takes | The rest ignored | `[wcs/filter-arity]` | Remove them |
|
|
2703
|
-
| Unquoted `true` / `false` / `null` in `eq` / `ne` (`eq(true)`) | Compares a boolean or `null` value with the string | Compares with the typed value | `eq('true')` to keep the text comparison |
|
|
2704
|
-
| Unquoted `true` / `false` / `null` in `defaults` | Falls back to the string (`"null"`) | Falls back to the typed value | `defaults('null')` to keep the text |
|
|
2705
|
-
| `0n` through `truthy` / `falsy` / `defaults` | Truthy | Falsy (JavaScript truthiness) | — |
|
|
2706
|
-
| `undefined` into `textContent` / `innerText` / `innerHTML` | Keeps the previous text (in a reused row, the previous row's) | Empties it | Return `""` or `null` for "no value" |
|
|
2707
|
-
| `undefined` / `null` into `attr.*` | Writes `"undefined"` / `"null"` | Removes the attribute | — |
|
|
2708
|
-
| `undefined` into `style.*` | Keeps the previous value | Clears it | — |
|
|
2709
|
-
| `$resolve(path, indexes, undefined)` | Reads | Writes `undefined` (the argument count decides) | `$resolve(path, indexes)` to read |
|
|
2710
|
-
| `$resolve(path, indexes, value)` / `$setAll` on a readonly proxy | Writes | Throws `This state is readonly.` | Write from `createState("writable", …)` |
|
|
2711
|
-
| A component writing through a `#ro` mount (`state#ro: user`) | Writes the host's tree | `[wcs/mount-readonly]` | Write on the host, or drop `#ro` |
|
|
2712
|
-
| An own default shadowed by a partial mount (`state.name: user.name` while the component declares `name`) | The default wins, with a warning | The explicit mount wins | Remove the default |
|
|
2713
|
-
|
|
2714
|
-
One more change arrives early instead of being announced: `$errorCallback` on a volume or a mounted component, which was ignored silently, is now named by the warning that already lists the root-only keys (`$commandTokens`, `$on`, …), as 3.0 does. It still runs only on the root state.
|
|
2715
|
-
|
|
2716
|
-
A value warning fires only when that value actually arrives, so a clean console proves nothing about the paths your tests did not reach. The syntax rows are checked when a binding is first parsed, and `npx @wcstack/lint <file>` (and the VS Code extension) reports them with the same check, as `wcs/v3-migration` at info severity, without running the page.
|
|
2717
|
-
|
|
2718
2787
|
## Configuration
|
|
2719
2788
|
|
|
2720
2789
|
Pass a partial configuration object to `bootstrapState()`:
|