@wcstack/state 1.26.0 → 1.28.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 +100 -8
- package/README.md +103 -7
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +70 -3
- package/dist/index.esm.js +1407 -174
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.esm.js +213 -2
- package/dist/parser.d.ts +74 -0
- package/dist/parser.esm.js +1452 -0
- package/dist/wcs-manifest.json +112 -1
- package/package.json +5 -1
package/README.ja.md
CHANGED
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
- **宣言的データバインディング** — `data-wcs` 属性によるプロパティ / テキスト / イベント / 構造バインディング
|
|
96
96
|
- **リアクティブ Proxy** — ES Proxy による依存追跡付き自動 DOM 更新
|
|
97
97
|
- **構造ディレクティブ** — `<template>` 要素による `for`, `if` / `elseif` / `else`
|
|
98
|
-
- **組み込みフィルタ** — フォーマット、比較、算術、日付など
|
|
98
|
+
- **組み込みフィルタ** — フォーマット、比較、算術、日付など 46 種類
|
|
99
99
|
- **双方向バインディング** — `<input>`, `<select>`, `<textarea>` で自動有効
|
|
100
100
|
- **Web Component バインディング** — Shadow DOM コンポーネントとの双方向状態バインディング
|
|
101
101
|
- **command token** — pub/sub チャネル(`command.<method>: tokenName`)で state から wc-bindable カスタム要素のメソッドを起動
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
|
|
221
221
|
解決順序: `state` → `src` (.json / .js) → `json` → 内包 `<script>` → `setInitialState()` 待機。
|
|
222
222
|
|
|
223
|
-
> **Content-Security-Policy 下では:** 5 番(内包 `<script type="module">`)は `blob:` URL 経由で評価されるため `script-src blob:` が必要です。ページの nonce では救えません。厳格な CSP を敷く場合は 4 番(`src="./state.js"`)を使ってください。追加ディレクティブは不要です。詳細は [docs/csp.md](../../docs/csp.md)。
|
|
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
|
### 名前付き状態
|
|
226
226
|
|
|
@@ -394,7 +394,7 @@ authority はバインディング単位で `#init=` により上書きできま
|
|
|
394
394
|
|
|
395
395
|
- `enableDirectionalInitialSync: false`(opt-out)のとき `#init=`/`#sync=` を書くと throw します。
|
|
396
396
|
- **1.20 以前からの移行:** output-only メンバに対して state 側に都合のよい初期値(`value: []`、`query: ""` 等)をシードしないでください — 要素側の実初期値(多くは `null`/`undefined`)がシードを置き換えます。シードは要素の実初期値に合わせ、表示用の値は派生 getter で null ガードしてください。
|
|
397
|
-
- **1.21.x まで**、`init=element` / `init=auto` / `init=none` はバインディングの生存期間全体で state→element 書き込みを抑止しており、真に双方向なメンバには使えませんでした。現在は authority は初期同期のみを支配します(`docs/architecture-hardening/09-remediation-design.md` §3.6)。
|
|
397
|
+
- **1.21.x まで**、`init=element` / `init=auto` / `init=none` はバインディングの生存期間全体で state→element 書き込みを抑止しており、真に双方向なメンバには使えませんでした。現在は authority は初期同期のみを支配します(`docs/architecture-hardening/09-remediation-design.ja.md` §3.6)。
|
|
398
398
|
|
|
399
399
|
### ラジオボタンバインディング
|
|
400
400
|
|
|
@@ -985,7 +985,7 @@ export default {
|
|
|
985
985
|
|
|
986
986
|
## フィルタ
|
|
987
987
|
|
|
988
|
-
|
|
988
|
+
46 種類の組み込みフィルタが入力(DOM → 状態)と出力(状態 → DOM)の両方向で利用できます。
|
|
989
989
|
|
|
990
990
|
### 比較
|
|
991
991
|
|
|
@@ -1008,6 +1008,8 @@ export default {
|
|
|
1008
1008
|
| `mul(n)` | 乗算 | `price\|mul(1.1)` |
|
|
1009
1009
|
| `div(n)` | 除算 | `total\|div(100)` |
|
|
1010
1010
|
| `mod(n)` | 剰余 | `index\|mod(2)` |
|
|
1011
|
+
| `abs` | 絶対値 | `delta\|abs` |
|
|
1012
|
+
| `clamp(min, max)` | 範囲内に丸める | `ratio\|clamp(0,100)` |
|
|
1011
1013
|
|
|
1012
1014
|
### 数値フォーマット
|
|
1013
1015
|
|
|
@@ -1019,6 +1021,7 @@ export default {
|
|
|
1019
1021
|
| `ceil(n?)` | 切り上げ | `value\|ceil` |
|
|
1020
1022
|
| `locale(loc?)` | ロケール数値フォーマット | `count\|locale` / `count\|locale(ja-JP)` |
|
|
1021
1023
|
| `percent(n?)` | パーセンテージフォーマット | `ratio\|percent(1)` |
|
|
1024
|
+
| `unit(u)` | 単位(任意の接尾辞)を付加 | `width\|unit(px)` → `"40px"` |
|
|
1022
1025
|
|
|
1023
1026
|
### 文字列
|
|
1024
1027
|
|
|
@@ -1033,6 +1036,8 @@ export default {
|
|
|
1033
1036
|
| `pad(n, char?)` | 先頭パディング | `id\|pad(5,0)` → `"00001"` |
|
|
1034
1037
|
| `rep(n)` | 繰り返し | `text\|rep(3)` |
|
|
1035
1038
|
| `rev` | 反転 | `text\|rev` |
|
|
1039
|
+
| `truncate(n, suffix?)` | 切り詰めて省略記号を付加 | `title\|truncate(20)` → `"…"` 付き |
|
|
1040
|
+
| `join(sep?)` | 配列を連結(既定 `", "`) | `tags\|join` / `tags\|join(/)` |
|
|
1036
1041
|
|
|
1037
1042
|
### 型変換
|
|
1038
1043
|
|
|
@@ -1053,6 +1058,7 @@ export default {
|
|
|
1053
1058
|
| `time(loc?)` | 時刻フォーマット | `timestamp\|time` |
|
|
1054
1059
|
| `datetime(loc?)` | 日付 + 時刻 | `timestamp\|datetime(en-US)` |
|
|
1055
1060
|
| `ymd(sep?)` | YYYY-MM-DD | `timestamp\|ymd` / `timestamp\|ymd(/)` |
|
|
1061
|
+
| `hms(sep?)` | HH:MM:SS | `timestamp\|hms` / `timestamp\|hms(-)` |
|
|
1056
1062
|
|
|
1057
1063
|
### 真偽値 / デフォルト
|
|
1058
1064
|
|
|
@@ -1125,6 +1131,18 @@ customElements.define("my-light-component", MyLightComponent);
|
|
|
1125
1131
|
- バインディングでは `@my-light` のように状態名を明示的に参照する必要があります
|
|
1126
1132
|
- `<wcs-state>` はコンポーネント要素の直下に配置する必要があります
|
|
1127
1133
|
|
|
1134
|
+
- ホスト側からのバインド(`<my-light-component data-wcs="state.message: user.name">`)も
|
|
1135
|
+
Shadow DOM 形と同様に使えます。コンポーネントのサブツリーは**独立したバインディングスコープ**
|
|
1136
|
+
として扱われ、コンポーネント側の状態が名前登録を終えてから配線されます
|
|
1137
|
+
|
|
1138
|
+
> **注意**: Light DOM は名前空間を上位スコープと共有するため、**同じ `name` を持つインスタンスを
|
|
1139
|
+
> 同一スコープに複数置くことはできません**。リストの行ごとにコンポーネントを配置するような形は
|
|
1140
|
+
> Shadow DOM を使ってください。
|
|
1141
|
+
>
|
|
1142
|
+
> また `State.getBindingsReady(root)` はコンポーネントのスコープを含みません(Shadow DOM 形で
|
|
1143
|
+
> 子が別 rootNode にあるのと同じ扱いです)。コンポーネント内部の描画完了まで待ちたい場合は、
|
|
1144
|
+
> コンポーネント側の `<wcs-state>` の初期化を待ってください。
|
|
1145
|
+
|
|
1128
1146
|
### ホスト側の使用方法
|
|
1129
1147
|
|
|
1130
1148
|
```html
|
|
@@ -1218,8 +1236,20 @@ this.shadowRoot.innerHTML = `
|
|
|
1218
1236
|
|
|
1219
1237
|
- ホストが `rows` を差し替えても、行フィールド(`rows.0.name`)だけを書いても、コンポーネント内の行に反映されます
|
|
1220
1238
|
- コンポーネント側から `items.*.name` を書き戻すと、ホストの `rows` に届きます
|
|
1221
|
-
|
|
1222
|
-
|
|
1239
|
+
|
|
1240
|
+
#### 入れ子とスコープの重ね方
|
|
1241
|
+
|
|
1242
|
+
コンポーネント自身がホスト側の `for` の中にあり、**さらにコンポーネント内でも `for` を回す**入れ子構成にも対応しています。外側の行と内側の行の対応はフレームワークが保ちます:
|
|
1243
|
+
|
|
1244
|
+
```html
|
|
1245
|
+
<template data-wcs="for: groups">
|
|
1246
|
+
<my-list data-wcs="state.items: groups.*.children"></my-list>
|
|
1247
|
+
</template>
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
コンポーネントの中にさらにコンポーネントを置いて、**スコープを重ねる**こともできます。途中のコンポーネントが配列を素通しするだけで自分では `for` を回さなくても、正本スコープ起点の行フィールド書き込みは最下層の行まで届きます。
|
|
1251
|
+
|
|
1252
|
+
コンポーネントの作者が自分の置かれる深さを意識する必要はありません。`$1` / イベントハンドラのインデックス / `$updatedCallback` / `$getAll` はいずれも**自分のスコープ内の位置**を報告します。
|
|
1223
1253
|
|
|
1224
1254
|
## Command Token(メソッドバインディング)
|
|
1225
1255
|
|
|
@@ -1520,7 +1550,7 @@ event token は command token と同じ `Token` pub/sub プリミティブを共
|
|
|
1520
1550
|
|
|
1521
1551
|
command token / event token が運ぶのは離散的なやり取りです。**`$streams`** は残る形 —— 連続的なフローをカバーします。非同期 producer(async iterable / async generator / `ReadableStream`)を宣言すると、フレームワークがそれを **fold して単一の reactive プロパティに畳み込みます** —— 各チャンクは通常のパス代入を通るため、バインディング・パス getter・`$updatedCallback` は自分で値を代入した場合とまったく同じように反応します。`args` 関数が読んだ state パスが変化すると、実行中の producer は abort され、新しい引数で source が張り直されます(switchMap 型の依存駆動 restart)。stream は `$connectedCallback` 完了後に eager に起動し、要素の disconnect で abort されます。
|
|
1522
1552
|
|
|
1523
|
-
`$updatedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path
|
|
1553
|
+
`$updatedCallback` は引き続き binding 駆動です。stream 宣言だけでは headless な購読にならず、その value/status/error の live DOM binding が実際に適用されたときだけ callback の path に現れます。描画せずに stream の値へ反応したい場合は、そのパスに [`$watch`](#watch-watch) を宣言してください。観測契約は [stream リファレンス](docs/streams.md) を参照してください。
|
|
1524
1554
|
|
|
1525
1555
|
```html
|
|
1526
1556
|
<wcs-state>
|
|
@@ -1597,6 +1627,68 @@ $streams: {
|
|
|
1597
1627
|
|
|
1598
1628
|
完全な契約 —— ライフサイクルと所有権・restart セマンティクス・flush 粒度・スコープ外リスト —— は [docs/streams.ja.md](docs/streams.ja.md) を参照してください。
|
|
1599
1629
|
|
|
1630
|
+
## Watch(`$watch`)
|
|
1631
|
+
|
|
1632
|
+
`$updatedCallback` は **binding 駆動** です。その更新で live DOM binding が実際に適用された path だけを報告するため、**描画していない値の変化は見えません**。**`$watch`** はその headless 版で、ページ上でそのパスがバインドされているかどうかに関わらず、state の変化で発火します(**ワイルドカードの行パスだけは例外**で、headless に成立させるには `$listKeys` が要ります。後述)。
|
|
1633
|
+
|
|
1634
|
+
```html
|
|
1635
|
+
<wcs-state>
|
|
1636
|
+
<script type="module">
|
|
1637
|
+
export default {
|
|
1638
|
+
isLoading: false,
|
|
1639
|
+
items: [],
|
|
1640
|
+
startedAt: 0,
|
|
1641
|
+
|
|
1642
|
+
$watch: {
|
|
1643
|
+
// 立ち上がり検出は cur/prev を自分で比較する
|
|
1644
|
+
isLoading(cur, prev) {
|
|
1645
|
+
if (cur === true && prev === false) { this.startedAt = Date.now(); }
|
|
1646
|
+
},
|
|
1647
|
+
|
|
1648
|
+
// ワイルドカードパスは変化した行ごとに 1 回発火する
|
|
1649
|
+
//(リストを `for` で描画しているか、`$listKeys` の宣言が要る。後述)
|
|
1650
|
+
"items.*.price"(cur, prev, index) {
|
|
1651
|
+
this.lastPriceChange = `#${index}: ${prev} → ${cur}`;
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
};
|
|
1655
|
+
</script>
|
|
1656
|
+
</wcs-state>
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
ハンドラの `this` は **writable** な state proxy なので書き戻せます。その書き込みは次の更新バッチに乗ります。戻り値は無視され、await もされません。
|
|
1660
|
+
|
|
1661
|
+
| 引数 | 契約 |
|
|
1662
|
+
|---|---|
|
|
1663
|
+
| `cur` | drain 時点の値(そのバッチの確定値) |
|
|
1664
|
+
| `prev` | **バッチ開始時点**の値(first-write-wins)。意味を持つのは**スカラのときだけ**(下記) |
|
|
1665
|
+
| `...indexes` | ワイルドカードパスのときのみ。そのスコープ自身のループ添字(`$1` / `$2` と同じ規約) |
|
|
1666
|
+
|
|
1667
|
+
**`prev` はスカラ限定です。** same-value guard が既に読んでいる旧値を再利用するため watch のための追加読みは発生せず、その帰結として参照型(in-place 変異では同じ参照になるため)・`$postUpdate` 経由・`config.sameValueGuard` オフのときは `undefined` になります。
|
|
1668
|
+
|
|
1669
|
+
**`$watch` は独自の発火条件を持ちません。** 更新バッチに載ったものをそのまま発火します。これはうまく噛み合っていて、同値の primitive 書き込みは enqueue 前に落ちている(=実質的に変化時のみ発火)一方、occurrence(`semantics: "event"` の property)は**意図的に**落とされないので `cur === prev` で発火します。エッジ検出が要るならハンドラ内で `cur` と `prev` を比較してください。
|
|
1670
|
+
|
|
1671
|
+
**getter を watch すると eager になります。** computed は本来 lazy で、依存は評価時にしか記録されません —— つまり描画していない getter は本来一度も発火しません。`$watch` に宣言すると接続時に 1 回評価され、以後は依存に触れたバッチの終端で毎回評価されます。`prev` は前回の評価値です。重い computed を watch すればその評価コストが毎バッチ乗り、getter 内の例外は watch 経由で表面化します。ワイルドカード getter(`items.*.tax`)は eager 化**しません**(初回評価がリスト全体を舐めることになるため)。この形は DOM にバインドされている場合にのみ発火し、`prev` は常に `undefined` です(行ごとの評価値を保持しないため)。
|
|
1672
|
+
|
|
1673
|
+
発火順序は 3 層に分かれ、利用者が意思を持てるのは真ん中の層だけです。
|
|
1674
|
+
|
|
1675
|
+
| 層 | 順序 | 制御 |
|
|
1676
|
+
|---|---|---|
|
|
1677
|
+
| 機構間 | `$updatedCallback` → `$watch` → `$streams` restart | 固定 |
|
|
1678
|
+
| ハンドラ間 | `$watch` の宣言順 | **宣言を並べ替える** |
|
|
1679
|
+
| 同一パスの行間 | `indexes` 昇順 | 固定 |
|
|
1680
|
+
|
|
1681
|
+
主なルール:
|
|
1682
|
+
|
|
1683
|
+
- **自 state のみ** —— パスに `@stateName` は書けません。他の state 要素の watch は宣言時に拒否されます。
|
|
1684
|
+
- **中間値は観測できません** —— 1 バッチ内の `a → b → c` は `cur = c` / `prev = a` で 1 回だけ発火します(binding 更新と同じ契約)。
|
|
1685
|
+
- **行単位の差分を見たいなら `$listKeys`** —— 未宣言のまま配列全体を代入すると、行 watch は**全行**について `prev === undefined` で発火します(どの行もパス書き込みを通っていないため)。`$listKeys` を宣言すればキー突合が per-field 書き込みに分解するので、変化した行だけが発火し `prev` もスカラで取れます。
|
|
1686
|
+
- **headless な行 watch には `$listKeys` が必要** —— `$watch` が単独では headless にならない唯一の箇所です。`items` から `items.*.price` への展開はリストの `for` バインディングが駆動しており、watch を宣言してもそのパスをリストとしては登録しません(意図的)。したがって `for` バインドも `$listKeys` も無い状態で配列を代入すると、行 watch は**一度も**発火しません。`$listKeys` を宣言する(キー突合がフィールドごとにパス書き込みするので展開を経由しない)か、リストを描画してください。スカラーパスは `user.name` のようなネストしたものも含め、この条件なしに headless で発火します。
|
|
1687
|
+
- **ハンドラの例外は隔離されます** —— throw はコンソールに報告され、残りの watch(と stream の restart)は続行します。loud fail する `$connectedCallback` / `$updatedCallback` とは異なる扱いです。
|
|
1688
|
+
- **書き込みの連鎖には上限があります** —— ハンドラの書き込みは新しいバッチを作るため、相互に書き合う watch は無限ループになり得ます。32 段で打ち切り、コンソールに報告します(値と DOM は巻き戻しません)。
|
|
1689
|
+
- **mapped な `bind-component` の子では使えません** —— 子の state は `$` 始まりのプロパティを遮る proxy に包まれるため、宣言が届きません(`$streams` も同様)。plain な(マップされていない)子では宣言できます。
|
|
1690
|
+
- **SSR では実行されません** —— ハンドラの副作用がサーバーとクライアントで二重に走るためです。
|
|
1691
|
+
|
|
1600
1692
|
## Inputs と属性ミラー
|
|
1601
1693
|
|
|
1602
1694
|
`wcBindable.inputs` は一方向のプロパティ入力(state → 要素)を宣言します。エントリに `attribute` を設定すると、フレームワークはプロパティを書き込むたびにその値を当該 HTML 属性へも書き込むため、`attributeChangedCallback`・CSS の属性セレクタ・DevTools がすべてプロパティ値と同期し続けます。
|
|
@@ -1707,7 +1799,7 @@ JavaScript のクラス定義なしで、**HTML だけ**でカスタム要素を
|
|
|
1707
1799
|
|
|
1708
1800
|
### `$bindables` / `$commands` と wc-bindable プロトコル
|
|
1709
1801
|
|
|
1710
|
-
`$bindables` は変更イベント付きのコンポーネント**プロパティ**として公開する状態プロパティを、`$commands` は起動可能な**コマンド**として公開する状態メソッドを宣言します。この 2 つで [wc-bindable プロトコル](https://github.com/
|
|
1802
|
+
`$bindables` は変更イベント付きのコンポーネント**プロパティ**として公開する状態プロパティを、`$commands` は起動可能な**コマンド**として公開する状態メソッドを宣言します。この 2 つで [wc-bindable プロトコル](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/README.md)の宣言が組み立てられます:
|
|
1711
1803
|
|
|
1712
1804
|
```javascript
|
|
1713
1805
|
export default {
|
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ That's it. No build, no bootstrap code, no framework.
|
|
|
95
95
|
- **Declarative data binding** — `data-wcs` attribute for property / text / event / structural binding
|
|
96
96
|
- **Reactive Proxy** — ES Proxy-based automatic DOM updates with dependency tracking
|
|
97
97
|
- **Structural directives** — `for`, `if` / `elseif` / `else` via `<template>` elements
|
|
98
|
-
- **Built-in filters** —
|
|
98
|
+
- **Built-in filters** — 46 filters for formatting, comparison, arithmetic, date, and more
|
|
99
99
|
- **Two-way binding** — automatic for `<input>`, `<select>`, `<textarea>`
|
|
100
100
|
- **Web Component binding** — bidirectional state binding with Shadow DOM components
|
|
101
101
|
- **Command tokens** — invoke methods on wc-bindable custom elements from state via a pub/sub channel (`command.<method>: tokenName`)
|
|
@@ -443,7 +443,7 @@ Internally converted to comment-based bindings (`<!--@@:expression-->`).
|
|
|
443
443
|
|
|
444
444
|
### Spread Binding (`...`)
|
|
445
445
|
|
|
446
|
-
For custom elements that declare the [`wc-bindable` protocol](#
|
|
446
|
+
For custom elements that declare the [`wc-bindable` protocol](#bindables--commands-and-the-wc-bindable-protocol), `...: target` wires all of the element's **properties + inputs** to a single state object in one line:
|
|
447
447
|
|
|
448
448
|
```html
|
|
449
449
|
<wcs-fetch data-wcs="...: usersFetch"></wcs-fetch>
|
|
@@ -986,7 +986,7 @@ export default {
|
|
|
986
986
|
|
|
987
987
|
## Filters
|
|
988
988
|
|
|
989
|
-
|
|
989
|
+
46 built-in filters are available for both input (DOM → state) and output (state → DOM) directions.
|
|
990
990
|
|
|
991
991
|
### Comparison
|
|
992
992
|
|
|
@@ -1009,6 +1009,8 @@ export default {
|
|
|
1009
1009
|
| `mul(n)` | Multiply | `price\|mul(1.1)` |
|
|
1010
1010
|
| `div(n)` | Divide | `total\|div(100)` |
|
|
1011
1011
|
| `mod(n)` | Modulo | `index\|mod(2)` |
|
|
1012
|
+
| `abs` | Absolute value | `delta\|abs` |
|
|
1013
|
+
| `clamp(min, max)` | Constrain to a range | `ratio\|clamp(0,100)` |
|
|
1012
1014
|
|
|
1013
1015
|
### Number Formatting
|
|
1014
1016
|
|
|
@@ -1020,6 +1022,7 @@ export default {
|
|
|
1020
1022
|
| `ceil(n?)` | Ceiling | `value\|ceil` |
|
|
1021
1023
|
| `locale(loc?)` | Locale number format | `count\|locale` / `count\|locale(ja-JP)` |
|
|
1022
1024
|
| `percent(n?)` | Percentage format | `ratio\|percent(1)` |
|
|
1025
|
+
| `unit(u)` | Append a unit (any suffix) | `width\|unit(px)` → `"40px"` |
|
|
1023
1026
|
|
|
1024
1027
|
### String
|
|
1025
1028
|
|
|
@@ -1034,6 +1037,8 @@ export default {
|
|
|
1034
1037
|
| `pad(n, char?)` | Pad start | `id\|pad(5,0)` → `"00001"` |
|
|
1035
1038
|
| `rep(n)` | Repeat | `text\|rep(3)` |
|
|
1036
1039
|
| `rev` | Reverse | `text\|rev` |
|
|
1040
|
+
| `truncate(n, suffix?)` | Shorten and append an ellipsis | `title\|truncate(20)` |
|
|
1041
|
+
| `join(sep?)` | Join an array (default `", "`) | `tags\|join` / `tags\|join(/)` |
|
|
1037
1042
|
|
|
1038
1043
|
### Type Conversion
|
|
1039
1044
|
|
|
@@ -1054,6 +1059,7 @@ export default {
|
|
|
1054
1059
|
| `time(loc?)` | Time format | `timestamp\|time` |
|
|
1055
1060
|
| `datetime(loc?)` | Date + Time | `timestamp\|datetime(en-US)` |
|
|
1056
1061
|
| `ymd(sep?)` | YYYY-MM-DD | `timestamp\|ymd` / `timestamp\|ymd(/)` |
|
|
1062
|
+
| `hms(sep?)` | HH:MM:SS | `timestamp\|hms` / `timestamp\|hms(-)` |
|
|
1057
1063
|
|
|
1058
1064
|
### Boolean / Default
|
|
1059
1065
|
|
|
@@ -1126,6 +1132,18 @@ customElements.define("my-light-component", MyLightComponent);
|
|
|
1126
1132
|
- Bindings must explicitly reference the state name with `@my-light`
|
|
1127
1133
|
- `<wcs-state>` must be a direct child of the component element
|
|
1128
1134
|
|
|
1135
|
+
- Binding from the host (`<my-light-component data-wcs="state.message: user.name">`) works just as it
|
|
1136
|
+
does for Shadow DOM. The component's subtree is treated as an **independent binding scope** and is
|
|
1137
|
+
wired once the component's own state has registered its name
|
|
1138
|
+
|
|
1139
|
+
> **Note**: Light DOM shares its namespace with the parent scope, so **two instances carrying the same
|
|
1140
|
+
> `name` cannot live in one scope**. Use Shadow DOM for shapes that place a component on every row of
|
|
1141
|
+
> a list.
|
|
1142
|
+
>
|
|
1143
|
+
> Also, `State.getBindingsReady(root)` does not cover the component's scope — the same as the Shadow
|
|
1144
|
+
> DOM form, where the child lives in a different rootNode. Await the component's own `<wcs-state>`
|
|
1145
|
+
> initialization when you need to wait for its contents to render.
|
|
1146
|
+
|
|
1129
1147
|
### Host Usage
|
|
1130
1148
|
|
|
1131
1149
|
```html
|
|
@@ -1219,8 +1237,24 @@ this.shadowRoot.innerHTML = `
|
|
|
1219
1237
|
|
|
1220
1238
|
- Replacing `rows` or writing a single row field (`rows.0.name`) both reach the rows inside the component
|
|
1221
1239
|
- Writing `items.*.name` from inside the component reaches the host's `rows`
|
|
1222
|
-
|
|
1223
|
-
|
|
1240
|
+
|
|
1241
|
+
#### Nesting and stacking scopes
|
|
1242
|
+
|
|
1243
|
+
A component that sits inside a host `for:` *and* runs its own `for:` over the array it was handed
|
|
1244
|
+
is supported. The framework keeps the outer row and the inner row related:
|
|
1245
|
+
|
|
1246
|
+
```html
|
|
1247
|
+
<template data-wcs="for: groups">
|
|
1248
|
+
<my-list data-wcs="state.items: groups.*.children"></my-list>
|
|
1249
|
+
</template>
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
Components can also be placed inside components, **stacking scopes**. An intermediate component
|
|
1253
|
+
that only passes the array through — running no `for:` of its own — still lets a row-field write
|
|
1254
|
+
from the owning scope reach the rows at the bottom.
|
|
1255
|
+
|
|
1256
|
+
A component's author never has to know how deeply it is placed. `$1`, event-handler indexes,
|
|
1257
|
+
`$updatedCallback` and `$getAll` all report positions **within the component's own scope**.
|
|
1224
1258
|
|
|
1225
1259
|
## Command Token (Method Binding)
|
|
1226
1260
|
|
|
@@ -1521,7 +1555,7 @@ Event tokens share the same `Token` pub/sub primitive as command tokens — `nam
|
|
|
1521
1555
|
|
|
1522
1556
|
Command tokens and event tokens carry discrete interactions. **`$streams`** covers the remaining shape: a continuous flow. Declare an async producer (async iterable / async generator / `ReadableStream`) and the framework **folds it into a single reactive property** — each chunk goes through normal path assignment, so bindings, path getters, and `$updatedCallback` react exactly as if you had assigned the value yourself. When a state path read by the `args` function changes, the running producer is aborted and the source is restarted with the new arguments (switchMap-style dependency-driven restart). Streams start eagerly after `$connectedCallback` completes and are aborted when the element disconnects.
|
|
1523
1557
|
|
|
1524
|
-
`$updatedCallback` remains binding-driven: a stream declaration alone is not a headless subscription. Its path appears in the callback only when a live DOM binding for that value/status/error is actually applied.
|
|
1558
|
+
`$updatedCallback` remains binding-driven: a stream declaration alone is not a headless subscription. Its path appears in the callback only when a live DOM binding for that value/status/error is actually applied. To react to a stream's value without rendering it, declare [`$watch`](#watch-watch) on that path; see the [stream reference](docs/streams.md) for the observation contract.
|
|
1525
1559
|
|
|
1526
1560
|
```html
|
|
1527
1561
|
<wcs-state>
|
|
@@ -1598,6 +1632,68 @@ Key rules:
|
|
|
1598
1632
|
|
|
1599
1633
|
See [docs/streams.md](docs/streams.md) for the full contract — lifecycle and ownership, restart semantics, flush granularity, and the out-of-scope list.
|
|
1600
1634
|
|
|
1635
|
+
## Watch (`$watch`)
|
|
1636
|
+
|
|
1637
|
+
`$updatedCallback` is **binding-driven**: it reports the paths whose live DOM bindings were actually applied in that update, so a value you never render is invisible to it. **`$watch`** is the headless counterpart — it fires on state changes whether or not anything on the page is bound to the path. (One exception, spelled out below: a *wildcard* row path needs `$listKeys` to work headlessly.)
|
|
1638
|
+
|
|
1639
|
+
```html
|
|
1640
|
+
<wcs-state>
|
|
1641
|
+
<script type="module">
|
|
1642
|
+
export default {
|
|
1643
|
+
isLoading: false,
|
|
1644
|
+
items: [],
|
|
1645
|
+
startedAt: 0,
|
|
1646
|
+
|
|
1647
|
+
$watch: {
|
|
1648
|
+
// rising-edge detection: you compare cur/prev yourself
|
|
1649
|
+
isLoading(cur, prev) {
|
|
1650
|
+
if (cur === true && prev === false) { this.startedAt = Date.now(); }
|
|
1651
|
+
},
|
|
1652
|
+
|
|
1653
|
+
// wildcard paths fire once per changed row
|
|
1654
|
+
// (needs the list rendered with `for`, or `$listKeys` declared — see below)
|
|
1655
|
+
"items.*.price"(cur, prev, index) {
|
|
1656
|
+
this.lastPriceChange = `#${index}: ${prev} → ${cur}`;
|
|
1657
|
+
},
|
|
1658
|
+
},
|
|
1659
|
+
};
|
|
1660
|
+
</script>
|
|
1661
|
+
</wcs-state>
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
The handler runs with `this` bound to a **writable** state proxy, so it can write back; those writes land in the next update batch. The return value is ignored and never awaited.
|
|
1665
|
+
|
|
1666
|
+
| Argument | Contract |
|
|
1667
|
+
|---|---|
|
|
1668
|
+
| `cur` | The value at drain time (the settled value for the batch) |
|
|
1669
|
+
| `prev` | The value at the **start of the batch** (first-write-wins). Meaningful **for scalars only** — see below |
|
|
1670
|
+
| `...indexes` | Only for wildcard paths: this scope's own loop indexes, same convention as `$1`, `$2` |
|
|
1671
|
+
|
|
1672
|
+
**`prev` is scalar-only.** It reuses the old value the same-value guard already reads, so watch costs no extra read — and it is `undefined` for reference types (an in-place mutation would give you the same reference anyway), for `$postUpdate`, and when `config.sameValueGuard` is off.
|
|
1673
|
+
|
|
1674
|
+
**Watch adds no firing condition of its own.** It fires for whatever landed in the update batch. That falls out well: an equal primitive write is already dropped before it is enqueued (so you effectively get change-only firing), while an occurrence write — a `semantics: "event"` property — is deliberately *not* dropped, and still fires with `cur === prev`. If you need edge detection, compare `cur` and `prev` in the handler.
|
|
1675
|
+
|
|
1676
|
+
**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 and again at the end of every batch that touches its dependencies. Its `prev` is the previous evaluation. Watch a heavy computed and you pay that evaluation on every batch; exceptions inside it surface through the watch instead of staying dormant. Wildcard getters (`items.*.tax`) are **not** made eager — priming one would sweep the whole list — so that form fires only when it is also bound to the DOM, and its `prev` is always `undefined` (no per-row evaluation is remembered).
|
|
1677
|
+
|
|
1678
|
+
Firing order is defined in three layers, and only the middle one is yours to steer:
|
|
1679
|
+
|
|
1680
|
+
| Layer | Order | Your control |
|
|
1681
|
+
|---|---|---|
|
|
1682
|
+
| Mechanisms | `$updatedCallback` → `$watch` → `$streams` restart | fixed |
|
|
1683
|
+
| Between handlers | declaration order in `$watch` | **reorder the declarations** |
|
|
1684
|
+
| Between rows of one path | ascending `indexes` | fixed |
|
|
1685
|
+
|
|
1686
|
+
Key rules:
|
|
1687
|
+
|
|
1688
|
+
- **Only its own state** — a path may not carry `@stateName`; watching another state element is rejected at declaration time.
|
|
1689
|
+
- **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.
|
|
1690
|
+
- **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.
|
|
1691
|
+
- **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.
|
|
1692
|
+
- **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.
|
|
1693
|
+
- **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.
|
|
1694
|
+
- **Not available on a mapped `bind-component` child** — its state is wrapped in a proxy that blanks out every `$`-prefixed property, so the declaration never arrives. This applies to `$streams` too. A plain (unmapped) child can declare it.
|
|
1695
|
+
- **SSR does not run watches** — handler side effects would otherwise execute on both server and client.
|
|
1696
|
+
|
|
1601
1697
|
## Inputs and Attribute Mirror
|
|
1602
1698
|
|
|
1603
1699
|
`wcBindable.inputs` declares one-way property inputs (state → element). When an entry sets `attribute`, the framework writes the value to that HTML attribute every time it writes the property, so `attributeChangedCallback`, CSS attribute selectors, and DevTools all stay in sync with the property value.
|
|
@@ -1708,7 +1804,7 @@ The definition element is hidden; each instance clones the template into its own
|
|
|
1708
1804
|
|
|
1709
1805
|
### `$bindables` / `$commands` and the wc-bindable Protocol
|
|
1710
1806
|
|
|
1711
|
-
`$bindables` declares which state **properties** are exposed as component properties with change events. `$commands` declares which state **methods** are exposed as invocable commands. Together they build the [wc-bindable protocol](https://github.com/
|
|
1807
|
+
`$bindables` declares which state **properties** are exposed as component properties with change events. `$commands` declares which state **methods** are exposed as invocable commands. Together they build the [wc-bindable protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/README.md) declaration:
|
|
1712
1808
|
|
|
1713
1809
|
```javascript
|
|
1714
1810
|
export default {
|