@wcstack/state 4.2.0 → 4.4.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 -10
- package/README.md +20 -9
- 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 +5 -4
- package/dist/split/auto.js +1 -1
- package/dist/split/chunks/chunk-467LYA2V.js +2 -0
- package/dist/split/chunks/chunk-467LYA2V.js.map +1 -0
- package/dist/split/chunks/{chunk-UPSLR4FS.js → chunk-CBMAVRYG.js} +2 -2
- package/dist/split/chunks/chunk-DUUQZITD.js +2 -0
- package/dist/split/chunks/{chunk-OKBVNGFK.js.map → chunk-DUUQZITD.js.map} +1 -1
- package/dist/split/chunks/{chunk-2RBI6DVS.js → chunk-FBBHQU7S.js} +2 -2
- package/dist/split/chunks/chunk-GBBBRPZR.js +2 -0
- package/dist/split/chunks/{chunk-BYQBV3ZC.js.map → chunk-GBBBRPZR.js.map} +1 -1
- package/dist/split/chunks/chunk-JWRQLKOQ.js +2 -0
- package/dist/split/chunks/chunk-NITPUCPA.js +2 -0
- package/dist/split/chunks/{chunk-CIBJLAA3.js → chunk-T3FN37DU.js} +2 -2
- package/dist/split/chunks/{chunk-BGUJ3HF6.js → chunk-TNYNJXEC.js} +2 -2
- package/dist/split/chunks/chunk-TNYNJXEC.js.map +1 -0
- package/dist/split/chunks/chunk-ULMHOGJ2.js +2 -0
- package/dist/split/chunks/chunk-ULMHOGJ2.js.map +1 -0
- package/dist/split/chunks/chunk-ZJYPRSTO.js +2 -0
- package/dist/split/chunks/chunk-ZJYPRSTO.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.js +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/scopes.js.map +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/wcs-manifest.json +4 -8
- package/package.json +1 -1
- package/dist/split/chunks/chunk-3Q7LGONS.js +0 -2
- package/dist/split/chunks/chunk-3Q7LGONS.js.map +0 -1
- package/dist/split/chunks/chunk-4ECL4ITF.js +0 -2
- package/dist/split/chunks/chunk-4ECL4ITF.js.map +0 -1
- package/dist/split/chunks/chunk-BGUJ3HF6.js.map +0 -1
- package/dist/split/chunks/chunk-BI4645UG.js +0 -2
- package/dist/split/chunks/chunk-BI4645UG.js.map +0 -1
- package/dist/split/chunks/chunk-BYQBV3ZC.js +0 -2
- package/dist/split/chunks/chunk-JK3B3LLB.js +0 -2
- package/dist/split/chunks/chunk-OKBVNGFK.js +0 -2
- package/dist/split/chunks/chunk-UBGJGMSG.js +0 -2
- /package/dist/split/chunks/{chunk-UPSLR4FS.js.map → chunk-CBMAVRYG.js.map} +0 -0
- /package/dist/split/chunks/{chunk-2RBI6DVS.js.map → chunk-FBBHQU7S.js.map} +0 -0
- /package/dist/split/chunks/{chunk-UBGJGMSG.js.map → chunk-JWRQLKOQ.js.map} +0 -0
- /package/dist/split/chunks/{chunk-JK3B3LLB.js.map → chunk-NITPUCPA.js.map} +0 -0
- /package/dist/split/chunks/{chunk-CIBJLAA3.js.map → chunk-T3FN37DU.js.map} +0 -0
package/README.ja.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
> **この README は 4.0 を説明しています。** 3.x から移行する場合は、[3.x からのアップグレード](#3x-からのアップグレード) と [3.x → 4.0 移行ガイド](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.ja.md) を読んでください。
|
|
6
6
|
|
|
7
|
+
> **どこまで読めば足りるか。** 入力と、そこから計算した値でできたアプリ —— たいていのアプリ —— に要るのは 5 つの節です: [基本的な使い方](#基本的な使い方)、[状態の更新](#状態の更新)(パスへ代入する。配列はその場で変えず、新しい配列を代入する)、[バインディング構文](#バインディング構文)、[構造ディレクティブ](#構造ディレクティブ)、[パス getter](#パス-getter算出プロパティ)(ワイルドカード getter と `$getAll`)。カスタム要素や I/O ノード(`<wcs-fetch>`・`<wcs-storage>`)を配線するページでは [Web Component バインディング](#web-component-バインディング) と [バインディング authority](#バインディング-authority-init--sync) を、行が新しいオブジェクトとして戻ってくるとき(取り直し、ストレージからの復元)は [`$listKeys`](#listkeys--再取得された行の同一性) を足してください。テキストの `{{ }}` と `<!--@@: path -->` は同じバインドです: `<template>` の外ではコメントの形(state が読み込まれる前に何も見せない)、中ではどちらでもかまいません。残りはリファレンスです: [機能の表](#この原理から導かれる機能)が問いごとに節を示し、各節が注意する誤りのほとんどは `npx @wcstack/lint` が報告します。
|
|
8
|
+
|
|
7
9
|
**これは便利な既存FWの別実装ではありません。パスがビューとモデルの契約になる——フロントエンドの外で確立した系譜を、Web標準の上に持ち込む試みです。**
|
|
8
10
|
|
|
9
11
|
多くのライブラリは、UI・状態・コンポーネントの結合点を JavaScript の中に置きます。`@wcstack/state` はそこを選びません。仮想DOMも、コンパイルも、hook も、selector も前提にせず、HTML とパス文字列だけを契約として UI と状態を結びつけます。
|
|
@@ -191,9 +193,9 @@
|
|
|
191
193
|
<script type="importmap">
|
|
192
194
|
{
|
|
193
195
|
"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.
|
|
196
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/core.js",
|
|
197
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/features/temporal.js",
|
|
198
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/features/scopes.js"
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
</script>
|
|
@@ -253,7 +255,7 @@
|
|
|
253
255
|
ページが名指したアドオンを、同じビルドから読み込みます。
|
|
254
256
|
|
|
255
257
|
```html
|
|
256
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
258
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/auto.js"></script>
|
|
257
259
|
<wcs-state features="scopes diagnostics">
|
|
258
260
|
<script type="module">
|
|
259
261
|
export default {
|
|
@@ -601,6 +603,8 @@ class YenInput extends HTMLElement {
|
|
|
601
603
|
|
|
602
604
|
どちらを選んでも、`element.value` とイベントから取り出す値は同じ論理状態を表していなければなりません(プロトコルの *Producer State Consistency Invariant*)。初期同期はプロパティを読み、以後の更新はイベントを読むからです。wcstack 内部でも両形が使われています —— `<wcs-fetch>` の `loading` は `getter` なしで真偽値を `detail` に載せ、`value` は `getter` で `detail.value` を読みます。DCC の `$bindables` が `getter: (e) => e.target[name]` を宣言するのは、サブパス書き込みには `detail` に載せる単一の値が無いからです。既定そのものを変える予定はありません。`e.detail` はすべての wc-bindable アダプタに対する規範(`@wc-bindable/core` の `bind()` とフレームワークアダプタも同じ既定)で、プロトコル上、既定の変更は新しいプロトコル識別子を要する破壊的変更に分類されています。
|
|
603
605
|
|
|
606
|
+
**state 自身の書き込みが返ってくるとき。** state がメンバーに書き(`element.value = v`)、要素がその setter の中から同期的に、同じ値(`Object.is`)でそのメンバーのイベントを dispatch したとき、そのイベントは state 自身の書き込みが返ってきたもの(書き戻し)です。state へは書かれず、そこに束ねたアクセサペアの setter も走りません。これはバインディングの一部で、`$behavior` のどのキーでも変わりません。後から dispatch したイベント(`await` の後、タイマーから)や、別のオブジェクト(コピー、JSON を読み直したもの)を運ぶイベントは要素自身の書き込みです。state へ届き、アクセサペアの setter が走ります。そうした setter は、state がすでに持っている値を受け取っても何も変わらないように書いてください。`<wcs-storage>` と getter / setter の組でフォーム全体を保存する形は、この規則の上に立っています: [フォームを 1 つのオブジェクトとして永続化する](../storage/README.ja.md#2-フォームを-1-つのオブジェクトとして永続化する)。
|
|
607
|
+
|
|
604
608
|
authority はバインディング単位で `#init=` により上書きできます:
|
|
605
609
|
|
|
606
610
|
| 値 | 初期同期 | 使える宣言 |
|
|
@@ -760,6 +764,7 @@ export default {
|
|
|
760
764
|
- **値は決してマークアップにならない。** バインディングがページに入れるもの —— `textContent:` や `innerHTML:` の値、カスタム要素がバインドされた値から描画する子 —— がバインディングとして読まれることはありません: その中の `{{ }}`・`data-wcs`・コメントバインディングは、ただのテキストのまま残ります。
|
|
761
765
|
- バインディングが中身を設定する要素(`textContent:`・`text:`・`innerText:`・`innerHTML:`・`html:`)の子はバインドされません。ページレベルでもテンプレートの中でも同じで、`#init=element` や `#init=none` によって書いた子がそのまま残る場合も同じです。`outerHTML:` / `outerText:` をバインドしたページレベルの要素の子もバインドされません。バインディングが必要なマークアップは別の要素に置いてください。
|
|
762
766
|
- `<script>`・`<style>`・`<noscript>`・`<iframe>` の中身は走査しません。その要素自身の `data-wcs`(`srcdoc:`、`attr.src:`)はバインドされます。
|
|
767
|
+
- **`data-wcs-ignore` の付いた要素はまったく走査しません。** その要素自身の `data-wcs` も中身も — そこにある `{{ }}`・`data-wcs`・コメントバインディングは、ページレベルでも、テンプレートの中でも、router が渡す中身でも読みません。ページが書いていないマークアップのための印です([セキュリティ](#セキュリティ))。属性名はバインド属性の名前に `-ignore` を付けたものなので、`bindAttributeName` に従います。4.3 から。
|
|
763
768
|
- `<wcs-state>` 要素の内側に書いたマークアップはページの一部です。Light DOM コンポーネントの中身は、ページではなくそのコンポーネント自身のエンジンがバインドします。
|
|
764
769
|
- ページレベルでは、要素の子が要素自身のバインディングより先にバインドされます。そのため、カスタム要素が最初のプロパティ書き込みを受け取るのは子がバインドされた後で、`$errorCallback` も子の失敗から先に受け取ります。テンプレートの中では文書順です。
|
|
765
770
|
- `for:` の行と `if:` の枝は、`data-wcs` 属性を除いたテンプレートの複製です。そのため `[data-wcs…]` に対する CSS ルールやテストのセレクタはそれらの要素にマッチしません —— クラスや自前の `data-*` 属性を使ってください。
|
|
@@ -1300,7 +1305,7 @@ export default {
|
|
|
1300
1305
|
| `this.$getAll(path, indexes?)` | ワイルドカードパスにマッチする全ての値を取得 |
|
|
1301
1306
|
| `this.$setAll(path, indexes, value, options?)` | ワイルドカードパスにマッチする全アドレスへ一括書き込み |
|
|
1302
1307
|
| `this.$resolve(path, indexes, value?)` | ワイルドカードパスを特定のインデックスで解決 |
|
|
1303
|
-
| `this.$postUpdate(path)` |
|
|
1308
|
+
| `this.$postUpdate(path)` | 指定パスの更新通知を手動で発行。中の `*` は getter と同じく今の行の添字で、行のイベントのハンドラから呼ぶと `$postUpdate("rows.*.cells.*.text")` はその行のセルだけを通知する。複数の行を通知するには、それぞれを名指す(`` this.$postUpdate(`rows.${r}.cells.${c}.text`) ``) |
|
|
1304
1309
|
| `this.$dependOn(path)` | キャッシュ無効化のための依存関係を手動で登録 |
|
|
1305
1310
|
| `this.$untracked(fn)` | fn 実行中の依存追跡を抑止して値を読む |
|
|
1306
1311
|
| `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | 鍵付き購読: 「`path` の値はこの行の鍵か」([鍵付き選択](#鍵付き選択eq--eqpath--eqindex)) |
|
|
@@ -1410,7 +1415,7 @@ export default {
|
|
|
1410
1415
|
};
|
|
1411
1416
|
```
|
|
1412
1417
|
|
|
1413
|
-
**同じオブジェクトが 2 つの位置にあるとき。** 1 つのリストの 2 つの位置に同じオブジェクトがあると(`items: [o, o, …]`)、片方の行の下のキーへの書き込み(`this["items.0.name"] = "z"
|
|
1418
|
+
**同じオブジェクトが 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
1419
|
|
|
1415
1420
|
## 再帰パス(`$recursion`)
|
|
1416
1421
|
|
|
@@ -1653,7 +1658,7 @@ export default {
|
|
|
1653
1658
|
|
|
1654
1659
|
`click`・`dblclick`・`input`・`change`・`submit`・`keydown`・`keyup`・`mousedown`・`mouseup`・`pointerdown`・`pointerup` の `on*:` バインディングは**委譲**されます: イベント種別ごとに 1 つのリスナーが、state がバインドするルート —— document、shadow root、あるいは Light DOM のマウントされたコンポーネントの中ではホスト要素 —— に置かれ、イベントが通過した要素のハンドラを内側から順に実行します。そのため:
|
|
1655
1660
|
|
|
1656
|
-
- ハンドラの中の `event.currentTarget` は要素ではなく、そのルートです。要素は `event.target` から探すか(`event.target.closest("li")`)、ハンドラが受け取るループインデックスを使ってください(`removeItem(event, index)`)。
|
|
1661
|
+
- ハンドラの中の `event.currentTarget` は要素ではなく、そのルートです。要素は `event.target` から探すか(`event.target.closest("li")`)、ハンドラが受け取るループインデックスを使ってください(`removeItem(event, index)`)。4.4 から、ソースが `currentTarget` を読む委譲されたハンドラが throw すると(`getBoundingClientRect is not a function`)、`diagnostics` アドオンがエラーの後に、ハンドラごとに 1 回その理由を言います(`[wcs/delegated-current-target]`、lint と同じ code)。
|
|
1657
1662
|
- 内側の `on*:` バインディングの `#stop` は、それより外側の `on*:` ハンドラを止めます。自分のコードが祖先に付けたリスナー(クリックできるカードへの `addEventListener`)は止められません: そのリスナーは、イベントがルートに届く前に実行済みです。
|
|
1658
1663
|
- 自分のコードが祖先で `stopPropagation()` を呼ぶと(モーダルの中身がオーバーレイにクリックを渡さないようにする場合など)、その内側の委譲されたハンドラはまったく実行されません。
|
|
1659
1664
|
- 別のルートの下へ移した要素(shadow root から `document.body` へ移したダイアログ)では、委譲されたハンドラはもう実行されません。
|
|
@@ -2098,6 +2103,7 @@ export default {
|
|
|
2098
2103
|
- 予約名 `$command` 自体は配列に含められない
|
|
2099
2104
|
- token は `$command` 配下にまとめられるためトップレベルの state 名前空間を汚さない。token と同名のリアクティブプロパティが共存できる
|
|
2100
2105
|
- `$command` 上の未宣言の名前にアクセスする(例: `this.$command.typo`)と `undefined` が返る。typo はその後の `.emit()` 呼び出しで `TypeError` として、あるいはバインディングの右辺で使った場合は、バインディングの取り付け時に `[wcs/token-undeclared] "$command.typo" is not declared in $commandTokens.`(`#1302`、did-you-mean 付き)として表面化する
|
|
2106
|
+
- トークンは何も取っておかない: どの `command.` 束縛も購読していない間の emit は消える。4.4 から、`diagnostics` アドオンがトークンごとに 1 回警告する(`[wcs/command-token-unbound]`)。束縛がまだ付いていない — 要素の定義が遅れる(autoloader、遅い CDN)、または同じ更新で描かれる `if:` の枝の中にある(`$renderedCallback` から emit する) — か、束縛が無い(その場合は lint も報告する)
|
|
2101
2107
|
|
|
2102
2108
|
### `command.<methodName>:` バインディング
|
|
2103
2109
|
|
|
@@ -2639,6 +2645,7 @@ class MyChip extends HTMLElement {
|
|
|
2639
2645
|
|
|
2640
2646
|
- 3.x はプロパティの後に属性にも書いていました(`String(value)`、オブジェクトは JSON)。setter で反映する要素は影響を受けません。setter を持たず、binder が属性を書くことに頼っていた要素には、setter が要ります(移行ガイド §3.4)。
|
|
2641
2647
|
- ネイティブ HTML 要素は `inputs` を完全に無視します —— 宣言が効くのは `static wcBindable` を公開するカスタム要素だけです。
|
|
2648
|
+
- `for:` の行と `if:` の枝は、挿入する前にバインドします。その中のカスタム要素は、`connectedCallback` が走る前に入力(`element.value = …`)を受け取ります。ページの直下では、要素は先に接続されています。既定値から描く `connectedCallback` は、setter が受け取ったものを上書きします —— 今のプロパティから描いてください(外部の評価では、金額欄が、まだ採用していない入力をこれで失いました。ページを 3.x から 4.0 に上げたときです)。もう 1 つの早い書き込み、要素が定義される前に代入されたプロパティは、[I/O ノードのガイドライン](../../docs/async-io-node-guidelines.ja.md)の `upgradeProperties` の手順が扱います。
|
|
2642
2649
|
|
|
2643
2650
|
## コンポーネント機構の選び方
|
|
2644
2651
|
|
|
@@ -3025,11 +3032,14 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
3025
3032
|
| ネストしたパスの打ち間違い(`user.nmae`) | `console.warn`(`wcs/binding-path-missing`)。更新は届かないままなので、直すのは書き手 |
|
|
3026
3033
|
| トップレベルのパスの打ち間違い(`cout`) | 読み取り時に throw —— `[wcs/binding-path-missing] Path "cout" does not exist on the state tree. Did you mean "count"?`(`#301`)—— そのためバインディングの適用が失敗し、ほかのバインディングの失敗と同じく報告される |
|
|
3027
3034
|
| `$watch` のキーの打ち間違い | `console.warn`(`wcs/watch-path-missing`)。単一セグメントでも報告する |
|
|
3035
|
+
| `$postUpdate` のパスの打ち間違い(`cells` に対する `` `rows.${r}.cell.${c}.text` ``) | 4.4 から: `console.warn`(`wcs/post-update-path-missing`)。単一セグメントでも報告する — その呼び出しは何も更新しない |
|
|
3036
|
+
| state が宣言していないトップレベルのキーへの書き込みで、近い宣言済みのキーがあるもの(`this.selectedID` と `selectedId`) | 4.4 から: キーごとに 1 回 `console.warn`(`wcs/write-path-missing`) — 書き込みは新しいキーを作るだけで、宣言したキーに束ねたものは変わらない。近い名前の無い新しいキーは、state がわざと足すことがあるので報告しない |
|
|
3037
|
+
| getter が読むパスの打ち間違い(`this["items.*.pirce"]`、`this.$getAll("items.*.pirce", [])`) | 4.4 から: パスごとに 1 回、読んだ getter の名前を添えて `console.warn`(`wcs/read-path-missing`)。近い名前があるときだけ — 読みは `undefined` になり、getter がそこから計算する値は違う。getter がそのパスを読んだあとで調べるので、後から足した行は描かれたときに調べる。トップレベルのキー(`this.totl`)は読んだ時点で throw する(`#301`)。`@wcstack/lint` は getter とメソッドの中の同じ読みを、実行せずに検査する |
|
|
3028
3038
|
|
|
3029
3039
|
判定は**過小近似**です。静的に決められない形では黙ります —— 誤検知でページを騒がせないことを優先しているためで、以下はすべて警告しません:
|
|
3030
3040
|
|
|
3031
3041
|
- 親が `null` / `undefined`(初期値 `null` に後から代入する形)
|
|
3032
|
-
-
|
|
3042
|
+
- どの行にも無い行フィールド —— ただし、行に近い名前がある(打ち間違いらしい。`name` の横の `.nmae`)なら警告します。行はデータで、任意のフィールド(ページが自分の行に付ける `pending` など)はほかの行にはありません。4.4 から。ほかの行にあるフィールドが最初の行に無いだけの場合も警告しません(4.3 から)
|
|
3033
3043
|
- 途中の getter の戻り値のサブプロパティ
|
|
3034
3044
|
- まだ読み込み中のボリュームのマウントパス配下のパス —— ボリュームが決着した時点で検査される
|
|
3035
3045
|
- `$` 始まりの予約名前空間(`$command.*` など)
|
|
@@ -3162,7 +3172,7 @@ this.$getAll("matrix.*.*", [row]);
|
|
|
3162
3172
|
|
|
3163
3173
|
`@wcstack/state` は値を DOM に入れます。値をサニタイズはせず、値をマークアップとして読むこともありません。マークアップとして読むのはページそのものです —— そのため、規則は「何がページに届くか」と「何をどこにバインドするか」についてのものになります。
|
|
3164
3174
|
|
|
3165
|
-
**自分で書いていないマークアップをページ走査に届かせてはいけません。** `<wcs-state>` は自分のルートをバインドするとき、マークアップの中のすべての `{{ … }}`、すべての `data-wcs` 属性、すべてのコメントバインディング(`<!--@@: …-->`)を読みます([ページ走査が何をバインドするか](#ページ走査が何をバインドするか))。サーバーのテンプレートやページのコードが、state ではなくページのマークアップへ書き込んだユーザーの内容も同じように読まれます: `{{ apiKey }}` と入力されたユーザー名は state の `apiKey` を描画し、投稿されたメッセージの中の `data-wcs` 属性はバインドされます —— クライアントサイドテンプレートインジェクション(CSTI)です。コメントバインディングは `$behavior.enableMustache: false` でもバインドされるので、`{{ }}` を OFF にしても防げません。4.
|
|
3175
|
+
**自分で書いていないマークアップをページ走査に届かせてはいけません。** `<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>`): 走査はその中を何も読みません([ページ走査が何をバインドするか](#ページ走査が何をバインドするか))。さらによいのは、ユーザーの内容をマークアップに入れないことです:
|
|
3166
3176
|
|
|
3167
3177
|
- ユーザーの内容は **state** に入れ、`textContent:`(または、テキストである `{{ }}` / コメントバインディング)でバインドしてください —— 値がマークアップとして読まれることはありません。
|
|
3168
3178
|
- ユーザーの 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))。
|
|
@@ -3587,7 +3597,7 @@ const html = await renderToString(template, {
|
|
|
3587
3597
|
|---------|------|
|
|
3588
3598
|
| **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用して、`<wcs-state>` の前に `<wcs-ssr>` 要素を置いたレンダリング済み HTML を出力。行・枝・テキストバインディングにはコメントで印を付ける。`$watch` のハンドラは実行されず、stream は `initial` の値のまま |
|
|
3589
3599
|
| **クライアント** | `<wcs-state enable-ssr>` は自分のソースから state を読み込み、その上にスナップショットのデータを被せ(getter・setter・メソッドは残る)、`$connectedCallback` をスキップし、サーバーの DOM をその場でバインドする。行と枝はその場で引き継がれ —— その中のカスタム要素は 1 回だけ接続され、切断・再接続されることはない —— バインディングを得る。テンプレートに合わなくなったサーバーの行はその場所で描画し直され、クライアントが描画しない行や枝(行が少ない、別の枝)はページがバインドされた時点で取り除かれる。ボリュームはマウントパスの位置でサーバーのデータを引き継ぐ |
|
|
3590
|
-
| **バージョン不一致** | `<wcs-ssr version>` の major.minor が違うスナップショットは `console.warn` を 1 回出して破棄され、ページはサーバーが無かったかのようにクライアントで描画される: state は自分のソースから読み込まれ、`$connectedCallback`
|
|
3600
|
+
| **バージョン不一致** | `<wcs-ssr version>` の major.minor が違うスナップショットは `console.warn` を 1 回出して破棄され、ページはサーバーが無かったかのようにクライアントで描画される: state は自分のソースから読み込まれ、`$connectedCallback` が走り、サーバーの行・枝・マーカーは、描画元のテンプレートに場所を譲る。4.4 から、場所を譲るのはページを計画できた後: このクライアントが計画できないマークアップ(新しい minor のフィルタ引数など)は、サーバーの HTML をそのまま残して初期化に失敗する |
|
|
3591
3601
|
|
|
3592
3602
|
### `enable-ssr` の動作
|
|
3593
3603
|
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
> **This README describes 4.0.** Coming from 3.x? Read [Upgrading from 3.x](#upgrading-from-3x) and the [3.x → 4.0 migration guide](https://github.com/wcstack/wcstack/blob/main/docs/migration-v4.md).
|
|
6
6
|
|
|
7
|
+
> **How much to read.** An app of inputs and the values computed from them — most apps — needs five sections: [Basic usage](#basic-usage), [Updating state](#updating-state) (assign to the path; a new array, not an array changed in place), [Binding syntax](#binding-syntax), [Structural directives](#structural-directives) and [Path getters](#path-getters-computed-properties) (wildcard getters and `$getAll`). Add [Web Component binding](#web-component-binding) and [Binding authority](#binding-authority-init--sync) when the page wires custom elements or I/O nodes (`<wcs-fetch>`, `<wcs-storage>`), and [`$listKeys`](#listkeys--identity-for-refetched-rows) when rows come back as new objects (a refetch, a restore from storage). For text, `{{ }}` and `<!--@@: path -->` are the same binding: write the comment form outside a `<template>` — it shows nothing before the state loads — and either inside. The rest is reference: the [features table](#features-derived-from-this-principle) names the section for each question, and `npx @wcstack/lint` reports most of the mistakes the sections warn about.
|
|
8
|
+
|
|
7
9
|
**This is not another convenient frontend framework. It brings a lineage established outside frontend development — the one where a path string is the contract between view and model — onto web standards.**
|
|
8
10
|
|
|
9
11
|
Most libraries place the coupling point between UI, state, and components inside JavaScript. `@wcstack/state` does not. It assumes no virtual DOM, no compilation step, no hooks, no selectors. UI and state are connected by HTML and path strings alone.
|
|
@@ -192,9 +194,9 @@ leaves features out, it can compose them instead:
|
|
|
192
194
|
<script type="importmap">
|
|
193
195
|
{
|
|
194
196
|
"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.
|
|
197
|
+
"@wcstack/state/core": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/core.js",
|
|
198
|
+
"@wcstack/state/features/temporal": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/features/temporal.js",
|
|
199
|
+
"@wcstack/state/features/scopes": "https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/features/scopes.js"
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
</script>
|
|
@@ -254,7 +256,7 @@ receptacle the owning add-on fills on install).
|
|
|
254
256
|
core, and the add-ons the page names from the same build.
|
|
255
257
|
|
|
256
258
|
```html
|
|
257
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.
|
|
259
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@wcstack/state@4.4.0/dist/split/auto.js"></script>
|
|
258
260
|
<wcs-state features="scopes diagnostics">
|
|
259
261
|
<script type="module">
|
|
260
262
|
export default {
|
|
@@ -607,6 +609,8 @@ class YenInput extends HTMLElement {
|
|
|
607
609
|
|
|
608
610
|
Whichever you pick, `element.value` and the value extracted from the event must be the same logical state (the protocol's *Producer State Consistency Invariant*): the initial sync reads the property, every later update reads the event. Both shapes are in use inside wcstack — `<wcs-fetch>`'s `loading` dispatches the boolean as `detail` with no `getter`, its `value` reads `detail.value` through one — and DCC `$bindables` declare `getter: (e) => e.target[name]` because a sub-path write has no single value to put in `detail`. The default itself is not going to change: it is normative for every wc-bindable adapter (`@wc-bindable/core`'s `bind()` and the framework adapters implement the same `e.detail`), and the protocol classes a different default as a breaking change requiring a new protocol identifier.
|
|
609
611
|
|
|
612
|
+
**State's own write coming back.** When state writes a member (`element.value = v`) and the element dispatches the member's event from that setter, synchronously, with the same value (`Object.is`), the event is state's own write echoing back: it is not written to the state, and the setter of an accessor pair bound there does not run. This is part of the binding — no `$behavior` key changes it. An event dispatched later (after an `await`, from a timer) or carrying another object (a copy, JSON parsed again) is the element's own write: it reaches the state, and an accessor pair's setter runs. Write such a setter so that receiving what the state already holds changes nothing. Persisting a whole form through `<wcs-storage>` with a getter / setter pair relies on this rule: [Persisting a form as one object](../storage/README.md#2-persisting-a-form-as-one-object).
|
|
613
|
+
|
|
610
614
|
Override the authority per binding with `#init=`:
|
|
611
615
|
|
|
612
616
|
| Value | Initial sync | Allowed on |
|
|
@@ -766,6 +770,7 @@ When a `<wcs-state>` binds its root (the document, a shadow root, or a Light DOM
|
|
|
766
770
|
- **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
771
|
- 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
772
|
- The content of `<script>`, `<style>`, `<noscript>` and `<iframe>` is not walked; the element's own `data-wcs` (`srcdoc:`, `attr.src:`) still binds.
|
|
773
|
+
- **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
774
|
- 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
775
|
- 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
776
|
- `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.
|
|
@@ -1306,7 +1311,7 @@ Inside state objects (getters / methods), the following APIs are available via `
|
|
|
1306
1311
|
| `this.$getAll(path, indexes?)` | Get all values matching a wildcard path |
|
|
1307
1312
|
| `this.$setAll(path, indexes, value, options?)` | Write to every address matching a wildcard path |
|
|
1308
1313
|
| `this.$resolve(path, indexes, value?)` | Resolve a wildcard path with specific indexes |
|
|
1309
|
-
| `this.$postUpdate(path)` | Manually trigger update notification for a path |
|
|
1314
|
+
| `this.$postUpdate(path)` | Manually trigger update notification for a path. A `*` in it is the current row's index, as in a getter: called from an event handler of a row, `$postUpdate("rows.*.cells.*.text")` announces that row's cell only. To announce several rows, name each one (`` this.$postUpdate(`rows.${r}.cells.${c}.text`) ``) |
|
|
1310
1315
|
| `this.$dependOn(path)` | Manually register a dependency for cache invalidation |
|
|
1311
1316
|
| `this.$untracked(fn)` | Read values inside fn without registering dependencies |
|
|
1312
1317
|
| `this.$eq(path, key)` / `$eqPath(path, keyPath)` / `$eqIndex(path, level?)` | Keyed subscription for "is this row's key the value of `path`?" (see [Keyed selection](#keyed-selection-eq--eqpath--eqindex)) |
|
|
@@ -1416,7 +1421,7 @@ export default {
|
|
|
1416
1421
|
};
|
|
1417
1422
|
```
|
|
1418
1423
|
|
|
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"`)
|
|
1424
|
+
**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
1425
|
|
|
1421
1426
|
## Recursive Paths (`$recursion`)
|
|
1422
1427
|
|
|
@@ -1659,7 +1664,7 @@ export default {
|
|
|
1659
1664
|
|
|
1660
1665
|
`on*:` bindings for `click`, `dblclick`, `input`, `change`, `submit`, `keydown`, `keyup`, `mousedown`, `mouseup`, `pointerdown` and `pointerup` are **delegated**: one listener per event type sits on the root the state binds — the document, a shadow root, or, inside a Light DOM mounted component, the host element — and it runs the handlers of the elements the event passed, innermost first. So:
|
|
1661
1666
|
|
|
1662
|
-
- `event.currentTarget` in the handler is that root, not the element. Find the element from `event.target` (`event.target.closest("li")`), or use the loop index the handler receives (`removeItem(event, index)`).
|
|
1667
|
+
- `event.currentTarget` in the handler is that root, not the element. Find the element from `event.target` (`event.target.closest("li")`), or use the loop index the handler receives (`removeItem(event, index)`). Since 4.4, when a delegated handler whose source reads `currentTarget` throws (`getBoundingClientRect is not a function`), the `diagnostics` add-on says why after the error, once per handler (`[wcs/delegated-current-target]`, the lint's code).
|
|
1663
1668
|
- `#stop` on an inner `on*:` binding stops the `on*:` handlers further out. It cannot stop a listener your own code added to an ancestor (`addEventListener` on a clickable card): that listener ran before the event reached the root.
|
|
1664
1669
|
- Your code calling `stopPropagation()` on an ancestor (a modal's content keeping clicks from the overlay) keeps the delegated handlers inside it from running at all.
|
|
1665
1670
|
- An element moved under another root (a dialog moved from a shadow root to `document.body`) no longer runs its delegated handlers.
|
|
@@ -2108,6 +2113,7 @@ export default {
|
|
|
2108
2113
|
- The reserved name `$command` itself cannot appear in the array
|
|
2109
2114
|
- Tokens are gathered under `$command` so they do not pollute the top-level state namespace; reactive properties with the same name as a token can coexist
|
|
2110
2115
|
- Accessing an undeclared name on `$command` (e.g. `this.$command.typo`) returns `undefined`. The typo then surfaces as a `TypeError` on the subsequent `.emit()` call, or — when used as a binding right-hand side — as `[wcs/token-undeclared] "$command.typo" is not declared in $commandTokens.` (`#1302`, with a did-you-mean) when the binding is attached
|
|
2116
|
+
- A token keeps nothing: an emit while no `command.` binding subscribes to it is lost. Since 4.4 the `diagnostics` add-on warns about it once per token (`[wcs/command-token-unbound]`). The binding may not be attached yet — its element is defined later (an autoloader, a slow CDN), or it is in an `if:` branch the same update renders, so emit from `$renderedCallback` — or there may be none (the lint reports that case too)
|
|
2111
2117
|
|
|
2112
2118
|
### `command.<methodName>:` Binding
|
|
2113
2119
|
|
|
@@ -2649,6 +2655,7 @@ Notes:
|
|
|
2649
2655
|
|
|
2650
2656
|
- 3.x also wrote the attribute after the property (`String(value)`, JSON for an object). An element that reflects in its setter is unaffected; one that has no setter and relied on the binder to write its attribute needs a setter now (Migration guide, §3.4).
|
|
2651
2657
|
- Native HTML elements ignore `inputs` entirely — the declaration only applies to custom elements that expose `static wcBindable`.
|
|
2658
|
+
- A `for:` row or an `if:` branch is bound before it is inserted: a custom element in it receives its inputs (`element.value = …`) before its `connectedCallback` runs. At page level the element is connected first. A `connectedCallback` that renders from its defaults overwrites what the setters received — render from the current properties instead. (An outside evaluation's amount field lost an entry it had not accepted yet this way when its page moved from 3.x to 4.0.) The other early write, a property set before the element is defined, is the [I/O node guidelines](../../docs/async-io-node-guidelines.md)' `upgradeProperties` step.
|
|
2652
2659
|
|
|
2653
2660
|
## Choosing a Component Mechanism
|
|
2654
2661
|
|
|
@@ -3035,11 +3042,15 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
3035
3042
|
| Typo in a nested path (`user.nmae`) | `console.warn` (`wcs/binding-path-missing`). Updates still never arrive — you fix it |
|
|
3036
3043
|
| Typo in a top-level path (`cout`) | Throws on read — `[wcs/binding-path-missing] Path "cout" does not exist on the state tree. Did you mean "count"?` (`#301`) — so the binding fails to apply, reported like any binding failure |
|
|
3037
3044
|
| Typo in a `$watch` key | `console.warn` (`wcs/watch-path-missing`), reported even for a single segment |
|
|
3045
|
+
| Typo in a `$postUpdate` path (`` `rows.${r}.cell.${c}.text` `` for `cells`) | Since 4.4: `console.warn` (`wcs/post-update-path-missing`), a single segment too — the call updates nothing |
|
|
3046
|
+
| A write of a top-level key the state does not declare, beside a near one (`this.selectedID` and `selectedId`) | Since 4.4: `console.warn` (`wcs/write-path-missing`), once per key — the write creates a new key, and nothing bound to the declared one changes. A new key with no near name is not reported: a state may add keys on purpose |
|
|
3047
|
+
| Typo in a path a getter reads (`this["items.*.pirce"]`, `this.$getAll("items.*.pirce", [])`) | Since 4.4: `console.warn` (`wcs/read-path-missing`), once per path, naming the getter, beside a near name only — the read gives `undefined`, and what the getter computes from it is wrong. Checked once a getter has read the path: a row added later is checked when it renders. A top-level key (`this.totl`) throws on read (`#301`). `@wcstack/lint` checks the same reads in getters and methods without running them |
|
|
3038
3048
|
|
|
3039
3049
|
The check **under-approximates**: it stays silent for anything it cannot decide statically, because a false alarm costs more than a missed one. None of these warn:
|
|
3040
3050
|
|
|
3041
3051
|
- A `null` / `undefined` parent (the "seed as `null`, assign later" shape)
|
|
3042
3052
|
- Row fields of a list that starts empty (the row shape is unknown)
|
|
3053
|
+
- A row field no row has, unless a row has a near name (a likely typo, `.nmae` beside `name`) — rows are data, and an optional field (a `pending` the page puts on its own rows) is absent from the others. Since 4.4; the first row lacking a field another row has does not warn either (4.3)
|
|
3043
3054
|
- Sub-properties of an intermediate getter's return value
|
|
3044
3055
|
- Paths under the mount path of a volume that is still loading — they are checked once it settles
|
|
3045
3056
|
- Reserved `$` namespaces (`$command.*` and friends)
|
|
@@ -3172,7 +3183,7 @@ In short:
|
|
|
3172
3183
|
|
|
3173
3184
|
`@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.
|
|
3174
3185
|
|
|
3175
|
-
**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.
|
|
3186
|
+
**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:
|
|
3176
3187
|
|
|
3177
3188
|
- Put user content in **state**, and bind it with `textContent:` (or `{{ }}` / a comment binding, which are text) — a value is never read as markup.
|
|
3178
3189
|
- 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)).
|
|
@@ -3601,7 +3612,7 @@ That's it. The client-side `@wcstack/state` finds the `<wcs-ssr>` element, takes
|
|
|
3601
3612
|
|-------|-------------|
|
|
3602
3613
|
| **Server** | `renderToString()` runs your template in happy-dom, executes `$connectedCallback` (including `fetch()`), applies all bindings, and outputs rendered HTML with a `<wcs-ssr>` element before the `<wcs-state>`. Rows, branches and text bindings are marked with comments. `$watch` handlers do not run, and streams keep their `initial` value |
|
|
3603
3614
|
| **Client** | `<wcs-state enable-ssr>` loads its state from its own source, takes the snapshot's data over it (getters, setters and methods stay), skips `$connectedCallback`, and binds the server's DOM in place. Rows and branches are adopted where they are — a custom element in them is connected once and never disconnected and connected again — and get their bindings. A server row that no longer fits its template is rendered again in its place; rows and branches the client does not render (fewer rows, another branch) go as soon as the page is bound. A volume adopts the server's data at its mount path |
|
|
3604
|
-
| **Version mismatch** | A snapshot whose `<wcs-ssr version>` has another major.minor is discarded with one `console.warn`, and the page renders on the client as if there were no server: the state loads from its own source, `$connectedCallback` runs, and the server's rows, branches and markers give way to the templates they were rendered from |
|
|
3615
|
+
| **Version mismatch** | A snapshot whose `<wcs-ssr version>` has another major.minor is discarded with one `console.warn`, and the page renders on the client as if there were no server: the state loads from its own source, `$connectedCallback` runs, and the server's rows, branches and markers give way to the templates they were rendered from. Since 4.4 they give way only after the page was planned: markup this client cannot plan (a newer minor's filter argument, for example) fails its initialization with the server's HTML left as it was |
|
|
3605
3616
|
|
|
3606
3617
|
### What `enable-ssr` Does
|
|
3607
3618
|
|