@wcstack/state 1.33.0 → 2.1.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 +33 -40
- package/README.md +32 -40
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +93 -77
- package/dist/index.esm.js +2532 -1564
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +1 -2
- package/dist/manifest.esm.js +1 -3
- package/dist/parser.d.ts +0 -1
- package/dist/parser.esm.js +8 -8
- package/dist/wcs-manifest.json +0 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
|----------|----------------|--------------|
|
|
47
47
|
| **状態** (`<wcs-state>`) | データ構造とビジネスロジック | どのDOM要素がバインドされているか |
|
|
48
48
|
| **UI** (`data-wcs`) | パス文字列と表示意図 | 状態がどう保存・算出されているか |
|
|
49
|
-
| **コンポーネント** (
|
|
49
|
+
| **コンポーネント** (`state: path`) | ホストが書くマウント表 | 他コンポーネントの内部実装 |
|
|
50
50
|
|
|
51
51
|
3つのレベルのパス契約が疎結合を実現しています:
|
|
52
52
|
|
|
53
53
|
1. **UI ↔ 状態** — `data-wcs="textContent: user.name"` という属性がバインディングのすべてです。フックもセレクタもリアクティブプリミティブもありません。コンポーネントのJavaScriptには、状態を参照するコードが**一行も**必要ありません。
|
|
54
54
|
|
|
55
|
-
2. **コンポーネント ↔ コンポーネント** —
|
|
55
|
+
2. **コンポーネント ↔ コンポーネント** — ホストが各コンポーネントへ部分木をマウントし(`<my-card data-wcs="state: user">`)、ボリュームが追加モジュールをツリーに接ぎ木します(`<wcs-state mount="i18n">`)。コンポーネント同士がお互いを直接インポートしたり参照したりすることはありません。すべての接続は単一ツリー上のパス接頭辞だけです。
|
|
56
56
|
|
|
57
57
|
3. **ループコンテキスト** — `for` ループ内では `*` が抽象インデックスとして機能します。`items.*.price` のようなバインディングは自動的に現在の要素へと解決されます。テンプレートは自身の具体的な位置(インデックス)を知る必要がなく、ワイルドカードがその契約となります。
|
|
58
58
|
|
|
@@ -222,21 +222,20 @@
|
|
|
222
222
|
|
|
223
223
|
> **Content-Security-Policy 下では:** 5 番(内包 `<script type="module">`)は `blob:` URL 経由で評価されるため `script-src blob:` が必要です。ページの nonce では救えません。厳格な CSP を敷く場合は 4 番(`src="./state.js"`)を使ってください。追加ディレクティブは不要です。詳細は [docs/csp.ja.md](../../docs/csp.ja.md)。
|
|
224
224
|
|
|
225
|
-
###
|
|
225
|
+
### 追加の状態をマウントする(`mount=`)
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
状態のツリーは **1 つの root に 1 本**です。状態をモジュールに分けたい場合はボリュームをマウントします。データはマウントパスの位置でルートツリーに接ぎ木され、バインディングは接頭辞付きのパスで読みます。
|
|
228
228
|
|
|
229
229
|
```html
|
|
230
|
-
<wcs-state
|
|
231
|
-
<wcs-state
|
|
230
|
+
<wcs-state mount="cart" src="./cart.js"></wcs-state>
|
|
231
|
+
<wcs-state src="./app.js"></wcs-state>
|
|
232
232
|
|
|
233
|
-
<div data-wcs="textContent: total
|
|
234
|
-
<div data-wcs="textContent: name@user"></div>
|
|
233
|
+
<div data-wcs="textContent: cart.total"></div>
|
|
235
234
|
```
|
|
236
235
|
|
|
237
|
-
|
|
236
|
+
ボリュームは getter・`$watch`・`$listKeys`・`$updatedCallback`・`$connectedCallback`/`$disconnectedCallback` を宣言できます(すべてマウントパス相対)。読み込み順は自由です(ルートより先に接続されたボリュームは、ルートの登録時に接ぎ木されます)。マウントパスは静的パスのみです(`*`・`$`・`#`・`@` は不可)。
|
|
238
237
|
|
|
239
|
-
>
|
|
238
|
+
> **v1 の名前付き状態からの移行:** `<wcs-state name="cart">` + `total@cart` は `<wcs-state mount="cart">` + `cart.total` になります。v2 では `name` 属性は fail-fast し、パス中の `@` は parse error です(どちらもこの誘導文付き)。移行の対応表: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9。
|
|
240
239
|
|
|
241
240
|
## 状態の更新
|
|
242
241
|
|
|
@@ -289,7 +288,7 @@ this.items.sort((a, b) => a.id - b.id);
|
|
|
289
288
|
### `data-wcs` 属性
|
|
290
289
|
|
|
291
290
|
```
|
|
292
|
-
property[#modifier]: path[
|
|
291
|
+
property[#modifier]: path[|filter[|filter(args)...]]
|
|
293
292
|
```
|
|
294
293
|
|
|
295
294
|
複数バインディングは `;` で区切ります:
|
|
@@ -303,7 +302,6 @@ property[#modifier]: path[@state][|filter[|filter(args)...]]
|
|
|
303
302
|
| `property` | バインドする DOM プロパティ | `value`, `textContent`, `checked` |
|
|
304
303
|
| `#modifier` | バインディング修飾子 | `#ro`, `#prevent`, `#stop`, `#onchange` |
|
|
305
304
|
| `path` | 状態プロパティパス | `count`, `user.name`, `users.*.name` |
|
|
306
|
-
| `@state` | 名前付き状態の参照 | `@cart`, `@user` |
|
|
307
305
|
| `\|filter` | 変換フィルタチェーン | `\|gt(0)`, `\|round\|locale` |
|
|
308
306
|
|
|
309
307
|
### プロパティ種別
|
|
@@ -488,7 +486,7 @@ export default {
|
|
|
488
486
|
|
|
489
487
|
- spread 右辺へのフィルタ(`...: target|filter`)はエラー
|
|
490
488
|
- 右辺パスの途中に `*` を含めても OK(例:`...: stores.*.fetch`)
|
|
491
|
-
-
|
|
489
|
+
- 右辺は素のツリーパス(`...: fetchX`、途中の `*` も可)
|
|
492
490
|
- カスタム要素クラスが未登録の場合、`customElements.whenDefined(tag)` 解決時に遅延展開される(autoloader による遅延ロードに対応)
|
|
493
491
|
- `wcBindable` 宣言**のない**要素はエラー(明示配線で書いてください)。spread は何を展開すべきかを契約から読み取るため
|
|
494
492
|
|
|
@@ -557,7 +555,6 @@ this.items = await (await fetch("/api/items")).json();
|
|
|
557
555
|
| `.name` | `users.*.name` | 現在の要素のプロパティ |
|
|
558
556
|
| `.` | `users.*` | 現在の要素そのもの |
|
|
559
557
|
| `.name\|uc` | `users.*.name\|uc` | フィルタは保持される |
|
|
560
|
-
| `.name@state` | `users.*.name@state` | 状態名は保持される |
|
|
561
558
|
|
|
562
559
|
プリミティブ配列では、`.` が要素の値を直接参照します:
|
|
563
560
|
|
|
@@ -1204,7 +1201,7 @@ customElements.define("my-component", MyComponent);
|
|
|
1204
1201
|
|
|
1205
1202
|
### コンポーネント定義(Light DOM)
|
|
1206
1203
|
|
|
1207
|
-
Light DOM コンポーネントは Shadow DOM を使用しません。
|
|
1204
|
+
Light DOM コンポーネントは Shadow DOM を使用しません。v2 では Shadow 形と同じ書き方になります —— コンポーネントのバインディングはマウント位置でホストのツリーへ変換されるため、**name も `@` セレクタも不要**で、同じコンポーネントをリストの行ごとに置けます:
|
|
1208
1205
|
|
|
1209
1206
|
```javascript
|
|
1210
1207
|
class MyLightComponent extends HTMLElement {
|
|
@@ -1212,30 +1209,21 @@ class MyLightComponent extends HTMLElement {
|
|
|
1212
1209
|
|
|
1213
1210
|
connectedCallback() {
|
|
1214
1211
|
this.innerHTML = `
|
|
1215
|
-
<wcs-state bind-component="state"
|
|
1216
|
-
<div data-wcs="text: message
|
|
1217
|
-
<input type="text" data-wcs="value: message
|
|
1212
|
+
<wcs-state bind-component="state"></wcs-state>
|
|
1213
|
+
<div data-wcs="text: message"></div>
|
|
1214
|
+
<input type="text" data-wcs="value: message" />
|
|
1218
1215
|
`;
|
|
1219
1216
|
}
|
|
1220
1217
|
}
|
|
1221
1218
|
customElements.define("my-light-component", MyLightComponent);
|
|
1222
1219
|
```
|
|
1223
1220
|
|
|
1224
|
-
- Light DOM コンポーネントでは `name` 属性が**必須**です(名前空間が上位スコープと共有されるため)
|
|
1225
|
-
- バインディングでは `@my-light` のように状態名を明示的に参照する必要があります
|
|
1226
1221
|
- `<wcs-state>` はコンポーネント要素の直下に配置する必要があります
|
|
1222
|
+
- **ホストからの配線が必須**です(`<my-light-component data-wcs="state.message: user.name">` または `state: user`)。配線の無い plain な Light DOM `bind-component` は v2 では成立しません(親と root を共有したまま独立ツリーは持てない)—— 誘導文付きで loud に失敗します: shadow を付けるか、ホストから配線してマウントにしてください。
|
|
1227
1223
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
> **注意**: Light DOM は名前空間を上位スコープと共有するため、**同じ `name` を持つインスタンスを
|
|
1233
|
-
> 同一スコープに複数置くことはできません**。リストの行ごとにコンポーネントを配置するような形は
|
|
1234
|
-
> Shadow DOM を使ってください。
|
|
1235
|
-
>
|
|
1236
|
-
> また `State.getBindingsReady(root)` はコンポーネントのスコープを含みません(Shadow DOM 形で
|
|
1237
|
-
> 子が別 rootNode にあるのと同じ扱いです)。コンポーネント内部の描画完了まで待ちたい場合は、
|
|
1238
|
-
> コンポーネント側の `<wcs-state>` の初期化を待ってください。
|
|
1224
|
+
> **注意**: `State.getBindingsReady(root)` はマウント記録の確定後、マウントスコープも待ちます。
|
|
1225
|
+
> コンポーネント内部の描画完了まで待ちたい場合は、コンポーネント側の `<wcs-state>` の初期化を
|
|
1226
|
+
> 待ってください。
|
|
1239
1227
|
|
|
1240
1228
|
### ホスト側の使用方法
|
|
1241
1229
|
|
|
@@ -1293,9 +1281,16 @@ customElements.define("user-card", UserCard);
|
|
|
1293
1281
|
- 部分マウントを併用できます: `state: user; state.theme: theme` は `theme` を 2 つ目の入口としてマウントします(最長接頭辞が勝つので、中の `theme.mode` はツリーの `theme.mode` を読みます)
|
|
1294
1282
|
- ループでは**行そのもの**をマウントします: `<template data-wcs="for: users"><user-row data-wcs="state: ."></user-row></template>`。行コンポーネントの中の `name` は `users.*.name`、中の `for: tags` は `users.*.tags.*` を回します
|
|
1295
1283
|
- **自前のキーは私有**です([docs/state-mount-design.md](../../docs/state-mount-design.md) §4-3 の R1): コンポーネントが自分で宣言したデータキー(`state = { mode: "view" }`)はその要素のもので、ツリーには書かれません。マウント先に同名のキーがあってそれを隠す形(`user.name` の上に `state = { name: "" }`)では、ランタイムが 1 回だけ warn します(`wcs/mount-own-key-shadow`)— ツリーを読みたければ既定値を消し、私有のままにしたければ名前を変えてください
|
|
1296
|
-
- 配列そのものをルートにマウントする形(`state: rows` + 中で `for
|
|
1284
|
+
- 配列そのものをルートにマウントする形(`state: rows` + 中で `for`)は非対応です。行をマウントする(`state: .`)か、配列を持つオブジェクトをマウントして中で `for` を回してください(`state: group` + `for: children`)。どちらも契約テストで固定されており、マウントがツリー拡張の唯一の手段です
|
|
1297
1285
|
|
|
1298
|
-
> プロパティ単位の形(`state.message: user.name
|
|
1286
|
+
> プロパティ単位の形(`state.message: user.name`)はそのまま動きます — 同じ機構の上の部分マウントです。
|
|
1287
|
+
> R1 はすべてのマウント形で厳格です — マップされるキーに既定値を
|
|
1288
|
+
> 宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)は自前のキーが
|
|
1289
|
+
> **私有**になり、ホストの値を隠します(1 回だけ `wcs/mount-own-key-shadow` が指します)。ツリーを
|
|
1290
|
+
> 読むには既定値を消してください。Light DOM のマウントに `name` は不要で、
|
|
1291
|
+
> `element.state`(および getter / メソッド内の `this`)の `$getAll` / `$setAll` / `$resolve` /
|
|
1292
|
+
> `$postUpdate` はコンポーネント自身の語彙で書けます — パスはマウント先へ翻訳され、ホスト行の
|
|
1293
|
+
> 添字は自動で前置されます。
|
|
1299
1294
|
|
|
1300
1295
|
### 独立した Web Component への状態注入(`__e2e__/single-component`)
|
|
1301
1296
|
|
|
@@ -1335,8 +1330,7 @@ customElements.define("my-component", MyComponent);
|
|
|
1335
1330
|
|
|
1336
1331
|
- `bind-component` 付きの `<wcs-state>` はコンポーネント要素の**直下**(トップレベル)に配置すること
|
|
1337
1332
|
- 親要素は**カスタム要素**(ハイフンを含むタグ名)であること
|
|
1338
|
-
- Light DOM
|
|
1339
|
-
- Light DOM のバインディングでは状態名を明示的に参照すること(例: `@my-light`)
|
|
1333
|
+
- Light DOM コンポーネントはホストからの配線が必須(plain 形は v2 で廃止)
|
|
1340
1334
|
|
|
1341
1335
|
### ループ内でのコンポーネント使用
|
|
1342
1336
|
|
|
@@ -1864,13 +1858,13 @@ $updatedCallback(paths) {
|
|
|
1864
1858
|
|
|
1865
1859
|
主なルール:
|
|
1866
1860
|
|
|
1867
|
-
-
|
|
1861
|
+
- **ツリーのパスのみ** —— パスに `@`(v1 の名前セレクタ)は書けません。含む宣言は loud に拒否されます。
|
|
1868
1862
|
- **中間値は観測できません** —— 1 バッチ内の `a → b → c` は `cur = c` / `prev = a` で 1 回だけ発火します(binding 更新と同じ契約)。
|
|
1869
1863
|
- **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
|
|
1870
1864
|
- **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。
|
|
1871
1865
|
- **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$updatedCallback` とは異なる扱いです。
|
|
1872
1866
|
- **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。32 段で打ち切り、コンソールに報告します(値と DOM は巻き戻しません)。
|
|
1873
|
-
-
|
|
1867
|
+
- **マウントされた `bind-component` スコープでは実行されません** —— マウントされたコンポーネントは宣言面を実行せず、`$watch` の宣言があると 1 回だけ console.warn でルート state(またはボリューム —— `<wcs-state mount>` は `$watch` / `$listKeys` / `$updatedCallback` を持てます)へ誘導します(`$streams` も同様)。plain な(配線なし Shadow の)子は独立ツリーを持つので宣言できます。
|
|
1874
1868
|
- **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
|
|
1875
1869
|
|
|
1876
1870
|
## Inputs と属性ミラー
|
|
@@ -2171,7 +2165,7 @@ dropped. Validate statically: npx @wcstack/lint <file>.
|
|
|
2171
2165
|
- 親が `null` / `undefined`(初期値 `null` に後から代入する形)
|
|
2172
2166
|
- 初期値が空配列のリストの行フィールド(行の形が分からない)
|
|
2173
2167
|
- 途中の getter の戻り値のサブプロパティ
|
|
2174
|
-
-
|
|
2168
|
+
- マウントされたコンポーネントのマーカーパス(`#m…` —— 私有キー・getter の実体はマウントのオーバーレイ側にあり raw state には無い)
|
|
2175
2169
|
- `$` 始まりの予約名前空間(`$command.*` など)
|
|
2176
2170
|
|
|
2177
2171
|
裏を返すと、**警告が出ない = 正しい保証にはなりません**。網羅した検査は `npx @wcstack/lint <file>` 側で行ってください。
|
|
@@ -2442,7 +2436,7 @@ bootstrapState();
|
|
|
2442
2436
|
|
|
2443
2437
|
| 属性 | 説明 |
|
|
2444
2438
|
|---|---|
|
|
2445
|
-
| `
|
|
2439
|
+
| `mount` | この state をルートツリーへ**ボリューム**として接ぎ木する静的ツリーパス(v2 — 撤去された `name` 属性の後継。ツリーは 1 root に 1 本) |
|
|
2446
2440
|
| `state` | `<script type="application/json">` 要素の ID |
|
|
2447
2441
|
| `src` | `.json` または `.js` ファイルの URL |
|
|
2448
2442
|
| `json` | インライン JSON 文字列 |
|
|
@@ -2452,7 +2446,6 @@ bootstrapState();
|
|
|
2452
2446
|
|
|
2453
2447
|
| プロパティ / メソッド | 説明 |
|
|
2454
2448
|
|---|---|
|
|
2455
|
-
| `name` | 状態名 |
|
|
2456
2449
|
| `initializePromise` | 状態の完全な初期化時に解決される Promise |
|
|
2457
2450
|
| `listPaths` | `for` ループで使用されるパスの Set |
|
|
2458
2451
|
| `getterPaths` | getter として定義されたパスの Set |
|
package/README.md
CHANGED
|
@@ -46,13 +46,13 @@ In every existing framework, the **component** is the coupling point between UI
|
|
|
46
46
|
|-------|---------------|----------------------|
|
|
47
47
|
| **State** (`<wcs-state>`) | Data structure and business logic | Which DOM nodes are bound |
|
|
48
48
|
| **UI** (`data-wcs`) | Path strings and display intent | How state is stored or computed |
|
|
49
|
-
| **Components** (
|
|
49
|
+
| **Components** (`state: path`) | The mount table the host writes | The other component's internals |
|
|
50
50
|
|
|
51
51
|
Three levels of path contracts keep everything loosely coupled:
|
|
52
52
|
|
|
53
53
|
1. **UI ↔ State** — A `data-wcs="textContent: user.name"` attribute is the entire binding. No hooks, no selectors, no reactive primitives. The component's JavaScript doesn't contain a single line that references state.
|
|
54
54
|
|
|
55
|
-
2. **Component ↔ Component** —
|
|
55
|
+
2. **Component ↔ Component** — The host mounts a subtree onto each component (`<my-card data-wcs="state: user">`), and volumes graft extra modules onto the tree (`<wcs-state mount="i18n">`). Components never import or depend on each other; every connection is a path prefix on the single tree, nothing more.
|
|
56
56
|
|
|
57
57
|
3. **Loop context** — Inside a `for` loop, `*` acts as an abstract index. Bindings like `items.*.price` resolve to the current element automatically. The template doesn't know its concrete position — the wildcard is the contract.
|
|
58
58
|
|
|
@@ -222,21 +222,20 @@ Resolution order: `state` → `src` (.json / .js) → `json` → inner `<script>
|
|
|
222
222
|
|
|
223
223
|
> **Under a Content-Security-Policy:** form 5 (inline `<script type="module">`) is evaluated through a `blob:` URL and therefore requires `script-src blob:`. A page nonce does not cover it. If you enforce a strict CSP, use form 4 (`src="./state.js"`) instead — it needs no extra directive. See [docs/csp.md](../../docs/csp.md).
|
|
224
224
|
|
|
225
|
-
###
|
|
225
|
+
### Mounting Additional State (`mount=`)
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
There is **one state tree per root**. To split state across modules, mount a volume: its data grafts onto the root tree at the mount path, and bindings read it by prefix.
|
|
228
228
|
|
|
229
229
|
```html
|
|
230
|
-
<wcs-state
|
|
231
|
-
<wcs-state
|
|
230
|
+
<wcs-state mount="cart" src="./cart.js"></wcs-state>
|
|
231
|
+
<wcs-state src="./app.js"></wcs-state>
|
|
232
232
|
|
|
233
|
-
<div data-wcs="textContent: total
|
|
234
|
-
<div data-wcs="textContent: name@user"></div>
|
|
233
|
+
<div data-wcs="textContent: cart.total"></div>
|
|
235
234
|
```
|
|
236
235
|
|
|
237
|
-
|
|
236
|
+
A volume may declare getters, `$watch`, `$listKeys`, `$updatedCallback`, and `$connectedCallback`/`$disconnectedCallback` — all relative to its mount path. Load order does not matter (a volume connected before the root is grafted when the root registers). Mount paths must be static (`*`, `$`, `#`, `@` are rejected).
|
|
238
237
|
|
|
239
|
-
> **
|
|
238
|
+
> **Migrating from v1's named states:** `<wcs-state name="cart">` + `total@cart` becomes `<wcs-state mount="cart">` + `cart.total`. In v2 the `name` attribute fails fast and `@` in a path is a parse error, each with this exact guidance. Migration table: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9.
|
|
240
239
|
|
|
241
240
|
## Updating State
|
|
242
241
|
|
|
@@ -289,7 +288,7 @@ this.items.sort((a, b) => a.id - b.id);
|
|
|
289
288
|
### `data-wcs` Attribute
|
|
290
289
|
|
|
291
290
|
```
|
|
292
|
-
property[#modifier]: path[
|
|
291
|
+
property[#modifier]: path[|filter[|filter(args)...]]
|
|
293
292
|
```
|
|
294
293
|
|
|
295
294
|
Multiple bindings separated by `;`:
|
|
@@ -303,7 +302,6 @@ Multiple bindings separated by `;`:
|
|
|
303
302
|
| `property` | DOM property to bind | `value`, `textContent`, `checked` |
|
|
304
303
|
| `#modifier` | Binding modifier | `#ro`, `#prevent`, `#stop`, `#onchange` |
|
|
305
304
|
| `path` | State property path | `count`, `user.name`, `users.*.name` |
|
|
306
|
-
| `@state` | Named state reference | `@cart`, `@user` |
|
|
307
305
|
| `\|filter` | Transform filter chain | `\|gt(0)`, `\|round\|locale` |
|
|
308
306
|
|
|
309
307
|
### Property Types
|
|
@@ -488,7 +486,7 @@ Runtime reads `customClass.wcBindable.properties + inputs` and expands each name
|
|
|
488
486
|
|
|
489
487
|
- Filters on the spread target (`...: target|filter`) are rejected.
|
|
490
488
|
- The right-hand path may contain `*` anywhere (e.g. `...: stores.*.fetch`).
|
|
491
|
-
-
|
|
489
|
+
- The right-hand side is a plain tree path (`...: fetchX` or `...: stores.*.fetch`).
|
|
492
490
|
- If the custom element class is not yet registered, expansion is deferred until `customElements.whenDefined(tag)` resolves — autoloader-style late registration is supported.
|
|
493
491
|
- Elements **without** a `wcBindable` declaration are rejected (write bindings explicitly). Spread requires the contract to know what to expand.
|
|
494
492
|
|
|
@@ -557,7 +555,6 @@ Inside a `for` loop, paths starting with `.` are expanded relative to the loop's
|
|
|
557
555
|
| `.name` | `users.*.name` | Property of the current element |
|
|
558
556
|
| `.` | `users.*` | The current element itself |
|
|
559
557
|
| `.name\|uc` | `users.*.name\|uc` | Filters are preserved |
|
|
560
|
-
| `.name@state` | `users.*.name@state` | State name is preserved |
|
|
561
558
|
|
|
562
559
|
For primitive arrays, `.` refers to the element value directly:
|
|
563
560
|
|
|
@@ -1205,7 +1202,7 @@ customElements.define("my-component", MyComponent);
|
|
|
1205
1202
|
|
|
1206
1203
|
### Component Definition (Light DOM)
|
|
1207
1204
|
|
|
1208
|
-
Light DOM components do not use Shadow DOM.
|
|
1205
|
+
Light DOM components do not use Shadow DOM. In v2 the form is identical to the Shadow form — the component's bindings are translated onto the host's tree at its mount point, so **no name and no `@` selectors are needed**, and the same component can sit on every row of a list:
|
|
1209
1206
|
|
|
1210
1207
|
```javascript
|
|
1211
1208
|
class MyLightComponent extends HTMLElement {
|
|
@@ -1213,30 +1210,20 @@ class MyLightComponent extends HTMLElement {
|
|
|
1213
1210
|
|
|
1214
1211
|
connectedCallback() {
|
|
1215
1212
|
this.innerHTML = `
|
|
1216
|
-
<wcs-state bind-component="state"
|
|
1217
|
-
<div data-wcs="text: message
|
|
1218
|
-
<input type="text" data-wcs="value: message
|
|
1213
|
+
<wcs-state bind-component="state"></wcs-state>
|
|
1214
|
+
<div data-wcs="text: message"></div>
|
|
1215
|
+
<input type="text" data-wcs="value: message" />
|
|
1219
1216
|
`;
|
|
1220
1217
|
}
|
|
1221
1218
|
}
|
|
1222
1219
|
customElements.define("my-light-component", MyLightComponent);
|
|
1223
1220
|
```
|
|
1224
1221
|
|
|
1225
|
-
- `name` attribute is **required** for Light DOM components (namespace is shared with the parent scope)
|
|
1226
|
-
- Bindings must explicitly reference the state name with `@my-light`
|
|
1227
1222
|
- `<wcs-state>` must be a direct child of the component element
|
|
1223
|
+
- The host **must wire it** (`<my-light-component data-wcs="state.message: user.name">` or `state: user`) — a plain, unwired Light DOM `bind-component` cannot exist in v2 (an independent tree cannot share the parent's root). It fails loudly with the migration guidance: attach a shadow root, or mount it from the host.
|
|
1228
1224
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
wired once the component's own state has registered its name
|
|
1232
|
-
|
|
1233
|
-
> **Note**: Light DOM shares its namespace with the parent scope, so **two instances carrying the same
|
|
1234
|
-
> `name` cannot live in one scope**. Use Shadow DOM for shapes that place a component on every row of
|
|
1235
|
-
> a list.
|
|
1236
|
-
>
|
|
1237
|
-
> Also, `State.getBindingsReady(root)` does not cover the component's scope — the same as the Shadow
|
|
1238
|
-
> DOM form, where the child lives in a different rootNode. Await the component's own `<wcs-state>`
|
|
1239
|
-
> initialization when you need to wait for its contents to render.
|
|
1225
|
+
> **Note**: `State.getBindingsReady(root)` covers mounted scopes once the mount record resolves; await
|
|
1226
|
+
> the component's own `<wcs-state>` initialization when you need its contents rendered.
|
|
1240
1227
|
|
|
1241
1228
|
### Host Usage
|
|
1242
1229
|
|
|
@@ -1294,9 +1281,16 @@ customElements.define("user-card", UserCard);
|
|
|
1294
1281
|
- A partial mount can sit next to it: `state: user; state.theme: theme` mounts `theme` as a second entry point (longest prefix wins, so `theme.mode` inside the component reads the tree's `theme.mode`).
|
|
1295
1282
|
- In a loop, mount **the row itself**: `<template data-wcs="for: users"><user-row data-wcs="state: ."></user-row></template>`. Inside the row component `name` is `users.*.name`, and its own `for: tags` runs over `users.*.tags.*`.
|
|
1296
1283
|
- **Own keys are private** (rule R1 in [docs/state-mount-design.md](../../docs/state-mount-design.md) §4-3): a data key the component declares itself (`state = { mode: "view" }`) belongs to that element and is never written to the tree. If it hides a key that exists at the mount point (`state = { name: "" }` mounted over `user.name`), the runtime warns once (`wcs/mount-own-key-shadow`) — remove the default to read the tree, or rename it to keep it private.
|
|
1297
|
-
- Mounting an array as the root (`state: rows` with `for` over it inside) is not supported
|
|
1284
|
+
- Mounting an array as the root (`state: rows` with `for` over it inside) is not supported; mount the row (`state: .`) or the object that holds the array (`state: group` with `for: children` inside). Both forms are contract-tested; mounts are the only way to extend the tree.
|
|
1298
1285
|
|
|
1299
|
-
> The per-property form (`state.message: user.name`) keeps working
|
|
1286
|
+
> The per-property form (`state.message: user.name`) keeps working — it is a partial mount on
|
|
1287
|
+
> the same machinery. R1 is strict for every mount form — a component that declares a default
|
|
1288
|
+
> for a mapped key (`state = { message: "" }` together with `state.message: ...`) keeps its
|
|
1289
|
+
> own key **private**, hiding the host value (a one-time `wcs/mount-own-key-shadow` warning
|
|
1290
|
+
> points at it). Drop the default to read the tree. The mounted `<wcs-state>` needs no `name`
|
|
1291
|
+
> in Light DOM, and `$getAll` / `$setAll` / `$resolve` / `$postUpdate` on `element.state`
|
|
1292
|
+
> (and on `this` inside getters/methods) speak the component's own vocabulary — paths are
|
|
1293
|
+
> translated onto the mount and the host row's indexes are prepended automatically.
|
|
1300
1294
|
|
|
1301
1295
|
### Standalone Web Component Injection (`__e2e__/single-component`)
|
|
1302
1296
|
|
|
@@ -1336,8 +1330,7 @@ customElements.define("my-component", MyComponent);
|
|
|
1336
1330
|
|
|
1337
1331
|
- `<wcs-state>` with `bind-component` must be a **direct child** of the component element (top-level)
|
|
1338
1332
|
- The parent element must be a **custom element** (tag name containing a hyphen)
|
|
1339
|
-
- Light DOM components
|
|
1340
|
-
- Light DOM bindings must reference the state name explicitly (e.g., `@my-light`)
|
|
1333
|
+
- Light DOM components must be wired from the host (the plain, unwired form was removed in v2)
|
|
1341
1334
|
|
|
1342
1335
|
### Loop with Components
|
|
1343
1336
|
|
|
@@ -1869,13 +1862,13 @@ Firing order is defined in three layers, and only the middle one is yours to ste
|
|
|
1869
1862
|
|
|
1870
1863
|
Key rules:
|
|
1871
1864
|
|
|
1872
|
-
- **
|
|
1865
|
+
- **Paths of the tree only** — a path may not contain `@` (the v1 name selector); such a declaration is rejected loudly.
|
|
1873
1866
|
- **Intermediate values are not observable** — a batch that goes `a → b → c` fires once with `cur = c`, `prev = a`, the same contract as binding updates.
|
|
1874
1867
|
- **Row-level diffs want `$listKeys`** — without it, assigning a whole array fires the row watch for *every* row with `prev === undefined`, because no row went through a path write. With `$listKeys` declared, the key match decomposes the assignment into per-field writes, so only changed rows fire and `prev` is a real scalar.
|
|
1875
1868
|
- **A headless row watch requires `$listKeys`** — this is the one place `$watch` is *not* headless on its own. Expanding `items` into `items.*.price` is driven by the list's `for` binding, and declaring a watch deliberately does not register the path as a list. So with neither a `for` binding nor `$listKeys`, assigning the array fires the row watch **zero** times. Add `$listKeys` (the key match writes each field by path, bypassing the expansion) or render the list. Scalar paths — including nested ones like `user.name` — are headless with no such condition.
|
|
1876
1869
|
- **Handler exceptions are isolated** — a throw is reported to the console and the remaining watches (and stream restarts) still run. This differs from `$connectedCallback` / `$updatedCallback`, which fail loudly.
|
|
1877
1870
|
- **Write chains are bounded** — a handler's writes form a new batch, so mutually-writing watches would loop forever; the chain is cut off after 32 links with a console error. Values and DOM are not rolled back.
|
|
1878
|
-
- **Not
|
|
1871
|
+
- **Not run on a mounted `bind-component` scope** — mounted components do not execute declaration surfaces: the `$watch` declaration is ignored with a one-time console warning that points to the root state (or a volume — `<wcs-state mount>` hosts `$watch` / `$listKeys` / `$updatedCallback`). This applies to `$streams` too. A plain (unwired Shadow) child owns an independent tree and can declare it.
|
|
1879
1872
|
- **SSR does not run watches** — handler side effects would otherwise execute on both server and client.
|
|
1880
1873
|
|
|
1881
1874
|
## Inputs and Attribute Mirror
|
|
@@ -2176,7 +2169,7 @@ The check **under-approximates**: it stays silent for anything it cannot decide
|
|
|
2176
2169
|
- A `null` / `undefined` parent (the "seed as `null`, assign later" shape)
|
|
2177
2170
|
- Row fields of a list that starts empty (the row shape is unknown)
|
|
2178
2171
|
- Sub-properties of an intermediate getter's return value
|
|
2179
|
-
-
|
|
2172
|
+
- Marker paths of mounted components (`#m…` — private keys and getters live on the mount overlay, not the raw state)
|
|
2180
2173
|
- Reserved `$` namespaces (`$command.*` and friends)
|
|
2181
2174
|
|
|
2182
2175
|
So **no warning is not a proof of correctness.** For exhaustive checking, run `npx @wcstack/lint <file>`.
|
|
@@ -2450,7 +2443,7 @@ bootstrapState();
|
|
|
2450
2443
|
|
|
2451
2444
|
| Attribute | Description |
|
|
2452
2445
|
|---|---|
|
|
2453
|
-
| `
|
|
2446
|
+
| `mount` | Static tree path to graft this state onto the root tree as a **volume** (v2 — replaces the removed `name` attribute; one state tree per root) |
|
|
2454
2447
|
| `state` | ID of a `<script type="application/json">` element |
|
|
2455
2448
|
| `src` | URL to `.json` or `.js` file |
|
|
2456
2449
|
| `json` | Inline JSON string |
|
|
@@ -2460,7 +2453,6 @@ bootstrapState();
|
|
|
2460
2453
|
|
|
2461
2454
|
| Property / Method | Description |
|
|
2462
2455
|
|---|---|
|
|
2463
|
-
| `name` | State name |
|
|
2464
2456
|
| `initializePromise` | Resolves when state is fully initialized |
|
|
2465
2457
|
| `listPaths` | Set of paths used in `for` loops |
|
|
2466
2458
|
| `getterPaths` | Set of paths defined as getters |
|