@wcstack/state 1.31.0 → 1.32.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 +124 -7
- package/README.md +126 -7
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.esm.js +970 -108
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +28 -8
- package/dist/parser.esm.js +28 -8
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -769,19 +769,20 @@ export default {
|
|
|
769
769
|
+ this["regions.*.prefectures.*.cities.*.name"];
|
|
770
770
|
},
|
|
771
771
|
|
|
772
|
-
// 県レベル —
|
|
772
|
+
// 県レベル — 市からの集約。`indexes` 省略時はループ文脈([$1, $2])が
|
|
773
|
+
// 既定になるので、この県の市だけが合計される
|
|
773
774
|
get "regions.*.prefectures.*.totalPopulation"() {
|
|
774
|
-
return this.$getAll("regions.*.prefectures.*.cities.*.population"
|
|
775
|
+
return this.$getAll("regions.*.prefectures.*.cities.*.population")
|
|
775
776
|
.reduce((a, b) => a + b, 0);
|
|
776
777
|
},
|
|
777
778
|
|
|
778
|
-
// 地方レベル —
|
|
779
|
+
// 地方レベル — 県からの集約(文脈 [$1] でこの地方に絞られる)
|
|
779
780
|
get "regions.*.totalPopulation"() {
|
|
780
|
-
return this.$getAll("regions.*.prefectures.*.totalPopulation"
|
|
781
|
+
return this.$getAll("regions.*.prefectures.*.totalPopulation")
|
|
781
782
|
.reduce((a, b) => a + b, 0);
|
|
782
783
|
},
|
|
783
784
|
|
|
784
|
-
// トップレベル —
|
|
785
|
+
// トップレベル — ループ文脈なし。[] は「マッチ全件」
|
|
785
786
|
get totalPopulation() {
|
|
786
787
|
return this.$getAll("regions.*.totalPopulation", [])
|
|
787
788
|
.reduce((a, b) => a + b, 0);
|
|
@@ -943,6 +944,7 @@ export default {
|
|
|
943
944
|
| API | 説明 |
|
|
944
945
|
|---|---|
|
|
945
946
|
| `this.$getAll(path, indexes?)` | ワイルドカードパスにマッチする全ての値を取得 |
|
|
947
|
+
| `this.$setAll(path, indexes, value, options?)` | ワイルドカードパスにマッチする全アドレスへ一括書き込み |
|
|
946
948
|
| `this.$resolve(path, indexes, value?)` | ワイルドカードパスを特定のインデックスで解決 |
|
|
947
949
|
| `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行 |
|
|
948
950
|
| `this.$trackDependency(path)` | キャッシュ無効化のための依存関係を手動で登録 |
|
|
@@ -967,6 +969,66 @@ export default {
|
|
|
967
969
|
};
|
|
968
970
|
```
|
|
969
971
|
|
|
972
|
+
`indexes` はパスの `*` に対する**前方一致の接頭辞**です。不足した階層は全展開され、`[]` は常に「マッチ全件」を意味します。**省略**した場合はループ文脈の添字(`[$1, $2, ...]`)が既定になり、パスが文脈と共有するワイルドカード階層に敷かれます:
|
|
973
|
+
|
|
974
|
+
```javascript
|
|
975
|
+
export default {
|
|
976
|
+
regions: [ /* { prefectures: [ { population: … }, … ] } */ ],
|
|
977
|
+
// ループ文脈 [$1] — 省略すると現在の地方に絞られる
|
|
978
|
+
get "regions.*.total"() {
|
|
979
|
+
return this.$getAll("regions.*.prefectures.*.population").reduce((a, b) => a + b, 0);
|
|
980
|
+
},
|
|
981
|
+
// ループ文脈なし — 省略は全展開([] と同じ)
|
|
982
|
+
get grandTotal() {
|
|
983
|
+
return this.$getAll("regions.*.total").reduce((a, b) => a + b, 0);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
文脈がパスより深い分は切り詰められます(`[$1, $2]` の文脈は `*` 1 本のパスを `[$1]` で絞ります)。一方、文脈がループ添字を持っているのにパスと**ワイルドカード階層をまったく共有しない**場合 —— たとえば `regions.*` の getter 内での `$getAll("users.*.name")` —— は、黙って全 users を読む代わりに **throw** します。文脈の添字は別のリストのものであり、流用しても無視しても書き手の意図とは食い違うためです。この形では添字を明示してください(全件なら `[]`)。
|
|
989
|
+
|
|
990
|
+
#### `$setAll` — 配列要素を作り直さずに一括更新
|
|
991
|
+
|
|
992
|
+
`$setAll` は `$getAll` の書き側の対称形で、ワイルドカードパスにマッチする全アドレスへ書き込みます。狙いは記述の短さではなく、**配列そのものを保つ**ことです。`this.users = this.users.map(...)` のように作り直すと ListIndex・行 getter のキャッシュ・差分描画がまとめて捨てられますが、`$setAll` は行ごとの in-place な書き込みに分解するのでリストの同一性が保たれます。
|
|
993
|
+
|
|
994
|
+
```javascript
|
|
995
|
+
export default {
|
|
996
|
+
users: [{ selected: false }, { selected: false }],
|
|
997
|
+
|
|
998
|
+
toggleAll(e) {
|
|
999
|
+
this.$setAll("users.*.selected", [], e.target.checked); // ブロードキャスト
|
|
1000
|
+
},
|
|
1001
|
+
invertAll() {
|
|
1002
|
+
this.$setAll("users.*.selected", [], cur => !cur); // mapper
|
|
1003
|
+
},
|
|
1004
|
+
rankTopThree() {
|
|
1005
|
+
// undefined を返したアドレスはスキップされる(=この行は変えない)
|
|
1006
|
+
this.$setAll("users.*.score", [], (cur, i) => i < 3 ? cur * 2 : undefined);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
形は 3 つあり、3 番目だけは明示的に要求する必要があります。
|
|
1012
|
+
|
|
1013
|
+
| 第 3 引数 | 意味 |
|
|
1014
|
+
|---|---|
|
|
1015
|
+
| 関数 | **mapper** — マッチしたアドレスごとに `(current, ...indexes)` で呼ばれる |
|
|
1016
|
+
| それ以外 | **ブロードキャスト** — 配列も含め、同じ値が全アドレスに書かれる |
|
|
1017
|
+
| 配列 + `{ spread: true }` | **spread** — マッチ順に 1 件ずつ配る |
|
|
1018
|
+
|
|
1019
|
+
配列が既定でブロードキャストされるのは、対象プロパティ自体が配列型でありうるためです。`$setAll("users.*.tags", [], ["admin"])` は「全員に `["admin"]`」なのか「1 人目に `"admin"`」なのか判別できません。`{ spread: true }` を明示すればこの推測が消え、長さがマッチ件数と噛み合わなければ黙って誤配せずに throw します。
|
|
1020
|
+
|
|
1021
|
+
`indexes` の意味は `$getAll` と同じ**前方一致の接頭辞**(不足はその階層を全展開)ですが、**省略はできません**。書き込みには暗黙のループ文脈を与えないため、`for` テンプレートの中でも `this.$setAll("users.*.selected", [], true)` は現在行ではなく**全行**を意味します。
|
|
1022
|
+
|
|
1023
|
+
```javascript
|
|
1024
|
+
this.$setAll("matrix.*.*", [0], 0); // 0 行目だけ全列
|
|
1025
|
+
this.$setAll("users.*", [], rows, { spread: true }); // 配列を保ったまま各行を差し替え
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
`undefined` はどの形でも書き込まれず「このアドレスはスキップ」を意味します。mapper が `return` を忘れて全行を潰す事故を防ぐためで、クリアしたい場合は `null` を使います。戻り値は実際に書き込んだ件数です。
|
|
1029
|
+
|
|
1030
|
+
なお `$setAll` は依存解決を一括化する仕組みではありません。描画は 1 バッチに畳まれますが、書き込みは 1 件ずつ登録されるので、コストは置き換え対象の手書きループと同じです。得られるのはリストが保たれることであって、実行回数の削減ではありません。
|
|
1031
|
+
|
|
970
1032
|
#### `$resolve` — 明示的なインデックスでのアクセス
|
|
971
1033
|
|
|
972
1034
|
`$resolve` は特定のワイルドカードインデックスの値を読み書きします:
|
|
@@ -1750,6 +1812,8 @@ $updatedCallback(paths) {
|
|
|
1750
1812
|
| ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
|
|
1751
1813
|
| 同一パスの行間 | `indexes` 昇順 | 固定 |
|
|
1752
1814
|
|
|
1815
|
+
**機構間の層を動かす唯一のもの**が、`state` 参加者を受け付ける `<wcs-view-transition>` です。バインディング適用 —— したがって `$updatedCallback` —— がフレームで着地する一方、`$watch` と `$streams` restart は state アドレスを消費し DOM を見ないので、drain がキューされた microtask に留まります。タグがある間の順序は `$watch` → `$streams` restart → `$updatedCallback` です。この層を並べ替えるものはページ上でこれ 1 つだけです。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 を参照してください。
|
|
1816
|
+
|
|
1753
1817
|
主なルール:
|
|
1754
1818
|
|
|
1755
1819
|
- **自 state のみ** —— パスに `@stateName` は書けません。他の state 要素の watch は宣言時に拒否されます。
|
|
@@ -2011,6 +2075,31 @@ export default {
|
|
|
2011
2075
|
- `$updatedCallback(paths, indexesListByPath)` は、その drain で live binding が適用された path の一覧を受け取ります。binding のない state 書き込みでは呼ばれず、`paths` にも現れません。ワイルドカードをもつパスが更新された場合は、`indexesListByPath` から対象のインデックス情報も取得可能です。`async` を使用できますが、戻り値は await されません。
|
|
2012
2076
|
- Web Component を使用している場合は、コンポーネント側に `async $stateReadyCallback(stateProp)` を定義おくことで、`bind-component` でバインドした状態が利用可能になった瞬間にフックとして呼び出されます。
|
|
2013
2077
|
|
|
2078
|
+
## 遷移アニメーション
|
|
2079
|
+
|
|
2080
|
+
入場アニメーションにこのパッケージは要らない。新しい `for` 行も mount する `if` 分岐も「新しく挿入された要素」なので、素の CSS で足りる。
|
|
2081
|
+
|
|
2082
|
+
```css
|
|
2083
|
+
li {
|
|
2084
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
2085
|
+
@starting-style { opacity: 0; transform: translateY(-4px); }
|
|
2086
|
+
}
|
|
2087
|
+
```
|
|
2088
|
+
|
|
2089
|
+
そこへ届かないのが**退場**と**移動**。削除された行は同期で detach され、並べ替えには中間状態が無い。[`@wcstack/view-transition`](https://github.com/wcstack/wcstack/tree/main/packages/view-transition) を足すと drain の DOM 変更が View Transition の中で行われ、変更前の状態はブラウザがスナップショットしてくれる。
|
|
2090
|
+
|
|
2091
|
+
```html
|
|
2092
|
+
<script type="module" src="https://esm.run/@wcstack/view-transition/auto"></script>
|
|
2093
|
+
<wcs-view-transition naming="auto"></wcs-view-transition>
|
|
2094
|
+
```
|
|
2095
|
+
|
|
2096
|
+
そのタグが `state` 参加者を受け付けている間、知っておくべき帰結が 2 つある。
|
|
2097
|
+
|
|
2098
|
+
- drain は microtask ではなくフレームで着地する。state に書いてから `await Promise.resolve()` で DOM を読むコードは遷移を待つ必要がある。`$updatedCallback` はバインディング適用の直後という*位置*こそ変わらないが、その適用ごと 1 フレーム後ろへずれる。
|
|
2099
|
+
- `$watch` と `$streams` restart は元の microtask に留まるため、`$updatedCallback` の**前**に走るようになる。
|
|
2100
|
+
|
|
2101
|
+
適用すべきバインディングが実際にあるバッチだけがタグへ渡されるので、headless なパスへの書き込みが遷移を起こすことはない。タグが無ければ drain は従来どおり。[docs/timing-and-firing-contract.ja.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.ja.md) §4.3 参照。
|
|
2102
|
+
|
|
2014
2103
|
## 診断と失敗の扱い
|
|
2015
2104
|
|
|
2016
2105
|
### 存在しないパスへの配線は報告されます
|
|
@@ -2045,7 +2134,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
2045
2134
|
|
|
2046
2135
|
| 診断 | 何を見るか | 直し方 |
|
|
2047
2136
|
|---|---|---|
|
|
2048
|
-
| `wcs/index-arity` | `$resolve(path, indexes)` は `*` の本数と**厳密一致**、`$getAll(path, indexes)` は**上限**(不足は「残りの階層を全展開」という正当な接頭辞) | 本数を合わせる |
|
|
2137
|
+
| `wcs/index-arity` | `$resolve(path, indexes)` は `*` の本数と**厳密一致**、`$getAll(path, indexes)` / `$setAll(path, indexes, …)` は**上限**(不足は「残りの階層を全展開」という正当な接頭辞) | 本数を合わせる |
|
|
2049
2138
|
| `wcs/wildcard-rank` | パスの `*` の本数(と `$N` の N)が、囲む `for` の段数を超えていないか | `for` を足すか、`$resolve(path, indexes)` で行を明示する |
|
|
2050
2139
|
| `wcs/getter-cycle` | パス getter どうしが循環参照していないか | 循環を断つ |
|
|
2051
2140
|
|
|
@@ -2102,13 +2191,41 @@ bootstrapState({
|
|
|
2102
2191
|
|---|---|---|
|
|
2103
2192
|
| `bindAttributeName` | `'data-wcs'` | バインディング属性名 |
|
|
2104
2193
|
| `tagNames.state` | `'wcs-state'` | 状態要素のタグ名 |
|
|
2105
|
-
| `locale` | `'en'` |
|
|
2194
|
+
| `locale` | `<html lang>`、無ければ `'en'` | ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)のロケール — [ロケール](#ロケール)を参照 |
|
|
2106
2195
|
| `debug` | `false` | デバッグモード |
|
|
2107
2196
|
| `enableMustache` | `true` | `{{ }}` 構文の有効化 |
|
|
2108
2197
|
| `enableDirectionalInitialSync` | `true` | 方向認識のバインディング authority(`#init=` / `#sync=` バインド modifier)— [バインディング authority](#バインディング-authority-init--sync) 参照。既定 on。`false` で opt-out |
|
|
2109
2198
|
| `enablePropagationContext` | `true` | バインド間の因果伝播トラッキング(echo/diamond のループ防止)。既定 on。`false` で opt-out |
|
|
2110
2199
|
| `enableContractAnalyzer` | `false` | opt-in の開発時 contract analyzer(`analyzeContract` を公開) |
|
|
2111
2200
|
|
|
2201
|
+
### ロケール
|
|
2202
|
+
|
|
2203
|
+
ロケールで書式化するフィルタは 4 つある — `locale` / `date` / `time` / `datetime`。
|
|
2204
|
+
これらは `config.locale` を読み、その既定は **`<html lang>`** である。
|
|
2205
|
+
|
|
2206
|
+
```html
|
|
2207
|
+
<html lang="ja-JP">
|
|
2208
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
2209
|
+
```
|
|
2210
|
+
|
|
2211
|
+
他に何も要らない。`<html lang>` はページの言語を書く HTML 標準の場所であり、
|
|
2212
|
+
そこを既定にすればロケールの正本が 1 つで済む。同時に、**CDN 一発のページが
|
|
2213
|
+
ロケールを設定できるようになる** — `auto` は `bootstrapState()` を引数なしで呼ぶので、
|
|
2214
|
+
これが無いと渡す口が無かった。明示指定(`bootstrapState({ locale })`)は常に優先し、
|
|
2215
|
+
不正な BCP-47 タグは `Intl` の中で落ちる前に警告して無視する。
|
|
2216
|
+
|
|
2217
|
+
**`config.locale` を後から変えても何も再描画されない。** これは state ではなく
|
|
2218
|
+
グローバル設定なので、依存グラフに載らない。フィルタ自体はバインド構築時に
|
|
2219
|
+
取り込むのではなく**適用のたびに読む**ので、別の理由で再描画されたバインドは新しい値を
|
|
2220
|
+
拾う — 起動順序の事故から復帰するには足りるが、ページの言語を切り替えるには足りない。
|
|
2221
|
+
言語はページが描画される前に決めること。マークアップに `<html lang>` を書くか、
|
|
2222
|
+
`<head>` の同期スクリプトで書けば構造的にそうなる。
|
|
2223
|
+
|
|
2224
|
+
呼び出しごとの上書き(`price|locale(fr-FR)`)は従来どおり使え、こちらはバインド式の
|
|
2225
|
+
一部なのでバインド時に固定される。リロードなしで言語を切り替えたい場合は
|
|
2226
|
+
[docs/i18n-design.md](../../docs/i18n-design.md) を参照。短く言えば、翻訳はフィルタでは
|
|
2227
|
+
なくパスに置く。
|
|
2228
|
+
|
|
2112
2229
|
> この 3 つは **architecture-hardening** 機能で、規範は `docs/architecture-hardening/` に
|
|
2113
2230
|
> あります。`enablePropagationContext` は**既定 on** — write-path コストは一方向バインドで
|
|
2114
2231
|
> ほぼゼロ(echo しうる双方向 wire のみ因果 bookkeeping を行う)で、フラグは恒久的な
|
package/README.md
CHANGED
|
@@ -769,19 +769,20 @@ export default {
|
|
|
769
769
|
+ this["regions.*.prefectures.*.cities.*.name"];
|
|
770
770
|
},
|
|
771
771
|
|
|
772
|
-
// Prefecture level — aggregate from cities
|
|
772
|
+
// Prefecture level — aggregate from cities. `indexes` omitted: it defaults to
|
|
773
|
+
// the loop context ([$1, $2]), so only this prefecture's cities are summed
|
|
773
774
|
get "regions.*.prefectures.*.totalPopulation"() {
|
|
774
|
-
return this.$getAll("regions.*.prefectures.*.cities.*.population"
|
|
775
|
+
return this.$getAll("regions.*.prefectures.*.cities.*.population")
|
|
775
776
|
.reduce((a, b) => a + b, 0);
|
|
776
777
|
},
|
|
777
778
|
|
|
778
|
-
// Region level — aggregate from prefectures
|
|
779
|
+
// Region level — aggregate from prefectures (context [$1] narrows to this region)
|
|
779
780
|
get "regions.*.totalPopulation"() {
|
|
780
|
-
return this.$getAll("regions.*.prefectures.*.totalPopulation"
|
|
781
|
+
return this.$getAll("regions.*.prefectures.*.totalPopulation")
|
|
781
782
|
.reduce((a, b) => a + b, 0);
|
|
782
783
|
},
|
|
783
784
|
|
|
784
|
-
// Top level —
|
|
785
|
+
// Top level — no loop context; [] means "every match"
|
|
785
786
|
get totalPopulation() {
|
|
786
787
|
return this.$getAll("regions.*.totalPopulation", [])
|
|
787
788
|
.reduce((a, b) => a + b, 0);
|
|
@@ -943,6 +944,7 @@ Inside state objects (getters / methods), the following APIs are available via `
|
|
|
943
944
|
| API | Description |
|
|
944
945
|
|---|---|
|
|
945
946
|
| `this.$getAll(path, indexes?)` | Get all values matching a wildcard path |
|
|
947
|
+
| `this.$setAll(path, indexes, value, options?)` | Write to every address matching a wildcard path |
|
|
946
948
|
| `this.$resolve(path, indexes, value?)` | Resolve a wildcard path with specific indexes |
|
|
947
949
|
| `this.$postUpdate(path)` | Manually trigger update notification for a path |
|
|
948
950
|
| `this.$trackDependency(path)` | Manually register a dependency for cache invalidation |
|
|
@@ -968,6 +970,66 @@ export default {
|
|
|
968
970
|
};
|
|
969
971
|
```
|
|
970
972
|
|
|
973
|
+
`indexes` is a **prefix** over the path's wildcards: missing levels expand fully, and `[]` always means "every match". When `indexes` is **omitted**, it defaults to the enclosing loop context (`[$1, $2, ...]`), applied to the wildcard levels the path shares with that context:
|
|
974
|
+
|
|
975
|
+
```javascript
|
|
976
|
+
export default {
|
|
977
|
+
regions: [ /* { prefectures: [ { population: … }, … ] } */ ],
|
|
978
|
+
// Loop context [$1] — omission narrows to the current region
|
|
979
|
+
get "regions.*.total"() {
|
|
980
|
+
return this.$getAll("regions.*.prefectures.*.population").reduce((a, b) => a + b, 0);
|
|
981
|
+
},
|
|
982
|
+
// No loop context — omission expands everything (same as [])
|
|
983
|
+
get grandTotal() {
|
|
984
|
+
return this.$getAll("regions.*.total").reduce((a, b) => a + b, 0);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
Context levels deeper than the path needs are dropped (a `[$1, $2]` context narrows a one-wildcard path by `[$1]`). But if the path shares **no** wildcard level with a context that does hold loop indexes — say `$getAll("users.*.name")` inside a `regions.*` getter — `$getAll` **throws** instead of silently reading every user: the context indexes belong to a different list, and neither reusing nor ignoring them is what the author meant. Pass indexes explicitly there (`[]` for every match).
|
|
990
|
+
|
|
991
|
+
#### `$setAll` — Update Every Array Element In Place
|
|
992
|
+
|
|
993
|
+
`$setAll` is the write-side counterpart of `$getAll`: it writes to every address a wildcard path matches. The point is not brevity but **keeping the array itself**. Rebuilding it (`this.users = this.users.map(...)`) throws away the list indexes, the per-row getter caches, and the render diff; `$setAll` decomposes into in-place per-row writes instead, so the list identity survives.
|
|
994
|
+
|
|
995
|
+
```javascript
|
|
996
|
+
export default {
|
|
997
|
+
users: [{ selected: false }, { selected: false }],
|
|
998
|
+
|
|
999
|
+
toggleAll(e) {
|
|
1000
|
+
this.$setAll("users.*.selected", [], e.target.checked); // broadcast
|
|
1001
|
+
},
|
|
1002
|
+
invertAll() {
|
|
1003
|
+
this.$setAll("users.*.selected", [], cur => !cur); // mapper
|
|
1004
|
+
},
|
|
1005
|
+
rankTopThree() {
|
|
1006
|
+
// `undefined` skips that address — "leave this row alone"
|
|
1007
|
+
this.$setAll("users.*.score", [], (cur, i) => i < 3 ? cur * 2 : undefined);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
Three forms, and the third one has to be asked for explicitly:
|
|
1013
|
+
|
|
1014
|
+
| Third argument | Meaning |
|
|
1015
|
+
|---|---|
|
|
1016
|
+
| a function | **mapper** — called as `(current, ...indexes)` per matched address |
|
|
1017
|
+
| anything else | **broadcast** — the same value is written everywhere, arrays included |
|
|
1018
|
+
| an array **plus** `{ spread: true }` | **spread** — one entry handed to each matched address, in match order |
|
|
1019
|
+
|
|
1020
|
+
Arrays broadcast by default because the target property may itself be array-valued — `$setAll("users.*.tags", [], ["admin"])` would otherwise be ambiguous. Opting into `{ spread: true }` removes the guesswork, and a length that does not equal the match count throws rather than silently misaligning.
|
|
1021
|
+
|
|
1022
|
+
`indexes` works exactly as in `$getAll` — a **prefix**, where missing levels mean "expand all of them" — but it is **required**. Writes get no implicit loop context, so inside a `for` template `this.$setAll("users.*.selected", [], true)` still means *every* user, never the current row.
|
|
1023
|
+
|
|
1024
|
+
```javascript
|
|
1025
|
+
this.$setAll("matrix.*.*", [0], 0); // row 0 only, every column
|
|
1026
|
+
this.$setAll("users.*", [], rows, { spread: true }); // replace each row, keep the array
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
`undefined` is never written — it means "skip this address" in all three forms, which keeps a mapper that forgets to `return` from wiping every row. Use `null` to clear. The return value is the number of addresses actually written.
|
|
1030
|
+
|
|
1031
|
+
One thing `$setAll` is not: a shortcut for the dependency walk. Rendering still coalesces into a single batch, but each write is enqueued individually, so the cost matches the hand-written loop it replaces. What it buys you is the preserved list, not fewer cycles.
|
|
1032
|
+
|
|
971
1033
|
#### `$resolve` — Access by Explicit Index
|
|
972
1034
|
|
|
973
1035
|
`$resolve` reads or writes a value at a specific wildcard index:
|
|
@@ -1755,6 +1817,8 @@ Firing order is defined in three layers, and only the middle one is yours to ste
|
|
|
1755
1817
|
| Between handlers | declaration order in `$watch` | **reorder the declarations** |
|
|
1756
1818
|
| Between rows of one path | ascending `indexes` | fixed |
|
|
1757
1819
|
|
|
1820
|
+
**The one thing that moves the mechanism layer** is a `<wcs-view-transition>` that accepts the `state` participant. Binding application — and with it `$updatedCallback` — then lands on a frame, while `$watch` and the `$streams` restart stay on the microtask the drain was queued on, because they consume state addresses and not the DOM. For as long as the tag is present the order is `$watch` → `$streams` restart → `$updatedCallback`. Nothing else on the page reorders this layer; see [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
|
|
1821
|
+
|
|
1758
1822
|
Key rules:
|
|
1759
1823
|
|
|
1760
1824
|
- **Only its own state** — a path may not carry `@stateName`; watching another state element is rejected at declaration time.
|
|
@@ -2016,6 +2080,31 @@ All hooks except `$disconnectedCallback` support `async` — you can use `async/
|
|
|
2016
2080
|
- `$updatedCallback(paths, indexesListByPath)` receives the paths whose live bindings were applied in that drain. Unbound state writes do not invoke it or appear in `paths`. For wildcard updates, `indexesListByPath` contains the updated index sets. Can be `async`, but the return value is not awaited
|
|
2017
2081
|
- In Web Components, define `async $stateReadyCallback(stateProp)` to receive a hook when the bound state becomes available via `bind-component`
|
|
2018
2082
|
|
|
2083
|
+
## Transition animations
|
|
2084
|
+
|
|
2085
|
+
Enter animations need nothing from this package — a new `for` row and a mounting `if` branch are newly inserted elements, so plain CSS covers them:
|
|
2086
|
+
|
|
2087
|
+
```css
|
|
2088
|
+
li {
|
|
2089
|
+
transition: opacity 0.2s, transform 0.2s;
|
|
2090
|
+
@starting-style { opacity: 0; transform: translateY(-4px); }
|
|
2091
|
+
}
|
|
2092
|
+
```
|
|
2093
|
+
|
|
2094
|
+
**Leaving** and **moving** cannot be reached that way: removed rows are detached synchronously, and a reorder has no intermediate state. Adding [`@wcstack/view-transition`](https://github.com/wcstack/wcstack/tree/main/packages/view-transition) makes the drain apply its DOM changes inside a View Transition, where the browser snapshots the old state for you:
|
|
2095
|
+
|
|
2096
|
+
```html
|
|
2097
|
+
<script type="module" src="https://esm.run/@wcstack/view-transition/auto"></script>
|
|
2098
|
+
<wcs-view-transition naming="auto"></wcs-view-transition>
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
Two consequences to know while that tag accepts the `state` participant:
|
|
2102
|
+
|
|
2103
|
+
- The drain lands on a frame instead of a microtask, so code that writes state and then reads the DOM after `await Promise.resolve()` must wait for the transition. `$updatedCallback` still fires immediately after the bindings are applied — its *position* is unchanged, but it moves a frame later along with them.
|
|
2104
|
+
- Because `$watch` and the `$streams` restart stay on the original microtask, they now run **before** `$updatedCallback` instead of after it.
|
|
2105
|
+
|
|
2106
|
+
Only a batch that actually has bindings to apply is handed to the tag, so a write to a headless path never starts a transition. Without the tag the drain is exactly what it was. See [docs/timing-and-firing-contract.md](https://github.com/wcstack/wcstack/blob/main/docs/timing-and-firing-contract.md) §4.3.
|
|
2107
|
+
|
|
2019
2108
|
## Diagnostics and failure handling
|
|
2020
2109
|
|
|
2021
2110
|
### Wiring to a path that does not exist is reported
|
|
@@ -2050,7 +2139,7 @@ Anything that follows mechanically from the path string is reported at runtime a
|
|
|
2050
2139
|
|
|
2051
2140
|
| Diagnostic | What it checks | Fix |
|
|
2052
2141
|
|---|---|---|
|
|
2053
|
-
| `wcs/index-arity` | `$resolve(path, indexes)` must match the `*` count **exactly**; `$getAll(path, indexes)`
|
|
2142
|
+
| `wcs/index-arity` | `$resolve(path, indexes)` must match the `*` count **exactly**; `$getAll(path, indexes)` / `$setAll(path, indexes, …)` have it as an **upper bound** (fewer is a legitimate prefix meaning "expand the rest") | Match the count |
|
|
2054
2143
|
| `wcs/wildcard-rank` | The path's `*` count (and the N in `$N`) must not exceed the enclosing `for` nesting | Add a `for`, or name the row with `$resolve(path, indexes)` |
|
|
2055
2144
|
| `wcs/getter-cycle` | Path getters must not form a dependency cycle | Break the cycle |
|
|
2056
2145
|
|
|
@@ -2107,13 +2196,43 @@ All options with defaults:
|
|
|
2107
2196
|
|---|---|---|
|
|
2108
2197
|
| `bindAttributeName` | `'data-wcs'` | Binding attribute name |
|
|
2109
2198
|
| `tagNames.state` | `'wcs-state'` | State element tag name |
|
|
2110
|
-
| `locale` | `'en'` |
|
|
2199
|
+
| `locale` | `<html lang>`, else `'en'` | Locale for the locale-dependent filters (`locale` / `date` / `time` / `datetime`) — see [Locale](#locale) |
|
|
2111
2200
|
| `debug` | `false` | Debug mode |
|
|
2112
2201
|
| `enableMustache` | `true` | Enable `{{ }}` syntax |
|
|
2113
2202
|
| `enableDirectionalInitialSync` | `true` | Direction-aware binding authority (`#init=` / `#sync=` binding modifiers) — see [Binding Authority](#binding-authority-init--sync). Default on; set `false` to opt out |
|
|
2114
2203
|
| `enablePropagationContext` | `true` | Causal propagation tracking across bindings (echo/diamond loop prevention). Default on; set `false` to opt out |
|
|
2115
2204
|
| `enableContractAnalyzer` | `false` | Opt-in dev-time contract analyzer (exposes `analyzeContract`) |
|
|
2116
2205
|
|
|
2206
|
+
### Locale
|
|
2207
|
+
|
|
2208
|
+
Four filters format by locale — `locale`, `date`, `time`, `datetime`. They read
|
|
2209
|
+
`config.locale`, which **defaults to `<html lang>`**:
|
|
2210
|
+
|
|
2211
|
+
```html
|
|
2212
|
+
<html lang="ja-JP">
|
|
2213
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
2214
|
+
```
|
|
2215
|
+
|
|
2216
|
+
Nothing else is needed; `<html lang>` is the standard place to record a page's
|
|
2217
|
+
language, and making it the default keeps one source of truth. It also means the
|
|
2218
|
+
CDN one-liner can set the locale at all — `auto` calls `bootstrapState()` with no
|
|
2219
|
+
arguments, so before this there was no way in. An explicit
|
|
2220
|
+
`bootstrapState({ locale })` still wins, and an invalid BCP-47 tag is reported
|
|
2221
|
+
and ignored rather than left to throw inside `Intl`.
|
|
2222
|
+
|
|
2223
|
+
**Changing `config.locale` later does not re-render anything.** It is a global
|
|
2224
|
+
setting, not state, so it is not part of the dependency graph. The filters do
|
|
2225
|
+
read it on every application rather than capturing it when the binding is built,
|
|
2226
|
+
which means a binding that re-renders for its own reasons will pick up the new
|
|
2227
|
+
value — enough to recover from a mis-ordered startup, not enough to switch a
|
|
2228
|
+
page's language. Set the language before the page renders: writing `<html lang>`
|
|
2229
|
+
in the markup, or from a synchronous `<head>` script, does that structurally.
|
|
2230
|
+
|
|
2231
|
+
Per-call overrides stay available and are fixed at bind time, since they are part
|
|
2232
|
+
of the binding expression: `price|locale(fr-FR)`. For a page that switches
|
|
2233
|
+
language without reloading, see [docs/i18n-design.md](../../docs/i18n-design.md) —
|
|
2234
|
+
the short answer is that translations belong on a path, not in a filter.
|
|
2235
|
+
|
|
2117
2236
|
> These three are **architecture-hardening** features; their normative reference is
|
|
2118
2237
|
> `docs/architecture-hardening/`. `enablePropagationContext` defaults **on** — its
|
|
2119
2238
|
> write-path cost is near-zero for one-way bindings (only echo-capable two-way
|