@wcstack/state 4.1.1 → 4.2.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 +20 -17
- package/README.md +20 -17
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.esm.js +73 -34
- package/dist/parser.esm.js +10 -4
- package/dist/split/auto.js +1 -1
- package/dist/split/chunks/chunk-2RBI6DVS.js +2 -0
- package/dist/split/chunks/chunk-2RBI6DVS.js.map +1 -0
- package/dist/split/chunks/{chunk-V3HFN3MW.js → chunk-3Q7LGONS.js} +2 -2
- package/dist/split/chunks/{chunk-V3HFN3MW.js.map → chunk-3Q7LGONS.js.map} +1 -1
- package/dist/split/chunks/{chunk-UFIDBJBP.js → chunk-4ECL4ITF.js} +2 -2
- package/dist/split/chunks/{chunk-7ZU3CLEG.js → chunk-BGUJ3HF6.js} +2 -2
- package/dist/split/chunks/{chunk-4HLDITUB.js → chunk-BI4645UG.js} +2 -2
- package/dist/split/chunks/chunk-BI4645UG.js.map +1 -0
- package/dist/split/chunks/{chunk-JAEYZSVS.js → chunk-BYQBV3ZC.js} +2 -2
- package/dist/split/chunks/{chunk-R5OJ52SF.js → chunk-CIBJLAA3.js} +2 -2
- package/dist/split/chunks/{chunk-R5OJ52SF.js.map → chunk-CIBJLAA3.js.map} +1 -1
- package/dist/split/chunks/chunk-JK3B3LLB.js +2 -0
- package/dist/split/chunks/chunk-OKBVNGFK.js +2 -0
- package/dist/split/chunks/{chunk-JZW5KGXQ.js.map → chunk-OKBVNGFK.js.map} +1 -1
- package/dist/split/chunks/{chunk-GGSHXG7N.js → chunk-UBGJGMSG.js} +2 -2
- package/dist/split/chunks/chunk-UBGJGMSG.js.map +1 -0
- package/dist/split/chunks/{chunk-7QVEF5E3.js → chunk-UPSLR4FS.js} +2 -2
- package/dist/split/core.js +1 -1
- package/dist/split/features/devtools.js +1 -1
- package/dist/split/features/diagnostics.js +1 -1
- package/dist/split/features/diagnostics.js.map +1 -1
- package/dist/split/features/formats.d.ts +1 -1
- package/dist/split/features/formats.js +1 -1
- package/dist/split/features/formats.js.map +1 -1
- package/dist/split/features/list-keys.js +1 -1
- package/dist/split/features/native-commands.js +1 -1
- package/dist/split/features/recursion.js +1 -1
- package/dist/split/features/scopes.js +1 -1
- package/dist/split/features/ssr.js +1 -1
- package/dist/split/features/temporal.js +1 -1
- package/dist/split/features/temporal.js.map +1 -1
- package/dist/wcs-manifest.json +25 -7
- package/package.json +1 -1
- package/dist/split/chunks/chunk-4HLDITUB.js.map +0 -1
- package/dist/split/chunks/chunk-BWGPKDHS.js +0 -2
- package/dist/split/chunks/chunk-BWGPKDHS.js.map +0 -1
- package/dist/split/chunks/chunk-GGSHXG7N.js.map +0 -1
- package/dist/split/chunks/chunk-JZW5KGXQ.js +0 -2
- package/dist/split/chunks/chunk-XEGL4FW5.js +0 -2
- /package/dist/split/chunks/{chunk-UFIDBJBP.js.map → chunk-4ECL4ITF.js.map} +0 -0
- /package/dist/split/chunks/{chunk-7ZU3CLEG.js.map → chunk-BGUJ3HF6.js.map} +0 -0
- /package/dist/split/chunks/{chunk-JAEYZSVS.js.map → chunk-BYQBV3ZC.js.map} +0 -0
- /package/dist/split/chunks/{chunk-XEGL4FW5.js.map → chunk-JK3B3LLB.js.map} +0 -0
- /package/dist/split/chunks/{chunk-7QVEF5E3.js.map → chunk-UPSLR4FS.js.map} +0 -0
package/README.ja.md
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
| **表計算**(VisiCalc, 1979) | アドレス、セルが**何であるか**を宣言する数式、依存グラフ、遅延再計算 — 更新コードはどこにもない | 格子座標ではなく名前を使い、数式をセル単位ではなく*形*単位で書く。`get "cart.items.*.subtotal"()` は各行へフィルダウンされるのではなく、ワイルドカードそのものが定義 |
|
|
57
57
|
| **Cocoa Bindings / KVC–KVO**(NeXT の EOF, 1994/Mac OS X 10.3, 2003) | キーパス(`person.address.street`)、「対象 + キーパス + value transformer」の三つ組、パスに沿って集計するコレクション演算子(`@sum.items.price`) | その三つ組が nib や `bind:toObject:withKeyPath:` の呼び出しではなくマークアップにあるので、grep・lint・diff できる。変更検知は KVC 準拠ではなく素のオブジェクトへの ES Proxy |
|
|
58
58
|
| **XForms**(W3C 勧告, 2003) | model / instance / view の分離、instance を指す `ref` パス、そして `<bind calculate="…">` — **パスの位置に**宣言される算出値であり、パス getter の直系の祖先 | パスはアドレスに徹し、計算は属性内の XPath ではなく state 上の JavaScript getter。XForms プロセッサなしで素のブラウザで動く |
|
|
59
|
-
| **WPF / XAML**(2006) | `{Binding Path=User.Name, Mode=TwoWay}`、部分木ごと基点を張り替える `DataContext`、ソースへの書き戻し時点を選ぶ `UpdateSourceTrigger`、両端を変換する `IValueConverter` | ビジュアルツリーを継承するコンテキストと `RelativeSource` / `ElementName` の脱出口ではなく、ルートごとに 1 本の state ツリー。`state: user` がその基点張り替えで、しかもホストの HTML に書かれる。変換器は登録するクラスではなく
|
|
59
|
+
| **WPF / XAML**(2006) | `{Binding Path=User.Name, Mode=TwoWay}`、部分木ごと基点を張り替える `DataContext`、ソースへの書き戻し時点を選ぶ `UpdateSourceTrigger`、両端を変換する `IValueConverter` | ビジュアルツリーを継承するコンテキストと `RelativeSource` / `ElementName` の脱出口ではなく、ルートごとに 1 本の state ツリー。`state: user` がその基点張り替えで、しかもホストの HTML に書かれる。変換器は登録するクラスではなく 48 個の閉じたフィルタで、コンパイルも不要 |
|
|
60
60
|
| **Android Data Binding**(2015) | レイアウトファイル自体に書くパス — `android:text="@{user.name}"`、双方向は `@={}` | ビルド手順も生成されるバインディングクラスもなく、属性の中に式を書かない |
|
|
61
61
|
| **SCADA / HMI のタグバインド**(産業用・数十年) | 設定だけでウィジェットのプロパティをタグパス(`Line1/Tank/Level`)へ配線する。パスを媒介変数化する*間接*バインド(`Folder/Tag_{1}`)で 1 画面が多数の機器を駆動する | ツリーが持つのはスカラーのフラットな名前空間ではなく、派生値・リスト・マウントされたコンポーネント。媒介変数はドロップダウンから与えるものではなく、バインディングが置かれた行が解決するループのワイルドカード |
|
|
62
62
|
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
| **構造ディレクティブ** | `<template>` 上の `for` と `if` / `elseif` / `else` | [構造ディレクティブ](#構造ディレクティブ) |
|
|
131
131
|
| **行の同一性** | 行は参照で差分を取るので、並べ替えや絞り込みで DOM を再利用する。`$listKeys` は再取得した配列でも行 DOM と行オブジェクトを保つ | [`$listKeys`](#listkeys--再取得された行の同一性) |
|
|
132
132
|
| **フォーム** | `input` / `select` / `textarea` の双方向バインディング、ラジオグループと単一値、チェックボックスグループと配列、`#ro` / `#onchange` / `#prevent` / `#stop` | [双方向バインディング](#双方向バインディング) · [修飾子](#修飾子) |
|
|
133
|
-
| **フィルタ** |
|
|
133
|
+
| **フィルタ** | 48 種類の組み込み、チェーン可能、`<html lang>` を見るロケール依存フォーマット | [フィルタ](#フィルタ) · [ロケール](#ロケール) |
|
|
134
134
|
| **派生状態** | パス getter がデータツリーの任意の深さの仮想プロパティを 1 箇所にフラットに宣言する。チェーンでき、setter も書ける | [パス getter](#パス-getter算出プロパティ) |
|
|
135
135
|
| **集計と一括書き込み** | `$getAll` / `$setAll` / `$resolve` が `items.*.price` を横断して読み書きする。配列は作り直さない | [Proxy API](#proxy-api) |
|
|
136
136
|
| **再帰パス** | `$recursion` が木の形の繰り返し地点を宣言し、1 本の `**` getter が全深さを覆う | [再帰パス](#再帰パスrecursion) |
|
|
@@ -191,9 +191,9 @@
|
|
|
191
191
|
<script type="importmap">
|
|
192
192
|
{
|
|
193
193
|
"imports": {
|
|
194
|
-
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
195
|
-
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
196
|
-
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
194
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/core.js",
|
|
195
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/features/temporal.js",
|
|
196
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/features/scopes.js"
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
</script>
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
| `@wcstack/state/features/scopes` | `bind-component`・`mount=` のボリューム・オーバーレイの公開 getter・DCC(`data-wc-definition`) |
|
|
234
234
|
| `@wcstack/state/features/recursion` | `$recursion` と `**` パス |
|
|
235
235
|
| `@wcstack/state/features/ssr` | `enable-ssr`: サーバー描画とハイドレーション |
|
|
236
|
-
| `@wcstack/state/features/formats` | 残りの
|
|
236
|
+
| `@wcstack/state/features/formats` | 残りの 38 個のフィルタ: 算術・型変換・デフォルト(`add`・`int`・`coalesce` …)と書式(`upper`・`date`・`currency`・`round`・`truncate` …) |
|
|
237
237
|
| `@wcstack/state/features/devtools` | DevTools Hook Protocol への source 登録 |
|
|
238
238
|
| `@wcstack/state/features/diagnostics` | エラーメッセージの文章(did-you-mean と lint への案内を含む)と、開発時の警告(束縛や `$watch` のパスが解決しない、`wcs/default-getter-mismatch`)。入れなければ、メッセージは番号と値だけになり([エラーメッセージ](#エラーメッセージと番号))、これらの警告は出ず、4.0 で外した 3.x の名前(`$scan`・`$streams`・`$updatedCallback`・`$trackDependency`・`$untrackDependency`)も見つけない |
|
|
239
239
|
| `@wcstack/state/define` | `defineState` と型だけ — ランタイムは 0 |
|
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
ページが名指したアドオンを、同じビルドから読み込みます。
|
|
254
254
|
|
|
255
255
|
```html
|
|
256
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
256
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/auto.js"></script>
|
|
257
257
|
<wcs-state features="scopes diagnostics">
|
|
258
258
|
<script type="module">
|
|
259
259
|
export default {
|
|
@@ -1215,7 +1215,7 @@ getter の例外は握り潰されません。評価された場所(バイン
|
|
|
1215
1215
|
|---|---|
|
|
1216
1216
|
| **追跡されるのは `this` を通した *パス* の読み取りだけ。** `this.form` は `form` を、`this["form.name"]` は `form.name` を追跡する。`this.form.name` が追跡するのは **`form` だけ** —— `.name` は返ってきたオブジェクトへの素のプロパティアクセスでしかない。`Date.now()`・DOM・モジュール変数・クロージャで掴んだオブジェクトは何も登録しない | その入力に対して getter は二度と再評価されず、最初の値が残り続ける(上の例)。`this.form.name` を読む getter は、`<input data-wcs="value: form.name">` を編集しても再実行されない —— `this["form.name"]` で読む |
|
|
1217
1217
|
| **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
|
|
1218
|
-
| **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`$behavior.sameValueGuard`。`semantics: "event"`
|
|
1218
|
+
| **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`$behavior.sameValueGuard`。`semantics: "event"` のプロパティはどちらにせよ対象外)。一覧は例外で、行が配列の要素に合わせ直されるのは**新しい**配列を代入したときだけ。同じ配列の再代入や `$postUpdate` は、ある行の下を読み直すが、行を追加・削除・置き換えしない: `cards[i] = card; this.cards = cards` は古いカードを出し続ける。新しい配列を代入する(`this.cards = cards.with(i, card)`、`[...cards, x]`)。lint はその場の変更を報告する(`wcs/array-index-assign`、`wcs/array-mutation`) |
|
|
1219
1219
|
|
|
1220
1220
|
規則 1 は静的解析で捕まえられる唯一の規則です。getter が `this.form.name` を読んでいて、ドキュメントのどこかで `form.name` を書いている(`value:` バインド・spread・`this["form.name"] = …`)と、`wcs-validate` と VS Code 拡張が `wcs/getter-untracked-read` を報告します。ルートを丸ごと置換するだけの設計(router の params・`$stream` の fold)には出ません。
|
|
1221
1221
|
|
|
@@ -1674,7 +1674,7 @@ export default {
|
|
|
1674
1674
|
|
|
1675
1675
|
## フィルタ
|
|
1676
1676
|
|
|
1677
|
-
|
|
1677
|
+
48 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます: core に条件の 10 個のフィルタ(`eq`・`ne`・`not`・`lt`・`le`・`gt`・`ge`・`truthy`・`falsy`・`boolean`)、`formats` アドオンに残りの 38 個(算術・数値フォーマット・文字列・型変換・日付 / 時刻・デフォルト)があり、全部入りのエントリには両方が含まれます。`formats` なしの `/core` で `formats` のフィルタを使うと、束縛計画の段でアドオン名を挙げて失敗します。
|
|
1678
1678
|
|
|
1679
1679
|
3.x が 3.2 の改名の別名として残していた名前(`inc`・`dec`・`fix`・`uc`・`lc`・`cap`・`rep`・`rev`・`pad`・`null`)と `substr` フィルタは存在しません: 未知の名前と同じく失敗し、メッセージが代わりに書くものを示します(`"uc" was renamed "upper" in 3.2 and removed in 4.0 — write "upper".`。`substr(start, length)` は `slice(start, start + length)`)。
|
|
1680
1680
|
|
|
@@ -1711,6 +1711,7 @@ export default {
|
|
|
1711
1711
|
| `floor(n?)` | 切り捨て | `value\|floor` |
|
|
1712
1712
|
| `ceil(n?)` | 切り上げ | `value\|ceil` |
|
|
1713
1713
|
| `locale(loc?)` | ロケール数値フォーマット | `count\|locale` / `count\|locale(ja-JP)` |
|
|
1714
|
+
| `currency(code, loc?)` | ISO 4217 の通貨で書く | `price\|currency(JPY)` / `price\|currency(USD,en-US)` → `"$1,234.50"` |
|
|
1714
1715
|
| `percent(n?)` | パーセンテージフォーマット | `ratio\|percent(1)` |
|
|
1715
1716
|
| `unit(u)` | 単位(任意の接尾辞)を付加 | `width\|unit(px)` → `"40px"` |
|
|
1716
1717
|
|
|
@@ -1745,13 +1746,13 @@ export default {
|
|
|
1745
1746
|
|
|
1746
1747
|
| フィルタ | 説明 | 例 |
|
|
1747
1748
|
|---|---|---|
|
|
1748
|
-
| `date(loc?)` | 日付フォーマット | `createdAt\|date` / `createdAt\|date(ja-JP)` |
|
|
1749
|
-
| `time(loc?)` | 時刻フォーマット | `createdAt\|time` |
|
|
1750
|
-
| `datetime(loc?)` | 日付 + 時刻 | `createdAt\|datetime(en-US)` |
|
|
1749
|
+
| `date(loc?, style?)` | 日付フォーマット | `createdAt\|date` / `createdAt\|date(ja-JP)` / `createdAt\|date(en-US,medium)` → `"Oct 13, 2026"` |
|
|
1750
|
+
| `time(loc?, style?)` | 時刻フォーマット | `createdAt\|time` / `createdAt\|time(short)` |
|
|
1751
|
+
| `datetime(loc?, style?)` | 日付 + 時刻 | `createdAt\|datetime(en-US)` / `createdAt\|datetime(ja-JP,long)` |
|
|
1751
1752
|
| `ymd(sep?)` | YYYY-MM-DD(既定は `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
|
|
1752
1753
|
| `hms(sep?)` | HH:MM:SS(既定は `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
|
|
1753
1754
|
|
|
1754
|
-
**5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定はページのロケール([ロケール](#ロケール))で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)`
|
|
1755
|
+
**5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定はページのロケール([ロケール](#ロケール))で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)` は束縛式の一部なので組み立て時に固定されます)。`style?` は `full`・`long`・`medium`・`short`(Intl の `dateStyle` / `timeStyle`)で、無ければロケールの数字の書式(`2026/10/13`)、書式だけならページのロケールです(`date(medium)`)。`currency` の通貨は束縛を組み立てるときに確かめ、数は `loc` が無ければページのロケールで書きます。複数形などほかの書式は getter で書きます(`Intl.PluralRules`、`Intl.NumberFormat`)。
|
|
1755
1756
|
|
|
1756
1757
|
### 真偽値 / デフォルト
|
|
1757
1758
|
|
|
@@ -2579,7 +2580,7 @@ $renderedCallback(paths) {
|
|
|
2579
2580
|
|
|
2580
2581
|
**getter を watch すると eager になります。** computed は本来 lazy で、依存は評価時にしか記録されません —— つまり描画していない getter は本来一度も発火しません。`$watch` に宣言すると接続時に 1 回 —— ワイルドカード getter(`items.*.tax`)なら全行について —— 評価され、以後は依存に触れたバッチの終端で毎回評価されます。`prev` は前回の評価値で、変更が届けば、値が変わったかどうかに関わらず watch は発火します(上の 2 つの場合を除く)。重い computed を watch すればその評価コストが毎バッチ乗り、getter 内の例外は眠ったままにならず watch 経由で表面化します。接続時の評価が throw した場合は報告され(`$watch initial evaluation of "…" threw.`)、その watch の最初の `prev` は `undefined` になり、ほかはすべて続行します。
|
|
2581
2582
|
|
|
2582
|
-
**要素が既に持っていた値でも発火します。** watch は `$connectedCallback` の後に始まり、それより前の書き込みでは発火しません。しかし watch するパスに束ねた要素は、バインディングがつながった時点で値を既に持っていること(開いている `<wcs-sse>` の `connected`、router の最初の `typedParams`)も、少し後に報告することもあり、どちらになるかはページの読み込みの速さで変わります。そこで、watch が始まる前に要素が watch するパスへ書いた値は、始まったときにそのパスがまだその値なら、`prev` をシードにして 1
|
|
2583
|
+
**要素が既に持っていた値でも発火します。** watch は `$connectedCallback` の後に始まり、それより前の書き込みでは発火しません。しかし watch するパスに束ねた要素は、バインディングがつながった時点で値を既に持っていること(開いている `<wcs-sse>` の `connected`、router の最初の `typedParams`)も、少し後に報告することもあり、どちらになるかはページの読み込みの速さで変わります。そこで、watch が始まる前に要素が watch するパスへ書いた値は、始まったときにそのパスがまだその値なら、`prev` をシードにして 1 回発火します。そのパスを読む根の getter の watch —— `<wcs-storage>` が保存していたセッションを読む `get loggedIn() { return this["session.token"] != null; }` —— も 1 回発火し、`prev` は `undefined` です(getter にはそれより前の値がありません)。それより前のコードからの書き込み —— `$connectedCallback` —— は今までどおり発火せず、シードと同じ値はそもそも書き込まれません。
|
|
2583
2584
|
|
|
2584
2585
|
発火順序は 3 層に分かれ、利用者が意思を持てるのは真ん中の層だけです。
|
|
2585
2586
|
|
|
@@ -2915,7 +2916,7 @@ li {
|
|
|
2915
2916
|
| 9 | | `$setAll("<path>") needs indexes ([] for every match)` |
|
|
2916
2917
|
| 10 | | `$setAll("<path>", …, { spread: true }) needs an array of <n> values` |
|
|
2917
2918
|
| 11 | | `updates did not settle after 32 passes` |
|
|
2918
|
-
| 12 | | `binding "<
|
|
2919
|
+
| 12 | | `binding "<member>: <path>" failed to apply.`(`<member>` は書いたとおりの名前 — `textContent`・`class.active` —、名前の無い `{{ }}` などは種類 `text`。console には要素も添える) |
|
|
2919
2920
|
| 13 | | `failed to load "<src>": <status>` |
|
|
2920
2921
|
| 14 | | `this <wcs-state> failed to initialize; create a new one` |
|
|
2921
2922
|
| 15 | | `state is not initialized` |
|
|
@@ -2931,8 +2932,8 @@ li {
|
|
|
2931
2932
|
| 25 | | `view-transition naming-limit (<limit>) reached.` |
|
|
2932
2933
|
| 26 | | `filter <name> requires at least one option` |
|
|
2933
2934
|
| 27 | | `filter <name> requires a number as option` |
|
|
2934
|
-
| 28 | | `filter <name> requires a number value` |
|
|
2935
|
-
| 29 | | `filter <name> requires a date value` |
|
|
2935
|
+
| 28 | | `filter <name> requires a number value, not <what it was>` (a string adds: `— put |number before |<name>, or convert it where it is read`) |
|
|
2936
|
+
| 29 | | `filter <name> requires a date value (a Date), not <what it was>` (a string or a number adds: `— make the Date where it is read: a getter returning new Date(…)`) |
|
|
2936
2937
|
| 30 | | `filter <name> requires an array value` |
|
|
2937
2938
|
| 31 | | `init=/sync= modifiers require enableDirectionalInitialSync.` |
|
|
2938
2939
|
| 32 | | `"<text>": the "@name" selector was removed in v2 — there is a single state tree. …` |
|
|
@@ -2955,6 +2956,8 @@ li {
|
|
|
2955
2956
|
| 49 | | `<wcs-state src="…"> failed to initialize.` |
|
|
2956
2957
|
| 50 | | `<wcs-state src="…"> $connectedCallback failed.` |
|
|
2957
2958
|
| 51 | | `<wcs-state src="…"> $disconnectedCallback failed.` |
|
|
2959
|
+
| 52 | | `filter <name> takes a style of full, long, medium or short, not "<style>"` |
|
|
2960
|
+
| 53 | | `filter currency takes an ISO 4217 currency code (JPY, USD, EUR), not "<code>"` |
|
|
2958
2961
|
| 101 | `binding-syntax` | `Invalid bindText: "<text>". Missing ':' separator between propPart and statePart.` |
|
|
2959
2962
|
| 102 | `binding-syntax` | `"<text>": "<keyword>" takes no modifiers or filters on its left side — write "<keyword>:".` |
|
|
2960
2963
|
| 103 | `binding-syntax` | `"<text>": "else" takes no value — write "else:".` |
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ The premise — *a path string is the whole contract between a view and a model*
|
|
|
56
56
|
| **Spreadsheets** (VisiCalc, 1979) | An address, a formula declaring what a cell **is**, a dependency graph, lazy recomputation — and no update code anywhere | Names instead of grid coordinates, and one formula per *shape* rather than per cell: `get "cart.items.*.subtotal"()` is not filled down into the rows; the wildcard is the definition |
|
|
57
57
|
| **Cocoa Bindings / KVC–KVO** (NeXT's EOF, 1994; Mac OS X 10.3, 2003) | Key paths (`person.address.street`), a binding triple of target + key path + value transformer, and collection operators that aggregate along a path (`@sum.items.price`) | The triple lives in the markup instead of a nib or a `bind:toObject:withKeyPath:` call, so it can be grepped, linted and diffed. Change detection is an ES Proxy over plain objects rather than KVC compliance |
|
|
58
58
|
| **XForms** (W3C Recommendation, 2003) | Model / instance / view separation, `ref` paths into the instance, and `<bind calculate="…">` — a computed value declared **at a path**, the direct ancestor of a path getter | The path is an address and nothing else: the computation is a JavaScript getter on the state, not XPath inside an attribute. And it runs in a stock browser, with no XForms processor |
|
|
59
|
-
| **WPF / XAML** (2006) | `{Binding Path=User.Name, Mode=TwoWay}`; a `DataContext` that re-roots a whole subtree; `UpdateSourceTrigger` choosing when the source is written; `IValueConverter` between the ends | One state tree per root, rather than a context inherited down the visual tree with `RelativeSource` / `ElementName` escapes — `state: user` is that re-rooting, written in the host's HTML. Converters are a closed set of
|
|
59
|
+
| **WPF / XAML** (2006) | `{Binding Path=User.Name, Mode=TwoWay}`; a `DataContext` that re-roots a whole subtree; `UpdateSourceTrigger` choosing when the source is written; `IValueConverter` between the ends | One state tree per root, rather than a context inherited down the visual tree with `RelativeSource` / `ElementName` escapes — `state: user` is that re-rooting, written in the host's HTML. Converters are a closed set of 48 filters, not classes you register, and nothing is compiled |
|
|
60
60
|
| **Android Data Binding** (2015) | The path in the layout file itself — `android:text="@{user.name}"`, `@={}` for two-way | No build step and no generated binding class, and no expressions inside the attribute |
|
|
61
61
|
| **SCADA / HMI tag binding** (industrial, decades) | Widget properties wired to tag paths (`Line1/Tank/Level`) by configuration alone, and *indirect* bindings that parameterize the path (`Folder/Tag_{1}`) so one screen drives many devices | The tree carries derived values, lists and mounted components, not a flat namespace of scalars; the parameter is a loop's wildcard, resolved by the row the binding sits in rather than assigned from a dropdown |
|
|
62
62
|
|
|
@@ -130,7 +130,7 @@ Every row is a section of this README. Unless it appears under [Where the neighb
|
|
|
130
130
|
| **Structural directives** | `for` and `if` / `elseif` / `else` on `<template>` elements | [Structural directives](#structural-directives) |
|
|
131
131
|
| **Row identity** | Rows diff by reference, so a sort or a filter reuses the DOM; `$listKeys` keeps row DOM and row objects across refetched arrays | [`$listKeys`](#listkeys--identity-for-refetched-rows) |
|
|
132
132
|
| **Forms** | Two-way binding for `input` / `select` / `textarea`, a radio group to one value, a checkbox group to an array, `#ro` / `#onchange` / `#prevent` / `#stop` | [Two-way binding](#two-way-binding) · [Modifiers](#modifiers) |
|
|
133
|
-
| **Filters** |
|
|
133
|
+
| **Filters** | 48 built-ins, chainable, locale-aware formatting that reads `<html lang>` | [Filters](#filters) · [Locale](#locale) |
|
|
134
134
|
| **Derived state** | Path getters declare virtual properties at any depth from one flat place; they chain, and they take setters | [Path getters](#path-getters-computed-properties) |
|
|
135
135
|
| **Aggregation and bulk write** | `$getAll` / `$setAll` / `$resolve` read and write across `items.*.price` without rebuilding the array | [Proxy APIs](#proxy-apis) |
|
|
136
136
|
| **Recursive paths** | `$recursion` declares where a tree's shape repeats; one `**` getter covers every depth | [Recursive paths](#recursive-paths-recursion) |
|
|
@@ -192,9 +192,9 @@ leaves features out, it can compose them instead:
|
|
|
192
192
|
<script type="importmap">
|
|
193
193
|
{
|
|
194
194
|
"imports": {
|
|
195
|
-
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
196
|
-
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
197
|
-
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
195
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/core.js",
|
|
196
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/features/temporal.js",
|
|
197
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/features/scopes.js"
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
</script>
|
|
@@ -234,7 +234,7 @@ The file names under `dist/split/chunks/` carry a content hash: if you list chun
|
|
|
234
234
|
| `@wcstack/state/features/scopes` | `bind-component`, `mount=` volumes, overlay exports, DCC (`data-wc-definition`) |
|
|
235
235
|
| `@wcstack/state/features/recursion` | `$recursion` and `**` paths |
|
|
236
236
|
| `@wcstack/state/features/ssr` | `enable-ssr`: server rendering and hydration |
|
|
237
|
-
| `@wcstack/state/features/formats` | The other
|
|
237
|
+
| `@wcstack/state/features/formats` | The other 38 filters: arithmetic, conversion and defaults (`add`, `int`, `coalesce`, …) and formatting (`upper`, `date`, `currency`, `round`, `truncate`, …) |
|
|
238
238
|
| `@wcstack/state/features/devtools` | The DevTools hook protocol source |
|
|
239
239
|
| `@wcstack/state/features/diagnostics` | The sentences of error messages, with the did-you-mean and the pointer to lint, and the development-time warnings (a bound or `$watch` path that does not resolve, `wcs/default-getter-mismatch`). Without it a message is its number and its values ([Error messages](#error-messages-and-numbers)), those warnings are not printed, and the 3.x names 4.0 removed (`$scan`, `$streams`, `$updatedCallback`, `$trackDependency`, `$untrackDependency`) are not detected |
|
|
240
240
|
| `@wcstack/state/define` | `defineState` and the types only — no runtime at all |
|
|
@@ -254,7 +254,7 @@ receptacle the owning add-on fills on install).
|
|
|
254
254
|
core, and the add-ons the page names from the same build.
|
|
255
255
|
|
|
256
256
|
```html
|
|
257
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
257
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.2.0/dist/split/auto.js"></script>
|
|
258
258
|
<wcs-state features="scopes diagnostics">
|
|
259
259
|
<script type="module">
|
|
260
260
|
export default {
|
|
@@ -1221,7 +1221,7 @@ Three rules decide what the dependency graph sees. None of them matters until yo
|
|
|
1221
1221
|
|---|---|
|
|
1222
1222
|
| **Only path reads through `this` are tracked.** `this.form` tracks `form`; `this["form.name"]` tracks `form.name`; `this.form.name` tracks **`form` only** — the `.name` is a plain property access on the object that came back. `Date.now()`, the DOM, a module variable, a closed-over object register nothing | The getter is never re-evaluated for that input; the first value sticks (the examples above). A getter that reads `this.form.name` does not re-run when a bound `<input data-wcs="value: form.name">` changes — read `this["form.name"]` |
|
|
1223
1223
|
| **Reads inside a setter are not tracked.** A setter is an imperative assignment, not a derivation, so nothing it reads becomes a dependency of anything | A setter that reads `this.a` to decide what to write does not run again when `a` changes — only a getter re-runs |
|
|
1224
|
-
| **The same-value guard applies to primitives only.** A primitive write `Object.is`-equal to the current value is dropped before anything is enqueued; an object or array write always passes, even the same reference | Assigning the same string again fires nothing; assigning the same object again re-fires its bindings and `$watch` (`$behavior.sameValueGuard`; a `semantics: "event"` property is exempt either way) |
|
|
1224
|
+
| **The same-value guard applies to primitives only.** A primitive write `Object.is`-equal to the current value is dropped before anything is enqueued; an object or array write always passes, even the same reference | Assigning the same string again fires nothing; assigning the same object again re-fires its bindings and `$watch` (`$behavior.sameValueGuard`; a `semantics: "event"` property is exempt either way). A list is the exception: its rows follow the array's items only when a **new** array is assigned. The same array assigned again — or `$postUpdate` — re-reads what is below the rows it has, but adds, removes or replaces no row: `cards[i] = card; this.cards = cards` keeps showing the old card. Assign a new array (`this.cards = cards.with(i, card)`, `[...cards, x]`); lint reports the in-place change (`wcs/array-index-assign`, `wcs/array-mutation`) |
|
|
1225
1225
|
|
|
1226
1226
|
The first rule is the one static analysis can catch: `wcs-validate` and the VS Code extension report `wcs/getter-untracked-read` when a getter reads `this.form.name` and the document writes `form.name` somewhere (a `value:` binding, a spread, `this["form.name"] = …`). A root that is only ever replaced wholesale — router params, a `$stream` fold — is left alone.
|
|
1227
1227
|
|
|
@@ -1680,7 +1680,7 @@ Handlers run where the DOM puts them. An outer `#direct` handler runs **before**
|
|
|
1680
1680
|
|
|
1681
1681
|
## Filters
|
|
1682
1682
|
|
|
1683
|
-
|
|
1683
|
+
48 built-in filters are available for both input (DOM → state) and output (state → DOM) directions: the 10 condition filters in the core (`eq`, `ne`, `not`, `lt`, `le`, `gt`, `ge`, `truthy`, `falsy`, `boolean`) and the other 38 in the `formats` add-on (arithmetic, number formatting, string, type conversion, date / time, defaults), which the full entries include. On `/core` without `formats`, a filter of `formats` fails when the bindings are planned, naming the add-on.
|
|
1684
1684
|
|
|
1685
1685
|
The names 3.x kept as aliases of the 3.2 renames (`inc`, `dec`, `fix`, `uc`, `lc`, `cap`, `rep`, `rev`, `pad`, `null`) and the `substr` filter do not exist: they fail like any unknown name, and the message names what to write instead (`"uc" was renamed "upper" in 3.2 and removed in 4.0 — write "upper".`; `substr(start, length)` is `slice(start, start + length)`).
|
|
1686
1686
|
|
|
@@ -1717,6 +1717,7 @@ The names 3.x kept as aliases of the 3.2 renames (`inc`, `dec`, `fix`, `uc`, `lc
|
|
|
1717
1717
|
| `floor(n?)` | Floor | `value\|floor` |
|
|
1718
1718
|
| `ceil(n?)` | Ceiling | `value\|ceil` |
|
|
1719
1719
|
| `locale(loc?)` | Locale number format | `count\|locale` / `count\|locale(ja-JP)` |
|
|
1720
|
+
| `currency(code, loc?)` | Money in an ISO 4217 currency | `price\|currency(JPY)` / `price\|currency(USD,en-US)` → `"$1,234.50"` |
|
|
1720
1721
|
| `percent(n?)` | Percentage format | `ratio\|percent(1)` |
|
|
1721
1722
|
| `unit(u)` | Append a unit (any suffix) | `width\|unit(px)` → `"40px"` |
|
|
1722
1723
|
|
|
@@ -1751,13 +1752,13 @@ The names 3.x kept as aliases of the 3.2 renames (`inc`, `dec`, `fix`, `uc`, `lc
|
|
|
1751
1752
|
|
|
1752
1753
|
| Filter | Description | Example |
|
|
1753
1754
|
|---|---|---|
|
|
1754
|
-
| `date(loc?)` | Date format | `createdAt\|date` / `createdAt\|date(ja-JP)` |
|
|
1755
|
-
| `time(loc?)` | Time format | `createdAt\|time` |
|
|
1756
|
-
| `datetime(loc?)` | Date + Time | `createdAt\|datetime(en-US)` |
|
|
1755
|
+
| `date(loc?, style?)` | Date format | `createdAt\|date` / `createdAt\|date(ja-JP)` / `createdAt\|date(en-US,medium)` → `"Oct 13, 2026"` |
|
|
1756
|
+
| `time(loc?, style?)` | Time format | `createdAt\|time` / `createdAt\|time(short)` |
|
|
1757
|
+
| `datetime(loc?, style?)` | Date + Time | `createdAt\|datetime(en-US)` / `createdAt\|datetime(ja-JP,long)` |
|
|
1757
1758
|
| `ymd(sep?)` | YYYY-MM-DD (default `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
|
|
1758
1759
|
| `hms(sep?)` | HH:MM:SS (default `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
|
|
1759
1760
|
|
|
1760
|
-
**All five take a `Date`,** not a timestamp — a value of any other type is rejected with "requires a date value". State loaded from JSON carries a number or an ISO string, so convert it where it is read rather than in the binding: `get createdAt() { return new Date(this.createdAtMs); }`. `loc?` defaults to the page locale ([Locale](#locale)), and the default is re-read on **every** apply, so a locale settled after the bindings were planned still takes effect (an explicit `date(ja-JP)` is part of the binding expression and is fixed when it is planned).
|
|
1761
|
+
**All five take a `Date`,** not a timestamp — a value of any other type is rejected with "requires a date value". State loaded from JSON carries a number or an ISO string, so convert it where it is read rather than in the binding: `get createdAt() { return new Date(this.createdAtMs); }`. `loc?` defaults to the page locale ([Locale](#locale)), and the default is re-read on **every** apply, so a locale settled after the bindings were planned still takes effect (an explicit `date(ja-JP)` is part of the binding expression and is fixed when it is planned). `style?` is `full`, `long`, `medium` or `short` (Intl's `dateStyle` / `timeStyle`); without it the format is the locale's numeric one (`10/13/2026`), and a style alone keeps the page locale (`date(medium)`). `currency`'s code is checked when the bindings are planned; the number is formatted in the page locale unless `loc` names one. Plurals and other formats belong in a getter (`Intl.PluralRules`, `Intl.NumberFormat`).
|
|
1761
1762
|
|
|
1762
1763
|
### Boolean / Default
|
|
1763
1764
|
|
|
@@ -2589,7 +2590,7 @@ The handler runs with `this` bound to a **writable** state proxy, so it can writ
|
|
|
2589
2590
|
|
|
2590
2591
|
**Watching a getter makes it eager.** A computed getter is normally lazy, and its dependencies are only recorded when it is evaluated — so an unrendered getter would never fire at all. Declaring one in `$watch` evaluates it once at connect — for every row, when it is a wildcard getter (`items.*.tax`) — and again at the end of every batch that touches its dependencies. Its `prev` is the previous evaluation, and a change that reaches it fires the watch whether or not the value changed (but for the two cases above). Watch a heavy computed and you pay that evaluation on every batch; exceptions inside it surface through the watch instead of staying dormant. An evaluation at connect that throws is reported (`$watch initial evaluation of "…" threw.`), that watch's first `prev` is `undefined`, and everything else goes on.
|
|
2591
2592
|
|
|
2592
|
-
**A value an element already had fires too.** Watches start after `$connectedCallback`; a write before then does not fire them. But an element bound to a watched path may already hold its value when the binding attaches — an open `<wcs-sse>`'s `connected`, the router's first `typedParams` — or may report it a moment later, depending on how fast the page loaded. So an element's write to a watched path before the watches start fires the watch once when they start, with `prev` the seed, if the path still holds that value. A write from code before then — `$connectedCallback` — still fires nothing, and a value equal to the seed was never written.
|
|
2593
|
+
**A value an element already had fires too.** Watches start after `$connectedCallback`; a write before then does not fire them. But an element bound to a watched path may already hold its value when the binding attaches — an open `<wcs-sse>`'s `connected`, the router's first `typedParams` — or may report it a moment later, depending on how fast the page loaded. So an element's write to a watched path before the watches start fires the watch once when they start, with `prev` the seed, if the path still holds that value. A watch on a root getter that reads such a path — `get loggedIn() { return this["session.token"] != null; }` over a `<wcs-storage>`'s saved session — fires once too, with `prev` `undefined` (the getter had no value before). A write from code before then — `$connectedCallback` — still fires nothing, and a value equal to the seed was never written.
|
|
2593
2594
|
|
|
2594
2595
|
Firing order is defined in three layers, and only the middle one is yours to steer:
|
|
2595
2596
|
|
|
@@ -2925,7 +2926,7 @@ The first line is what `@wcstack/state` and `/auto` print, the second what `/cor
|
|
|
2925
2926
|
| 9 | | `$setAll("<path>") needs indexes ([] for every match)` |
|
|
2926
2927
|
| 10 | | `$setAll("<path>", …, { spread: true }) needs an array of <n> values` |
|
|
2927
2928
|
| 11 | | `updates did not settle after 32 passes` |
|
|
2928
|
-
| 12 | | `binding "<
|
|
2929
|
+
| 12 | | `binding "<member>: <path>" failed to apply.` (`<member>` as written — `textContent`, `class.active` —, or the type, `text`, for one without a name such as `{{ }}`; the console gets the node too) |
|
|
2929
2930
|
| 13 | | `failed to load "<src>": <status>` |
|
|
2930
2931
|
| 14 | | `this <wcs-state> failed to initialize; create a new one` |
|
|
2931
2932
|
| 15 | | `state is not initialized` |
|
|
@@ -2941,8 +2942,8 @@ The first line is what `@wcstack/state` and `/auto` print, the second what `/cor
|
|
|
2941
2942
|
| 25 | | `view-transition naming-limit (<limit>) reached.` |
|
|
2942
2943
|
| 26 | | `filter <name> requires at least one option` |
|
|
2943
2944
|
| 27 | | `filter <name> requires a number as option` |
|
|
2944
|
-
| 28 | | `filter <name> requires a number value` |
|
|
2945
|
-
| 29 | | `filter <name> requires a date value` |
|
|
2945
|
+
| 28 | | `filter <name> requires a number value, not <what it was>` (a string adds: `— put |number before |<name>, or convert it where it is read`) |
|
|
2946
|
+
| 29 | | `filter <name> requires a date value (a Date), not <what it was>` (a string or a number adds: `— make the Date where it is read: a getter returning new Date(…)`) |
|
|
2946
2947
|
| 30 | | `filter <name> requires an array value` |
|
|
2947
2948
|
| 31 | | `init=/sync= modifiers require enableDirectionalInitialSync.` |
|
|
2948
2949
|
| 32 | | `"<text>": the "@name" selector was removed in v2 — there is a single state tree. …` |
|
|
@@ -2965,6 +2966,8 @@ The first line is what `@wcstack/state` and `/auto` print, the second what `/cor
|
|
|
2965
2966
|
| 49 | | `<wcs-state src="…"> failed to initialize.` |
|
|
2966
2967
|
| 50 | | `<wcs-state src="…"> $connectedCallback failed.` |
|
|
2967
2968
|
| 51 | | `<wcs-state src="…"> $disconnectedCallback failed.` |
|
|
2969
|
+
| 52 | | `filter <name> takes a style of full, long, medium or short, not "<style>"` |
|
|
2970
|
+
| 53 | | `filter currency takes an ISO 4217 currency code (JPY, USD, EUR), not "<code>"` |
|
|
2968
2971
|
| 101 | `binding-syntax` | `Invalid bindText: "<text>". Missing ':' separator between propPart and statePart.` |
|
|
2969
2972
|
| 102 | `binding-syntax` | `"<text>": "<keyword>" takes no modifiers or filters on its left side — write "<keyword>:".` |
|
|
2970
2973
|
| 103 | `binding-syntax` | `"<text>": "else" takes no value — write "else:".` |
|