@wcstack/state 2.5.1 → 2.6.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
@@ -369,6 +369,8 @@ property[#modifier]: path[|filter[|filter(args)...]]
369
369
  | `checkbox` | チェックボックスグループの配列バインディング(双方向) |
370
370
  | `onclick`, `on*` | イベントハンドラバインディング |
371
371
 
372
+ `undefined` は書き込みをスキップします。要素は表示していたものを保ち、再利用されたリスト行では**前の行の**テキストが残ります。「値なし」は `""` か `null` を返してください(3.0 は表示のプロパティへの `undefined` を空にします — [3.0 への準備](#30-への準備wcsv3-migration))。
373
+
372
374
  ### 修飾子
373
375
 
374
376
  | 修飾子 | 説明 |
@@ -969,6 +971,7 @@ get total() { return this.price * exchangeRate; } // モジュール変
969
971
  | `this.$trackDependency(path)` | 依存を明示的に追加し、そのパスの変更でこの getter を dirty にする |
970
972
  | `this.$postUpdate(path)` | 追跡外の入力が変わったことを getter の外から通知する |
971
973
  | `this.$untrackDependency(fn)` | 依存として登録せずにパスを読む(上の対称) |
974
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path)` | 鍵付き購読: 「`path` はこの行の鍵に等しいか」をパターン依存なしで答える([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
972
975
 
973
976
  ```javascript
974
977
  // ✅ 時計を state 側で刻み、getter は純粋なまま
@@ -995,6 +998,38 @@ getter の例外は握り潰されません。評価された場所(バイン
995
998
 
996
999
  `$untrackDependency(fn)` は setter の規則を getter に意図的に適用するもので、`fn` の中の読み取りは追跡されません。`$trackDependency(path)` は最初の規則に対する逃げ道です。
997
1000
 
1001
+ ### 鍵付き選択(`$eq` / `$eqPath` / `$eqIndex`)
1002
+
1003
+ 「この行は選択中か」を答える行 getter は、依存追跡が最も効きにくい形です。`get "items.*.selected"() { return this.$1 === this.selectedIndex; }` と書くと全行が `selectedIndex` に依存し、1 クリックで 1 万行の getter が再評価されます。鍵付きの形は各行を **その行自身の鍵** の下に購読させ、パスへの書き込みでは「選択されていた行」と「選択される行」だけを再評価します:
1004
+
1005
+ | API | 鍵 | 選択の付き先 | 備考 |
1006
+ |---|---|---|---|
1007
+ | `this.$eq(path, key)` | 任意の値 | 鍵 | `path` は依存を張らずに読み、行は `key` の下に登録される |
1008
+ | `this.$eqPath(path, keyPath)` | `keyPath` の値(ワイルドカードはこの行で解決) | id | 鍵も依存を張らずに読むので、リストの置換や並べ替えで行が再評価されない |
1009
+ | `this.$eqIndex(path, level = 1)` | この行の index(`$1`。`level` でワイルドカード段を選ぶ) | index | `$1` を読む場合と違い getter を **index 依存に記録しない**: 行の移動はリスト差分が鍵を付け替えるので、1 行削除で再評価されるのは高々 2 行 |
1010
+
1011
+ ```javascript
1012
+ export default {
1013
+ items: [],
1014
+ selectedId: null,
1015
+ selectedIndex: null,
1016
+ // id による選択: 並べ替えや削除をまたいで同じ行に付いて行く
1017
+ get "items.*.selected"() { return this.$eqPath("selectedId", "items.*.id"); },
1018
+ // 位置による選択: 同じ仕事量で index に付く
1019
+ get "items.*.current"() { return this.$eqIndex("selectedIndex"); },
1020
+ onSelect(e, $1) { this.selectedId = this["items." + $1 + ".id"]; this.selectedIndex = $1; },
1021
+ };
1022
+ ```
1023
+
1024
+ 規則: 3 つの呼び出しが購読を張るのはリスト行の getter の中で評価されたときだけで、それ以外では比較結果を返すだけです。行の購読はリスト差分がその行を外した時点で落ちます。鍵の比較は `Object.is` ですが、Map の意味論により `+0` と `-0`、`NaN` 同士は同じ鍵になります。`$eqPath` は鍵を依存なしで読むので、行の鍵がその場で書き換わっても行は再評価されません — 変わらない同一性(id)に使い、鍵自体が動く場合は追跡付きの読みと `$eq` を組み合わせてください。
1025
+
1026
+ 行に届くもの:
1027
+
1028
+ - **`path` への書き込み** — オブジェクトを含むどんな値でも(`$eq("selected", this["items.*"])` に対する `this.selected = row`): 選ばれていた行と、新たに選ばれる行。
1029
+ - **`path` より上のオブジェクトへの書き込み** — `$eq("sel.id", …)` に対する `this.sel = { id: 2 }`: 同じく 2 行。旧いオブジェクトの下と新しいオブジェクトの下で `path` が持つ値を鍵にします。
1030
+ - **`path` が getter か getter の配下**(`get current()` に対する `$eq("current.id", …)`): 値が書き込みなしに変わるので、依存を張る普通の読み取りに戻ります。選択は正しく保たれますが、変化のたびに全行が再評価されます(鍵付きの形を使わないのと同じ)。2 行で済ませるには、`path` を書き込まれる state(`selectedId`)に向けてください。
1031
+ - **型変換はしません:** `"2"` は id `2` に一致しません。`<input>` や `<select>` は文字列を書くので、入口で変換する(`value|number: selectedId`)か、id を文字列で持ってください。
1032
+
998
1033
  ### ループインデックス変数(`$1`, `$2`, ...)
999
1034
 
1000
1035
  getter やイベントハンドラ内で、`this.$1`、`this.$2` などで現在のループイテレーションのインデックスを取得できます(0始まりの値、1始まりの命名):
@@ -1041,6 +1076,7 @@ export default {
1041
1076
  | `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行 |
1042
1077
  | `this.$trackDependency(path)` | キャッシュ無効化のための依存関係を手動で登録 |
1043
1078
  | `this.$untrackDependency(fn)` | fn 実行中の依存追跡を抑止して値を読む(`$trackDependency` と対称) |
1079
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | 鍵付き購読: 「`path` の値はこの行の鍵か」([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
1044
1080
  | `this.$stateElement` | `IStateElement` インスタンスへのアクセス |
1045
1081
  | `this.$1`, `this.$2`, ... | 現在のループインデックス(1始まりの命名、0始まりの値) |
1046
1082
 
@@ -1386,6 +1422,8 @@ export default {
1386
1422
  | `gt(n)` | より大きい | `count\|gt(0)` |
1387
1423
  | `ge(n)` | 以上 | `count\|ge(0)` |
1388
1424
 
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
+
1389
1427
  ### 算術
1390
1428
 
1391
1429
  | フィルタ | 説明 | 例 |
@@ -2641,6 +2679,38 @@ this.$getAll("matrix.*.*", [row]);
2641
2679
  | `$watch` の書き込み連鎖 | 32 | そのバッチの watch 発火をスキップ |
2642
2680
  | バインディングの適用失敗 | — | その 1 本のみスキップ |
2643
2681
 
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
+
2644
2714
  ## 設定
2645
2715
 
2646
2716
  `bootstrapState()` に部分的な設定オブジェクトを渡します:
package/README.md CHANGED
@@ -369,6 +369,8 @@ Multiple bindings separated by `;`:
369
369
  | `checkbox` | Checkbox group binding to array (two-way) |
370
370
  | `onclick`, `on*` | Event handler binding |
371
371
 
372
+ `undefined` skips the write: the element keeps what it showed, and in a reused list row that is the **previous row's** text. Return `""` or `null` for "no value" (3.0 empties display properties on `undefined` — [Preparing for 3.0](#preparing-for-30-wcsv3-migration)).
373
+
372
374
  ### Modifiers
373
375
 
374
376
  | Modifier | Description |
@@ -969,6 +971,7 @@ The rule: **read only through `this`, and don't write state or touch the DOM fro
969
971
  | `this.$trackDependency(path)` | Register an extra dependency so this getter is dirtied when that path changes |
970
972
  | `this.$postUpdate(path)` | Announce that an untracked input changed, from outside the getter |
971
973
  | `this.$untrackDependency(fn)` | Read a path *without* registering it as a dependency (the inverse) |
974
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path)` | A keyed subscription: "is `path` equal to this row's key?" without a pattern dependency (see [Keyed selection](#keyed-selection-eq--eqpath--eqindex)) |
972
975
 
973
976
  ```javascript
974
977
  // ✅ The clock ticks in state; the getter stays pure
@@ -995,6 +998,38 @@ The first rule is the one static analysis can catch: `wcs-validate` and the VS C
995
998
 
996
999
  `$untrackDependency(fn)` applies the setter rule to a getter on purpose: reads inside `fn` are not tracked. `$trackDependency(path)` is the escape hatch for the first rule.
997
1000
 
1001
+ ### Keyed selection (`$eq` / `$eqPath` / `$eqIndex`)
1002
+
1003
+ A row getter that answers "is this row the selected one?" is the classic case where dependency tracking scales badly: `get "items.*.selected"() { return this.$1 === this.selectedIndex; }` makes every row depend on `selectedIndex`, so one click re-evaluates 10,000 getters. The keyed forms subscribe each row under **its own key** instead, and a write to the path re-evaluates only the row that was selected and the row that becomes selected:
1004
+
1005
+ | API | Key | Selection follows | Notes |
1006
+ |---|---|---|---|
1007
+ | `this.$eq(path, key)` | any value | the key | `path` is read without a dependency; the row registers under `key` |
1008
+ | `this.$eqPath(path, keyPath)` | the value at `keyPath` (wildcards resolve to this row) | the id | reads the key without a dependency too, so replacing or reordering the list never re-evaluates the rows |
1009
+ | `this.$eqIndex(path, level = 1)` | this row's index (`$1`; `level` picks the wildcard) | the index | unlike reading `$1`, the getter is **not** recorded as index-dependent: the list diff re-keys moved rows, so removing a row re-evaluates at most two rows |
1010
+
1011
+ ```javascript
1012
+ export default {
1013
+ items: [],
1014
+ selectedId: null,
1015
+ selectedIndex: null,
1016
+ // selection by id: survives sorting and removal
1017
+ get "items.*.selected"() { return this.$eqPath("selectedId", "items.*.id"); },
1018
+ // selection by position: the same row count of work, keyed on the index
1019
+ get "items.*.current"() { return this.$eqIndex("selectedIndex"); },
1020
+ onSelect(e, $1) { this.selectedId = this["items." + $1 + ".id"]; this.selectedIndex = $1; },
1021
+ };
1022
+ ```
1023
+
1024
+ Rules: the three calls only subscribe when evaluated inside a getter under a list row (elsewhere they just return the comparison); 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
+
1026
+ What reaches the rows:
1027
+
1028
+ - **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
+ - **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.
1031
+ - **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
+
998
1033
  ### Loop Index Variables (`$1`, `$2`, ...)
999
1034
 
1000
1035
  Inside getters and event handlers, `this.$1`, `this.$2`, etc. provide the current loop iteration index (0-based value, 1-based naming):
@@ -1041,6 +1076,7 @@ Inside state objects (getters / methods), the following APIs are available via `
1041
1076
  | `this.$postUpdate(path)` | Manually trigger update notification for a path |
1042
1077
  | `this.$trackDependency(path)` | Manually register a dependency for cache invalidation |
1043
1078
  | `this.$untrackDependency(fn)` | Read values inside fn without registering dependencies (symmetric to `$trackDependency`) |
1079
+ | `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | Keyed subscription for "is this row's key the value of `path`?" (see [Keyed selection](#keyed-selection-eq--eqpath--eqindex)) |
1044
1080
  | `this.$command.<name>` | Access a `CommandToken` declared in `$commandTokens` (see [Command Token](#command-token-method-binding)) |
1045
1081
  | `this.$stateElement` | Access to the `IStateElement` instance |
1046
1082
  | `this.$1`, `this.$2`, ... | Current loop index (1-based naming, 0-based value) |
@@ -1387,6 +1423,8 @@ export default {
1387
1423
  | `gt(n)` | Greater than | `count\|gt(0)` |
1388
1424
  | `ge(n)` | Greater than or equal | `count\|ge(0)` |
1389
1425
 
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
+
1390
1428
  ### Arithmetic
1391
1429
 
1392
1430
  | Filter | Description | Example |
@@ -2645,6 +2683,38 @@ Every failure mode reports and continues; nothing already applied is reverted:
2645
2683
  | `$watch` write chain | 32 | Skip watch firing for that batch |
2646
2684
  | Binding apply failure | — | Skip that one binding |
2647
2685
 
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
+
2648
2718
  ## Configuration
2649
2719
 
2650
2720
  Pass a partial configuration object to `bootstrapState()`: