@wcstack/state 4.1.1 → 4.3.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 +23 -19
- package/README.md +23 -19
- 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 +78 -38
- 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-5Q4M6A3C.js +2 -0
- package/dist/split/chunks/{chunk-7QVEF5E3.js → chunk-6I7GGFNJ.js} +2 -2
- package/dist/split/chunks/{chunk-7ZU3CLEG.js → chunk-BGUJ3HF6.js} +2 -2
- package/dist/split/chunks/chunk-BKLFDVOP.js +2 -0
- package/dist/split/chunks/chunk-BKLFDVOP.js.map +1 -0
- 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-DOWWBZVS.js +2 -0
- package/dist/split/chunks/chunk-F3DVWVH3.js +2 -0
- package/dist/split/chunks/chunk-F3DVWVH3.js.map +1 -0
- package/dist/split/chunks/chunk-M67C46SN.js +2 -0
- package/dist/split/chunks/{chunk-JZW5KGXQ.js.map → chunk-M67C46SN.js.map} +1 -1
- package/dist/split/chunks/chunk-NMOSSDKJ.js +2 -0
- package/dist/split/chunks/chunk-NMOSSDKJ.js.map +1 -0
- 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/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/ssr.js.map +1 -1
- package/dist/split/features/temporal.js +1 -1
- package/dist/split/features/temporal.js.map +1 -1
- package/dist/wcs-manifest.json +29 -15
- package/package.json +1 -1
- package/dist/split/chunks/chunk-4HLDITUB.js +0 -2
- 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-JAEYZSVS.js +0 -2
- package/dist/split/chunks/chunk-JZW5KGXQ.js +0 -2
- package/dist/split/chunks/chunk-UFIDBJBP.js +0 -2
- package/dist/split/chunks/chunk-UFIDBJBP.js.map +0 -1
- package/dist/split/chunks/chunk-V3HFN3MW.js +0 -2
- package/dist/split/chunks/chunk-V3HFN3MW.js.map +0 -1
- package/dist/split/chunks/chunk-XEGL4FW5.js +0 -2
- /package/dist/split/chunks/{chunk-XEGL4FW5.js.map → chunk-5Q4M6A3C.js.map} +0 -0
- /package/dist/split/chunks/{chunk-7QVEF5E3.js.map → chunk-6I7GGFNJ.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-DOWWBZVS.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.3.0/dist/split/core.js",
|
|
195
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.3.0/dist/split/features/temporal.js",
|
|
196
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.3.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.3.0/dist/split/auto.js"></script>
|
|
257
257
|
<wcs-state features="scopes diagnostics">
|
|
258
258
|
<script type="module">
|
|
259
259
|
export default {
|
|
@@ -760,6 +760,7 @@ export default {
|
|
|
760
760
|
- **値は決してマークアップにならない。** バインディングがページに入れるもの —— `textContent:` や `innerHTML:` の値、カスタム要素がバインドされた値から描画する子 —— がバインディングとして読まれることはありません: その中の `{{ }}`・`data-wcs`・コメントバインディングは、ただのテキストのまま残ります。
|
|
761
761
|
- バインディングが中身を設定する要素(`textContent:`・`text:`・`innerText:`・`innerHTML:`・`html:`)の子はバインドされません。ページレベルでもテンプレートの中でも同じで、`#init=element` や `#init=none` によって書いた子がそのまま残る場合も同じです。`outerHTML:` / `outerText:` をバインドしたページレベルの要素の子もバインドされません。バインディングが必要なマークアップは別の要素に置いてください。
|
|
762
762
|
- `<script>`・`<style>`・`<noscript>`・`<iframe>` の中身は走査しません。その要素自身の `data-wcs`(`srcdoc:`、`attr.src:`)はバインドされます。
|
|
763
|
+
- **`data-wcs-ignore` の付いた要素はまったく走査しません。** その要素自身の `data-wcs` も中身も — そこにある `{{ }}`・`data-wcs`・コメントバインディングは、ページレベルでも、テンプレートの中でも、router が渡す中身でも読みません。ページが書いていないマークアップのための印です([セキュリティ](#セキュリティ))。属性名はバインド属性の名前に `-ignore` を付けたものなので、`bindAttributeName` に従います。4.3 から。
|
|
763
764
|
- `<wcs-state>` 要素の内側に書いたマークアップはページの一部です。Light DOM コンポーネントの中身は、ページではなくそのコンポーネント自身のエンジンがバインドします。
|
|
764
765
|
- ページレベルでは、要素の子が要素自身のバインディングより先にバインドされます。そのため、カスタム要素が最初のプロパティ書き込みを受け取るのは子がバインドされた後で、`$errorCallback` も子の失敗から先に受け取ります。テンプレートの中では文書順です。
|
|
765
766
|
- `for:` の行と `if:` の枝は、`data-wcs` 属性を除いたテンプレートの複製です。そのため `[data-wcs…]` に対する CSS ルールやテストのセレクタはそれらの要素にマッチしません —— クラスや自前の `data-*` 属性を使ってください。
|
|
@@ -1215,7 +1216,7 @@ getter の例外は握り潰されません。評価された場所(バイン
|
|
|
1215
1216
|
|---|---|
|
|
1216
1217
|
| **追跡されるのは `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
1218
|
| **setter の中の読み取りは追跡しない。** setter は命令的な代入であって派生ではないので、その中で読んだものは何の依存にもならない | 何を書くかを `this.a` を読んで決める setter は、`a` が変わっても再実行されない。再実行されるのは getter だけ |
|
|
1218
|
-
| **同値ガードはプリミティブにだけ効く。** 現在値と `Object.is` で等しいプリミティブの書き込みはキューに入る前に落とされる。オブジェクト・配列の書き込みは同じ参照でも必ず通る | 同じ文字列を再代入しても何も起きない。同じオブジェクトを再代入するとバインディングと `$watch` が再発火する(`$behavior.sameValueGuard`。`semantics: "event"`
|
|
1219
|
+
| **同値ガードはプリミティブにだけ効く。** 現在値と `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
1220
|
|
|
1220
1221
|
規則 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
1222
|
|
|
@@ -1410,7 +1411,7 @@ export default {
|
|
|
1410
1411
|
};
|
|
1411
1412
|
```
|
|
1412
1413
|
|
|
1413
|
-
**同じオブジェクトが 2 つの位置にあるとき。** 1 つのリストの 2 つの位置に同じオブジェクトがあると(`items: [o, o, …]`)、片方の行の下のキーへの書き込み(`this["items.0.name"] = "z"
|
|
1414
|
+
**同じオブジェクトが 2 つの位置にあるとき。** 1 つのリストの 2 つの位置に同じオブジェクトがあると(`items: [o, o, …]`)、片方の行の下のキーへの書き込み(`this["items.0.name"] = "z"`)は、そのオブジェクトを持つすべての行、その行 getter、`$getAll` に届きます(4.3 から)。配列はそうした書き込みのために 1 回だけ調べ、その要素がその場で書かれると調べ直します。普通のキーの 2 つの配列が同じオブジェクトを持つ場合(`backup = items; items = items.filter(…)` で両方を描画する形)は、こうは結び付きません: 片方の行の下への書き込みはもう片方の行に届きません(素の読み取り・ルートの getter・`$getAll` には新しい値が見えます)。こうした書き込みの後に、もう片方のリストの `$postUpdate`(`$postUpdate("backup")`)を呼ぶと、そのリストのすべての行がオブジェクトを表示し直します。外側の 2 つの行が、同じオブジェクトを持つ別々の配列を持つ場合(`groups: [{ items: a }, { items: [...a] }]`)も同じで、もう片方の一覧(`"groups.1.items"`)か外側の一覧(`"groups"`)の `$postUpdate` を呼んでください。これはこのバージョンの既知の制約です。getter が配列を結び付ける場合 —— TodoMVC 風の絞り込み(`todos` を絞り込んだコピーを返す `get shown()` を `for: shown` で描画する形)、並べ替え、切り出し —— は、どちら側から書いても、オブジェクトを持つすべての行と `todos.*` を読むものに届き、そのオブジェクトを配列に持つ getter は評価し直されます(チェックした行は未完了の絞り込みから抜け、名前を変えた行は並べ替えた表示で動きます)。書いたオブジェクトを配列に持たない getter は評価し直しません: 配列を読んだのであって、オブジェクトは読んでいないためです —— どの行にも追従させるには、その中で `$getAll("todos.*.done")` を読んでください。
|
|
1414
1415
|
|
|
1415
1416
|
## 再帰パス(`$recursion`)
|
|
1416
1417
|
|
|
@@ -1674,7 +1675,7 @@ export default {
|
|
|
1674
1675
|
|
|
1675
1676
|
## フィルタ
|
|
1676
1677
|
|
|
1677
|
-
|
|
1678
|
+
48 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます: core に条件の 10 個のフィルタ(`eq`・`ne`・`not`・`lt`・`le`・`gt`・`ge`・`truthy`・`falsy`・`boolean`)、`formats` アドオンに残りの 38 個(算術・数値フォーマット・文字列・型変換・日付 / 時刻・デフォルト)があり、全部入りのエントリには両方が含まれます。`formats` なしの `/core` で `formats` のフィルタを使うと、束縛計画の段でアドオン名を挙げて失敗します。
|
|
1678
1679
|
|
|
1679
1680
|
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
1681
|
|
|
@@ -1711,6 +1712,7 @@ export default {
|
|
|
1711
1712
|
| `floor(n?)` | 切り捨て | `value\|floor` |
|
|
1712
1713
|
| `ceil(n?)` | 切り上げ | `value\|ceil` |
|
|
1713
1714
|
| `locale(loc?)` | ロケール数値フォーマット | `count\|locale` / `count\|locale(ja-JP)` |
|
|
1715
|
+
| `currency(code, loc?)` | ISO 4217 の通貨で書く | `price\|currency(JPY)` / `price\|currency(USD,en-US)` → `"$1,234.50"` |
|
|
1714
1716
|
| `percent(n?)` | パーセンテージフォーマット | `ratio\|percent(1)` |
|
|
1715
1717
|
| `unit(u)` | 単位(任意の接尾辞)を付加 | `width\|unit(px)` → `"40px"` |
|
|
1716
1718
|
|
|
@@ -1745,13 +1747,13 @@ export default {
|
|
|
1745
1747
|
|
|
1746
1748
|
| フィルタ | 説明 | 例 |
|
|
1747
1749
|
|---|---|---|
|
|
1748
|
-
| `date(loc?)` | 日付フォーマット | `createdAt\|date` / `createdAt\|date(ja-JP)` |
|
|
1749
|
-
| `time(loc?)` | 時刻フォーマット | `createdAt\|time` |
|
|
1750
|
-
| `datetime(loc?)` | 日付 + 時刻 | `createdAt\|datetime(en-US)` |
|
|
1750
|
+
| `date(loc?, style?)` | 日付フォーマット | `createdAt\|date` / `createdAt\|date(ja-JP)` / `createdAt\|date(en-US,medium)` → `"Oct 13, 2026"` |
|
|
1751
|
+
| `time(loc?, style?)` | 時刻フォーマット | `createdAt\|time` / `createdAt\|time(short)` |
|
|
1752
|
+
| `datetime(loc?, style?)` | 日付 + 時刻 | `createdAt\|datetime(en-US)` / `createdAt\|datetime(ja-JP,long)` |
|
|
1751
1753
|
| `ymd(sep?)` | YYYY-MM-DD(既定は `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
|
|
1752
1754
|
| `hms(sep?)` | HH:MM:SS(既定は `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
|
|
1753
1755
|
|
|
1754
|
-
**5 つとも `Date` を取ります**(タイムスタンプではありません)。他の型は「requires a date value」で拒否されます。JSON から読み込んだ state は数値か ISO 文字列を持つので、束縛ではなく読み出し側で変換してください: `get createdAt() { return new Date(this.createdAtMs); }`。`loc?` の既定はページのロケール([ロケール](#ロケール))で、**適用のたびに**読み直されるため、束縛を組み立てた後に確定したロケールも反映されます(明示引数 `date(ja-JP)`
|
|
1756
|
+
**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
1757
|
|
|
1756
1758
|
### 真偽値 / デフォルト
|
|
1757
1759
|
|
|
@@ -2579,7 +2581,7 @@ $renderedCallback(paths) {
|
|
|
2579
2581
|
|
|
2580
2582
|
**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
2583
|
|
|
2582
|
-
**要素が既に持っていた値でも発火します。** watch は `$connectedCallback` の後に始まり、それより前の書き込みでは発火しません。しかし watch するパスに束ねた要素は、バインディングがつながった時点で値を既に持っていること(開いている `<wcs-sse>` の `connected`、router の最初の `typedParams`)も、少し後に報告することもあり、どちらになるかはページの読み込みの速さで変わります。そこで、watch が始まる前に要素が watch するパスへ書いた値は、始まったときにそのパスがまだその値なら、`prev` をシードにして 1
|
|
2584
|
+
**要素が既に持っていた値でも発火します。** 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
2585
|
|
|
2584
2586
|
発火順序は 3 層に分かれ、利用者が意思を持てるのは真ん中の層だけです。
|
|
2585
2587
|
|
|
@@ -2915,7 +2917,7 @@ li {
|
|
|
2915
2917
|
| 9 | | `$setAll("<path>") needs indexes ([] for every match)` |
|
|
2916
2918
|
| 10 | | `$setAll("<path>", …, { spread: true }) needs an array of <n> values` |
|
|
2917
2919
|
| 11 | | `updates did not settle after 32 passes` |
|
|
2918
|
-
| 12 | | `binding "<
|
|
2920
|
+
| 12 | | `binding "<member>: <path>" failed to apply.`(`<member>` は書いたとおりの名前 — `textContent`・`class.active` —、名前の無い `{{ }}` などは種類 `text`。console には要素も添える) |
|
|
2919
2921
|
| 13 | | `failed to load "<src>": <status>` |
|
|
2920
2922
|
| 14 | | `this <wcs-state> failed to initialize; create a new one` |
|
|
2921
2923
|
| 15 | | `state is not initialized` |
|
|
@@ -2931,8 +2933,8 @@ li {
|
|
|
2931
2933
|
| 25 | | `view-transition naming-limit (<limit>) reached.` |
|
|
2932
2934
|
| 26 | | `filter <name> requires at least one option` |
|
|
2933
2935
|
| 27 | | `filter <name> requires a number as option` |
|
|
2934
|
-
| 28 | | `filter <name> requires a number value` |
|
|
2935
|
-
| 29 | | `filter <name> requires a date value` |
|
|
2936
|
+
| 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`) |
|
|
2937
|
+
| 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
2938
|
| 30 | | `filter <name> requires an array value` |
|
|
2937
2939
|
| 31 | | `init=/sync= modifiers require enableDirectionalInitialSync.` |
|
|
2938
2940
|
| 32 | | `"<text>": the "@name" selector was removed in v2 — there is a single state tree. …` |
|
|
@@ -2955,6 +2957,8 @@ li {
|
|
|
2955
2957
|
| 49 | | `<wcs-state src="…"> failed to initialize.` |
|
|
2956
2958
|
| 50 | | `<wcs-state src="…"> $connectedCallback failed.` |
|
|
2957
2959
|
| 51 | | `<wcs-state src="…"> $disconnectedCallback failed.` |
|
|
2960
|
+
| 52 | | `filter <name> takes a style of full, long, medium or short, not "<style>"` |
|
|
2961
|
+
| 53 | | `filter currency takes an ISO 4217 currency code (JPY, USD, EUR), not "<code>"` |
|
|
2958
2962
|
| 101 | `binding-syntax` | `Invalid bindText: "<text>". Missing ':' separator between propPart and statePart.` |
|
|
2959
2963
|
| 102 | `binding-syntax` | `"<text>": "<keyword>" takes no modifiers or filters on its left side — write "<keyword>:".` |
|
|
2960
2964
|
| 103 | `binding-syntax` | `"<text>": "else" takes no value — write "else:".` |
|
|
@@ -3159,7 +3163,7 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
3159
3163
|
|
|
3160
3164
|
`@wcstack/state` は値を DOM に入れます。値をサニタイズはせず、値をマークアップとして読むこともありません。マークアップとして読むのはページそのものです —— そのため、規則は「何がページに届くか」と「何をどこにバインドするか」についてのものになります。
|
|
3161
3165
|
|
|
3162
|
-
**自分で書いていないマークアップをページ走査に届かせてはいけません。** `<wcs-state>` は自分のルートをバインドするとき、マークアップの中のすべての `{{ … }}`、すべての `data-wcs` 属性、すべてのコメントバインディング(`<!--@@: …-->`)を読みます([ページ走査が何をバインドするか](#ページ走査が何をバインドするか))。サーバーのテンプレートやページのコードが、state ではなくページのマークアップへ書き込んだユーザーの内容も同じように読まれます: `{{ apiKey }}` と入力されたユーザー名は state の `apiKey` を描画し、投稿されたメッセージの中の `data-wcs` 属性はバインドされます —— クライアントサイドテンプレートインジェクション(CSTI)です。コメントバインディングは `$behavior.enableMustache: false` でもバインドされるので、`{{ }}` を OFF にしても防げません。4.
|
|
3166
|
+
**自分で書いていないマークアップをページ走査に届かせてはいけません。** `<wcs-state>` は自分のルートをバインドするとき、マークアップの中のすべての `{{ … }}`、すべての `data-wcs` 属性、すべてのコメントバインディング(`<!--@@: …-->`)を読みます([ページ走査が何をバインドするか](#ページ走査が何をバインドするか))。サーバーのテンプレートやページのコードが、state ではなくページのマークアップへ書き込んだユーザーの内容も同じように読まれます: `{{ apiKey }}` と入力されたユーザー名は state の `apiKey` を描画し、投稿されたメッセージの中の `data-wcs` 属性はバインドされます —— クライアントサイドテンプレートインジェクション(CSTI)です。コメントバインディングは `$behavior.enableMustache: false` でもバインドされるので、`{{ }}` を OFF にしても防げません。4.3 からは、そうした内容を持つ要素に `data-wcs-ignore` を付けてください(`<div class="comment" data-wcs-ignore><%= comment %></div>`): 走査はその中を何も読みません([ページ走査が何をバインドするか](#ページ走査が何をバインドするか))。さらによいのは、ユーザーの内容をマークアップに入れないことです:
|
|
3163
3167
|
|
|
3164
3168
|
- ユーザーの内容は **state** に入れ、`textContent:`(または、テキストである `{{ }}` / コメントバインディング)でバインドしてください —— 値がマークアップとして読まれることはありません。
|
|
3165
3169
|
- ユーザーの HTML を `innerHTML:` / `html:` でバインドするのは、サニタイズした後だけにしてください。HTML のシンク(`innerHTML:`・`outerHTML:`・`srcdoc:`・`html:`)は、すでに `TrustedHTML` であるものを除くすべての値を、設置したポリシー(`setTrustedTypesPolicy()`、または `globalThis[Symbol.for("wcstack.trustedTypes.policy")]`)に通します。ブラウザに Trusted Types があってもなくても同じなので、設置したサニタイザは必ず走ります。このパッケージが identity ポリシーで値に署名することはありません([docs/csp.ja.md §7](../../docs/csp.ja.md))。
|
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.3.0/dist/split/core.js",
|
|
196
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.3.0/dist/split/features/temporal.js",
|
|
197
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.3.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.3.0/dist/split/auto.js"></script>
|
|
258
258
|
<wcs-state features="scopes diagnostics">
|
|
259
259
|
<script type="module">
|
|
260
260
|
export default {
|
|
@@ -766,6 +766,7 @@ When a `<wcs-state>` binds its root (the document, a shadow root, or a Light DOM
|
|
|
766
766
|
- **A value is never markup.** What a binding puts into the page — a `textContent:` or `innerHTML:` value, the children a custom element renders from a bound value — is never read as bindings: `{{ }}`, `data-wcs` and comment bindings in it stay text.
|
|
767
767
|
- The children of an element whose content a binding sets (`textContent:`, `text:`, `innerText:`, `innerHTML:`, `html:`) are not bound, at page level or inside templates — also when `#init=element` or `#init=none` leaves the children you wrote in place. Nor are the children of a page-level element bound with `outerHTML:` / `outerText:`. Put markup that needs bindings in a separate element.
|
|
768
768
|
- The content of `<script>`, `<style>`, `<noscript>` and `<iframe>` is not walked; the element's own `data-wcs` (`srcdoc:`, `attr.src:`) still binds.
|
|
769
|
+
- **An element marked `data-wcs-ignore` is not walked at all**, neither its own `data-wcs` nor anything in it: no `{{ }}`, `data-wcs` or comment binding there is read, at page level, inside templates, or in content the router hands over. It is the mark for markup the page did not write ([Security](#security)). The attribute is the bind attribute's name plus `-ignore`, so it follows `bindAttributeName`. Since 4.3.
|
|
769
770
|
- Markup written inside a `<wcs-state>` element is part of the page. A Light DOM component's content is bound by the component's own engine, not by the page.
|
|
770
771
|
- At page level an element's children are bound before the element's own bindings, so a custom element receives its first property writes after its children are bound, and `$errorCallback` receives failures children first. Inside templates the order is document order.
|
|
771
772
|
- `for:` rows and `if:` branches are clones of their templates without the `data-wcs` attribute, so a CSS rule or a test selector on `[data-wcs…]` does not match those elements — use a class or a `data-*` attribute of your own.
|
|
@@ -1221,7 +1222,7 @@ Three rules decide what the dependency graph sees. None of them matters until yo
|
|
|
1221
1222
|
|---|---|
|
|
1222
1223
|
| **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
1224
|
| **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) |
|
|
1225
|
+
| **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
1226
|
|
|
1226
1227
|
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
1228
|
|
|
@@ -1416,7 +1417,7 @@ export default {
|
|
|
1416
1417
|
};
|
|
1417
1418
|
```
|
|
1418
1419
|
|
|
1419
|
-
**One object at two positions.** When the same object sits at two positions of one list (`items: [o, o, …]`), a write to a key below one of those rows (`this["items.0.name"] = "z"`)
|
|
1420
|
+
**One object at two positions.** When the same object sits at two positions of one list (`items: [o, o, …]`), a write to a key below one of those rows (`this["items.0.name"] = "z"`) reaches every row holding it, their row getters and `$getAll` (since 4.3). The array is looked at once for such a write, and again after an element of it is written in place. Two arrays at plain keys that share an object (`backup = items; items = items.filter(…)`, both rendered) are not related that way: a write below a row of one does not reach the other's row, although plain reads, root getters and `$getAll` see the new value. Call `$postUpdate` on the other list (`$postUpdate("backup")`) after such a write, and every row of that list shows the object again. So do two outer rows holding different arrays that share an object (`groups: [{ items: a }, { items: [...a] }]`): call `$postUpdate` on the other list (`"groups.1.items"`) or on the outer one (`"groups"`). This is a known limitation of this version. When a getter relates the arrays — a TodoMVC-style filter (`get shown()` returning a filtered copy of `todos`, rendered with `for: shown`), a sort, a slice — a write from either side reaches every row holding the object and the `todos.*` readers, and the getters whose array holds it are evaluated again (a checked row leaves an "active" filter, a renamed row moves in a sorted view). A getter whose array does not hold the written object is not evaluated again: it read the array, not the object — read `$getAll("todos.*.done")` in it to follow every row.
|
|
1420
1421
|
|
|
1421
1422
|
## Recursive Paths (`$recursion`)
|
|
1422
1423
|
|
|
@@ -1680,7 +1681,7 @@ Handlers run where the DOM puts them. An outer `#direct` handler runs **before**
|
|
|
1680
1681
|
|
|
1681
1682
|
## Filters
|
|
1682
1683
|
|
|
1683
|
-
|
|
1684
|
+
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
1685
|
|
|
1685
1686
|
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
1687
|
|
|
@@ -1717,6 +1718,7 @@ The names 3.x kept as aliases of the 3.2 renames (`inc`, `dec`, `fix`, `uc`, `lc
|
|
|
1717
1718
|
| `floor(n?)` | Floor | `value\|floor` |
|
|
1718
1719
|
| `ceil(n?)` | Ceiling | `value\|ceil` |
|
|
1719
1720
|
| `locale(loc?)` | Locale number format | `count\|locale` / `count\|locale(ja-JP)` |
|
|
1721
|
+
| `currency(code, loc?)` | Money in an ISO 4217 currency | `price\|currency(JPY)` / `price\|currency(USD,en-US)` → `"$1,234.50"` |
|
|
1720
1722
|
| `percent(n?)` | Percentage format | `ratio\|percent(1)` |
|
|
1721
1723
|
| `unit(u)` | Append a unit (any suffix) | `width\|unit(px)` → `"40px"` |
|
|
1722
1724
|
|
|
@@ -1751,13 +1753,13 @@ The names 3.x kept as aliases of the 3.2 renames (`inc`, `dec`, `fix`, `uc`, `lc
|
|
|
1751
1753
|
|
|
1752
1754
|
| Filter | Description | Example |
|
|
1753
1755
|
|---|---|---|
|
|
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)` |
|
|
1756
|
+
| `date(loc?, style?)` | Date format | `createdAt\|date` / `createdAt\|date(ja-JP)` / `createdAt\|date(en-US,medium)` → `"Oct 13, 2026"` |
|
|
1757
|
+
| `time(loc?, style?)` | Time format | `createdAt\|time` / `createdAt\|time(short)` |
|
|
1758
|
+
| `datetime(loc?, style?)` | Date + Time | `createdAt\|datetime(en-US)` / `createdAt\|datetime(ja-JP,long)` |
|
|
1757
1759
|
| `ymd(sep?)` | YYYY-MM-DD (default `-`) | `createdAt\|ymd` / `createdAt\|ymd(/)` |
|
|
1758
1760
|
| `hms(sep?)` | HH:MM:SS (default `:`) | `createdAt\|hms` / `createdAt\|hms(-)` |
|
|
1759
1761
|
|
|
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).
|
|
1762
|
+
**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
1763
|
|
|
1762
1764
|
### Boolean / Default
|
|
1763
1765
|
|
|
@@ -2589,7 +2591,7 @@ The handler runs with `this` bound to a **writable** state proxy, so it can writ
|
|
|
2589
2591
|
|
|
2590
2592
|
**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
2593
|
|
|
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.
|
|
2594
|
+
**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
2595
|
|
|
2594
2596
|
Firing order is defined in three layers, and only the middle one is yours to steer:
|
|
2595
2597
|
|
|
@@ -2925,7 +2927,7 @@ The first line is what `@wcstack/state` and `/auto` print, the second what `/cor
|
|
|
2925
2927
|
| 9 | | `$setAll("<path>") needs indexes ([] for every match)` |
|
|
2926
2928
|
| 10 | | `$setAll("<path>", …, { spread: true }) needs an array of <n> values` |
|
|
2927
2929
|
| 11 | | `updates did not settle after 32 passes` |
|
|
2928
|
-
| 12 | | `binding "<
|
|
2930
|
+
| 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
2931
|
| 13 | | `failed to load "<src>": <status>` |
|
|
2930
2932
|
| 14 | | `this <wcs-state> failed to initialize; create a new one` |
|
|
2931
2933
|
| 15 | | `state is not initialized` |
|
|
@@ -2941,8 +2943,8 @@ The first line is what `@wcstack/state` and `/auto` print, the second what `/cor
|
|
|
2941
2943
|
| 25 | | `view-transition naming-limit (<limit>) reached.` |
|
|
2942
2944
|
| 26 | | `filter <name> requires at least one option` |
|
|
2943
2945
|
| 27 | | `filter <name> requires a number as option` |
|
|
2944
|
-
| 28 | | `filter <name> requires a number value` |
|
|
2945
|
-
| 29 | | `filter <name> requires a date value` |
|
|
2946
|
+
| 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`) |
|
|
2947
|
+
| 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
2948
|
| 30 | | `filter <name> requires an array value` |
|
|
2947
2949
|
| 31 | | `init=/sync= modifiers require enableDirectionalInitialSync.` |
|
|
2948
2950
|
| 32 | | `"<text>": the "@name" selector was removed in v2 — there is a single state tree. …` |
|
|
@@ -2965,6 +2967,8 @@ The first line is what `@wcstack/state` and `/auto` print, the second what `/cor
|
|
|
2965
2967
|
| 49 | | `<wcs-state src="…"> failed to initialize.` |
|
|
2966
2968
|
| 50 | | `<wcs-state src="…"> $connectedCallback failed.` |
|
|
2967
2969
|
| 51 | | `<wcs-state src="…"> $disconnectedCallback failed.` |
|
|
2970
|
+
| 52 | | `filter <name> takes a style of full, long, medium or short, not "<style>"` |
|
|
2971
|
+
| 53 | | `filter currency takes an ISO 4217 currency code (JPY, USD, EUR), not "<code>"` |
|
|
2968
2972
|
| 101 | `binding-syntax` | `Invalid bindText: "<text>". Missing ':' separator between propPart and statePart.` |
|
|
2969
2973
|
| 102 | `binding-syntax` | `"<text>": "<keyword>" takes no modifiers or filters on its left side — write "<keyword>:".` |
|
|
2970
2974
|
| 103 | `binding-syntax` | `"<text>": "else" takes no value — write "else:".` |
|
|
@@ -3169,7 +3173,7 @@ In short:
|
|
|
3169
3173
|
|
|
3170
3174
|
`@wcstack/state` puts values into the DOM; it does not sanitize them, and it never reads a value as markup. What it does read as markup is the page itself — so the rules are about what reaches the page, and what you bind where.
|
|
3171
3175
|
|
|
3172
|
-
**Markup you did not write must not reach the page walk.** When a `<wcs-state>` binds its root, it reads every `{{ … }}`, every `data-wcs` attribute and every comment binding (`<!--@@: …-->`) in the markup ([What the page walk binds](#what-the-page-walk-binds)). User content that a server template or your page code writes into the page's markup, rather than into state, is read the same way: a user name typed as `{{ apiKey }}` renders the state's `apiKey`, and a `data-wcs` attribute in a posted message binds — client-side template injection (CSTI). Comment bindings are bound even with `$behavior.enableMustache: false`, so turning `{{ }}` off does not protect against it.
|
|
3176
|
+
**Markup you did not write must not reach the page walk.** When a `<wcs-state>` binds its root, it reads every `{{ … }}`, every `data-wcs` attribute and every comment binding (`<!--@@: …-->`) in the markup ([What the page walk binds](#what-the-page-walk-binds)). User content that a server template or your page code writes into the page's markup, rather than into state, is read the same way: a user name typed as `{{ apiKey }}` renders the state's `apiKey`, and a `data-wcs` attribute in a posted message binds — client-side template injection (CSTI). Comment bindings are bound even with `$behavior.enableMustache: false`, so turning `{{ }}` off does not protect against it. Since 4.3, mark the element that holds such content with `data-wcs-ignore` (`<div class="comment" data-wcs-ignore><%= comment %></div>`): the walk reads nothing in it ([What the page walk binds](#what-the-page-walk-binds)). Better still, keep user content out of the markup:
|
|
3173
3177
|
|
|
3174
3178
|
- Put user content in **state**, and bind it with `textContent:` (or `{{ }}` / a comment binding, which are text) — a value is never read as markup.
|
|
3175
3179
|
- Bind user HTML with `innerHTML:` / `html:` only after sanitizing it. The HTML sinks (`innerHTML:`, `outerHTML:`, `srcdoc:`, `html:`) pass every value that is not already a `TrustedHTML` through the policy you install (`setTrustedTypesPolicy()`, or `globalThis[Symbol.for("wcstack.trustedTypes.policy")]`), with or without Trusted Types in the browser — so an installed sanitizer always runs. This package never signs a value with an identity policy ([docs/csp.md §7](../../docs/csp.md)).
|