@wcstack/state 1.11.1 → 1.12.1

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 CHANGED
@@ -16,7 +16,7 @@
16
16
  - selector
17
17
  - reactive primitive をコンポーネントへ引き込むための glue code
18
18
 
19
- None of these exist by design.
19
+ これらはどれも、設計上存在しません。
20
20
 
21
21
  なぜなら、このライブラリでは UI と状態の結合点を JavaScript の中に置かないからです。状態を「取り出して」コンポーネントへ渡すのではなく、HTML 側がパス文字列によって状態を参照します。要素は状態を所有せず、状態も要素を知りません。両者が共有するのはパスだけです。
22
22
 
@@ -96,6 +96,8 @@ None of these exist by design.
96
96
  - **組み込みフィルタ** — フォーマット、比較、算術、日付など 40 種類
97
97
  - **双方向バインディング** — `<input>`, `<select>`, `<textarea>` で自動有効
98
98
  - **Web Component バインディング** — Shadow DOM コンポーネントとの双方向状態バインディング
99
+ - **command token** — pub/sub チャネル(`command.<method>: tokenName`)で state から wc-bindable カスタム要素のメソッドを起動
100
+ - **event token** — command token の双対。wc-bindable 要素が dispatch するイベントを `eventToken.<prop>: tokenName` + `$on` マップで state が受信
99
101
  - **パス getter** — ドットパスキー getter(`get "users.*.fullName"()`)によるデータツリーの任意の深さへのフラットな仮想プロパティ定義、自動依存追跡・キャッシュ
100
102
  - **Mustache 構文** — テキストノードでの `{{ path|filter }}`
101
103
  - **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
@@ -419,6 +421,8 @@ export default {
419
421
  <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
420
422
  ```
421
423
 
424
+ **`undefined` は「無意見」** — 展開された state パスが `undefined` に解決される場合(slot オブジェクトでその input を初期化していない場合など)、プロパティ書き込みは**スキップ**され、要素側の既定値がそのまま生きます。実際に使うパスだけ初期化すれば十分で、`<wcs-fetch>` が `method` / `manual` / `body` を宣言していても `usersFetch: { value: null, loading: false }` だけで動きます。明示的にクリアしたい場合は `null` を代入してください(`null` は常に書き込まれます)。このスキップは spread に限らずすべてのプロパティバインディングに適用され、`config.debug` 時はスキップごとに `console.debug` でログが出ます。
425
+
422
426
  **制約事項**:
423
427
 
424
428
  - spread 右辺へのフィルタ(`...: target|filter`)はエラー
@@ -1245,6 +1249,150 @@ command token は state コードから emit する必要はありません。DO
1245
1249
  <my-list data-wcs="command.reset: $command.reset"></my-list>
1246
1250
  ```
1247
1251
 
1252
+ ## Event Token(イベントバインディング)
1253
+
1254
+ command token はコンポーネントへ *押し込み* ます(state がメソッドを起動)。**event token** はその正確な双対 —— コンポーネントから *引き出し* ます(要素がイベントを dispatch し、state が受信)。両者で要素 ↔ state 境界の双方向をカバーし、どちらの側も相手への参照を一切持ちません。共有されるのは token のみです。
1255
+
1256
+ | Token | 方向 | 購読者 | emit する側 |
1257
+ |---|---|---|---|
1258
+ | **command token** | state → 要素 | 要素(`command.<method>:`) | state(`$command.<name>.emit`) |
1259
+ | **event token** | 要素 → state | state(`$on`) | 要素(DOM イベントリスナー) |
1260
+
1261
+ - 要素側は wc-bindable カスタム要素に `eventToken.<property>: <tokenName>` を配線する
1262
+ - state 側は `$eventTokens` でチャネルを宣言し、`$on` マップで受信する
1263
+ - 購読者は `(state, event, ...listIndexes)` で呼び出される —— command token の emit 規約と対称
1264
+
1265
+ ### 基本的な使い方
1266
+
1267
+ ```html
1268
+ <wcs-state>
1269
+ <script type="module">
1270
+ export default {
1271
+ users: [],
1272
+ error: null,
1273
+
1274
+ $eventTokens: ["userCreated", "createFailed"],
1275
+ $on: {
1276
+ userCreated(state, event) {
1277
+ state.users = state.users.concat(event.detail);
1278
+ },
1279
+ createFailed(state, event) {
1280
+ state.error = event.detail;
1281
+ }
1282
+ }
1283
+ };
1284
+ </script>
1285
+ </wcs-state>
1286
+
1287
+ <!-- emitter — wc-bindable なカスタム要素であること -->
1288
+ <my-form data-wcs="eventToken.created: userCreated; eventToken.error: createFailed"></my-form>
1289
+ ```
1290
+
1291
+ `<my-form>` が自身の `created` プロパティに対応する DOM イベントを dispatch すると、`userCreated` token が発火し、`$on.userCreated` ハンドラが `(state, event)` で実行されます。
1292
+
1293
+ ### `$eventTokens` 宣言
1294
+
1295
+ `$eventTokens` 配列は、`eventToken.<prop>:` バインディングと `$on` キーが参照できるチャネル名を宣言します。宣言された名前のみが有効です(typo 耐性)。
1296
+
1297
+ ```javascript
1298
+ export default {
1299
+ $eventTokens: ["userCreated", "createFailed"],
1300
+ };
1301
+ ```
1302
+
1303
+ - エントリは空でない文字列であること
1304
+ - 重複するエントリは初期化時にエラーになる
1305
+ - ここで宣言されたが `$on` に無い token は購読者ゼロ —— emit しても no-op
1306
+
1307
+ ### `$on` —— state 側での受信
1308
+
1309
+ `$on` は各 event-token 名をハンドラに対応づけます。state は **第1引数** として渡される(`this` ではない)ため、ハンドラはメソッド省略記法でもアロー関数でも書けます —— `this` を束縛しない点は command token の emit 規約と同じです:
1310
+
1311
+ ```javascript
1312
+ $on: {
1313
+ // どちらの形式でも可 —— state は常に第1引数
1314
+ userCreated: (state, event) => { state.lastId = event.detail.id; },
1315
+ rowFailed(state, event, ...listIndexes) {
1316
+ const [i] = listIndexes; // `for` 内から発火した場合のループインデックス
1317
+ state.failedRows = state.failedRows.concat(i);
1318
+ }
1319
+ }
1320
+ ```
1321
+
1322
+ - `$on` のすべてのキーは `$eventTokens` で宣言済みであること(さもなくば初期化時に throw)
1323
+ - 各値は関数であること
1324
+ - シグネチャは `(state, event, ...listIndexes)` —— まず DOM の `Event`、続いて内包するループインデックス
1325
+
1326
+ ### `eventToken.<property>:` バインディング
1327
+
1328
+ ```html
1329
+ <my-target data-wcs="eventToken.error: createFailed"></my-target>
1330
+ ```
1331
+
1332
+ | 部位 | 説明 |
1333
+ |---|---|
1334
+ | `eventToken.` | 固定の prefix |
1335
+ | `<property>` | **wcBindable プロパティ名** —— 生の DOM イベント名ではない。実イベント名は `wcBindable.properties[].event` から解決される |
1336
+ | `<tokenName>` | `$eventTokens` で宣言されたベアな event-token 名(command token と違い `$` 名前空間 prefix は付けない) |
1337
+
1338
+ キーを生イベント名ではなくプロパティ名にすることで、command バインディングと同じ `wcBindable` 契約を経由でき、namespaced なイベント名(`ns:evt`)がバインディングの `:` 区切りと衝突しません。フレームワークは `properties[].event` を引いてその実イベントのリスナーを attach します:
1339
+
1340
+ ```javascript
1341
+ class MyTarget extends HTMLElement {
1342
+ static wcBindable = {
1343
+ protocol: "wc-bindable", version: 1,
1344
+ properties: [
1345
+ { name: "error", event: "thing-error" }, // eventToken.error → "thing-error" を listen
1346
+ { name: "created", event: "thing-created" },
1347
+ ],
1348
+ };
1349
+ }
1350
+ ```
1351
+
1352
+ 検証ルール:
1353
+
1354
+ - 要素は wc-bindable なカスタム要素であること(`static wcBindable`・`protocol: "wc-bindable"`・`version: 1`)。非 wc-bindable 要素は attach 時に拒否される。
1355
+ - `<property>` は `wcBindable.properties` に現れること —— **attach 時** に検証(fail-fast。クラス参照のみで足り、DOM 接続に非依存)。
1356
+ - `<tokenName>` は `$eventTokens` で宣言されていること —— **発火時** に検証。state はイベント発火時に要素の live root から解決されるため、attach 時にノードが detached になりうる `for` / `if` ブロック内や SSR ハイドレーション後でも機能する。
1357
+ - 修飾子 `#prevent` / `#stop` は通常のイベントバインディングと同様に機能する: `eventToken.error#prevent: createFailed`。
1358
+
1359
+ ### ループ内での使用
1360
+
1361
+ emitter が `for` ブロック内にある場合、`on*` ハンドラと同じく、内包するループインデックスがイベントの後ろに付与されます:
1362
+
1363
+ ```html
1364
+ <template data-wcs="for: rows">
1365
+ <my-row data-wcs="eventToken.failed: rowFailed"></my-row>
1366
+ </template>
1367
+ ```
1368
+
1369
+ ```javascript
1370
+ $on: {
1371
+ rowFailed(state, event, ...listIndexes) {
1372
+ const [i] = listIndexes; // 発火した行のインデックス
1373
+ state.failedRows = state.failedRows.concat(i);
1374
+ }
1375
+ }
1376
+ ```
1377
+
1378
+ ### ファンインとチェイン
1379
+
1380
+ 複数の要素が同じ token を配線できます(`eventToken.x: shared`)—— すべての dispatch が1つの `$on` ハンドラに届き、command token のファンアウトと対称です。さらに `$on` ハンドラは `state` を受け取るため、そこから command token を再 emit して 要素 → state → 要素 のチェインを組めます:
1381
+
1382
+ ```javascript
1383
+ $commandTokens: ["doRefresh"],
1384
+ $eventTokens: ["completed"],
1385
+ $on: {
1386
+ completed(state) {
1387
+ state.$command.doRefresh.emit(); // event in → command out
1388
+ }
1389
+ }
1390
+ ```
1391
+
1392
+ ### Token API
1393
+
1394
+ event token は command token と同じ `Token` pub/sub プリミティブを共有します —— `name` / `size` / `subscribe` / `unsubscribe` / `emit`、subscribe 順の保持つき([Token API](#token-api) 参照)。token はイベントごとに registry から解決されるため、`setInitialState()` による再構築後も最新の `$on` 購読者に届きます。所有する `<wcs-state>` が disconnect されると、event-token registry はクリアされます。
1395
+
1248
1396
  ## Inputs と属性ミラー
1249
1397
 
1250
1398
  `wcBindable.inputs` は一方向のプロパティ入力(state → 要素)を宣言します。エントリに `attribute` を設定すると、フレームワークはプロパティを書き込むたびにその値を当該 HTML 属性へも書き込むため、`attributeChangedCallback`・CSS の属性セレクタ・DevTools がすべてプロパティ値と同期し続けます。
@@ -1600,7 +1748,7 @@ const html = await renderToString(template, {
1600
1748
  });
1601
1749
  ```
1602
1750
 
1603
- これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を自動検出し、JSON スナップショットから状態を復元し��再レンダリングなしでリアクティビティを再開します。
1751
+ これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を自動検出し、JSON スナップショットから状態を復元し、再レンダリングなしでリアクティビティを再開します。
1604
1752
 
1605
1753
  ### 仕組み
1606
1754
 
@@ -1608,14 +1756,14 @@ const html = await renderToString(template, {
1608
1756
  |---------|------|
1609
1757
  | **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用、ハイドレーションデータを含む `<wcs-ssr>` 要素付きのレンダリング済み HTML を出力 |
1610
1758
  | **クライアント** | `<wcs-state enable-ssr>` が `<wcs-ssr>` の JSON から状態をロード、`$connectedCallback` をスキップ、`hydrateBindings()` が既存の DOM にリアクティビティを接続 |
1611
- | **フォールバック** | ���ーバー/クライアントのバージョン不一致時、SSR DOM をクリーンアップして `buildBindings()` でフルクライアントサイドレンダリングを実行 |
1759
+ | **フォールバック** | サーバー/クライアントのバージョン不一致時、SSR DOM をクリーンアップして `buildBindings()` でフルクライアントサイドレンダリングを実行 |
1612
1760
 
1613
1761
  ### `enable-ssr` の動作
1614
1762
 
1615
1763
  | コンテキスト | 動作 |
1616
1764
  |------------|------|
1617
1765
  | **サーバー**(`renderToString`) | 状態 JSON、テンプレートフラグメント、プロパティデータを含む `<wcs-ssr>` を生成 |
1618
- | **クラ��アント**(ハイドレーション) | `<wcs-ssr>` を読み取り、状態を復元、`$connectedCallback` をスキップ、既存 DOM のバイン���ィングをハイドレート |
1766
+ | **クライアント**(ハイドレーション) | `<wcs-ssr>` を読み取り、状態を復元、`$connectedCallback` をスキップ、既存 DOM のバインディングをハイドレート |
1619
1767
 
1620
1768
  API の詳細は [`@wcstack/server` README](../server/README.ja.md) を参照してください。
1621
1769
 
package/README.md CHANGED
@@ -97,6 +97,7 @@ That's it. No build, no bootstrap code, no framework.
97
97
  - **Two-way binding** — automatic for `<input>`, `<select>`, `<textarea>`
98
98
  - **Web Component binding** — bidirectional state binding with Shadow DOM components
99
99
  - **Command tokens** — invoke methods on wc-bindable custom elements from state via a pub/sub channel (`command.<method>: tokenName`)
100
+ - **Event tokens** — the dual of command tokens: receive a wc-bindable element's dispatched events in state via `eventToken.<prop>: tokenName` + the `$on` map
100
101
  - **Path getters** — dot-path key getters (`get "users.*.fullName"()`) for virtual properties at any depth in a data tree, all defined flat in one place with automatic dependency tracking and caching
101
102
  - **Mustache syntax** — `{{ path|filter }}` in text nodes
102
103
  - **Multiple state sources** — JSON, JS module, inline script, API, attribute
@@ -420,6 +421,8 @@ Runtime reads `customClass.wcBindable.properties + inputs` and expands each name
420
421
  <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
421
422
  ```
422
423
 
424
+ **`undefined` is "no opinion"** — when an expanded state path resolves to `undefined` (e.g. the slot object doesn't initialize that input), the property write is **skipped** and the element keeps its own default. You only need to initialize the paths you actually use; `usersFetch: { value: null, loading: false }` is enough even though `<wcs-fetch>` also declares `method` / `manual` / `body`. To explicitly clear a value, assign `null` — `null` is always written. (This skip applies to every property binding, not just spread; with `config.debug` each skipped write is logged via `console.debug`.)
425
+
423
426
  **Constraints**:
424
427
 
425
428
  - Filters on the spread target (`...: target|filter`) are rejected.
@@ -1247,6 +1250,150 @@ This is pure wiring: the event endpoint is connected to a command-token endpoint
1247
1250
  <my-list data-wcs="command.reset: $command.reset"></my-list>
1248
1251
  ```
1249
1252
 
1253
+ ## Event Token (Event Binding)
1254
+
1255
+ Command tokens push *into* a component (state invokes a method). **Event tokens** are the exact dual — they pull *out* of a component (an element dispatches an event, state receives it). Together they cover both directions of the element ↔ state boundary, and neither side ever holds a reference to the other — the token is the only shared object.
1256
+
1257
+ | Token | Direction | Subscribes | Emits |
1258
+ |---|---|---|---|
1259
+ | **Command token** | state → element | element (`command.<method>:`) | state (`$command.<name>.emit`) |
1260
+ | **Event token** | element → state | state (`$on`) | element (DOM event listener) |
1261
+
1262
+ - The element wires `eventToken.<property>: <tokenName>` on a wc-bindable custom element.
1263
+ - State declares channels with `$eventTokens` and receives them with the `$on` map.
1264
+ - Subscribers are called as `(state, event, ...listIndexes)` — symmetric with the command-token emit convention.
1265
+
1266
+ ### Basic Usage
1267
+
1268
+ ```html
1269
+ <wcs-state>
1270
+ <script type="module">
1271
+ export default {
1272
+ users: [],
1273
+ error: null,
1274
+
1275
+ $eventTokens: ["userCreated", "createFailed"],
1276
+ $on: {
1277
+ userCreated(state, event) {
1278
+ state.users = state.users.concat(event.detail);
1279
+ },
1280
+ createFailed(state, event) {
1281
+ state.error = event.detail;
1282
+ }
1283
+ }
1284
+ };
1285
+ </script>
1286
+ </wcs-state>
1287
+
1288
+ <!-- Emitters — must be wc-bindable custom elements -->
1289
+ <my-form data-wcs="eventToken.created: userCreated; eventToken.error: createFailed"></my-form>
1290
+ ```
1291
+
1292
+ When `<my-form>` dispatches the DOM event mapped to its `created` property, the `userCreated` token fires and the `$on.userCreated` handler runs with `(state, event)`.
1293
+
1294
+ ### `$eventTokens` Declaration
1295
+
1296
+ The `$eventTokens` array declares the channel names that `eventToken.<prop>:` bindings and `$on` keys may reference. Only declared names are valid (typo resistance).
1297
+
1298
+ ```javascript
1299
+ export default {
1300
+ $eventTokens: ["userCreated", "createFailed"],
1301
+ };
1302
+ ```
1303
+
1304
+ - Entries must be non-empty strings
1305
+ - Duplicate entries throw an error at initialization
1306
+ - A token declared here but absent from `$on` simply has no subscriber — emitting it is a no-op
1307
+
1308
+ ### `$on` — Receiving on the State Side
1309
+
1310
+ `$on` maps each event-token name to a handler. Because state is passed as the **first argument** (not via `this`), handlers can be written as either method shorthand or arrow functions — this mirrors the command-token emit convention, where `this` is likewise not bound:
1311
+
1312
+ ```javascript
1313
+ $on: {
1314
+ // both forms work — state is always the first parameter
1315
+ userCreated: (state, event) => { state.lastId = event.detail.id; },
1316
+ rowFailed(state, event, ...listIndexes) {
1317
+ const [i] = listIndexes; // loop index when fired from inside a `for`
1318
+ state.failedRows = state.failedRows.concat(i);
1319
+ }
1320
+ }
1321
+ ```
1322
+
1323
+ - Every `$on` key must be declared in `$eventTokens` (otherwise an error is thrown at initialization)
1324
+ - Each value must be a function
1325
+ - The signature is `(state, event, ...listIndexes)` — the DOM `Event` first, then any enclosing loop indexes
1326
+
1327
+ ### `eventToken.<property>:` Binding
1328
+
1329
+ ```html
1330
+ <my-target data-wcs="eventToken.error: createFailed"></my-target>
1331
+ ```
1332
+
1333
+ | Part | Description |
1334
+ |---|---|
1335
+ | `eventToken.` | Fixed prefix |
1336
+ | `<property>` | A **wcBindable property name** — not a raw DOM event name. The real event name is resolved from `wcBindable.properties[].event` |
1337
+ | `<tokenName>` | A bare event-token name declared in `$eventTokens` (no `$`-namespace prefix, unlike command tokens) |
1338
+
1339
+ The key is a property name rather than a raw event name so the binding goes through the same `wcBindable` contract that command bindings use — and so a namespaced event name (`ns:evt`) cannot collide with the binding's `:` separator. The framework looks up `properties[].event` and attaches a listener for that real event:
1340
+
1341
+ ```javascript
1342
+ class MyTarget extends HTMLElement {
1343
+ static wcBindable = {
1344
+ protocol: "wc-bindable", version: 1,
1345
+ properties: [
1346
+ { name: "error", event: "thing-error" }, // eventToken.error → listens for "thing-error"
1347
+ { name: "created", event: "thing-created" },
1348
+ ],
1349
+ };
1350
+ }
1351
+ ```
1352
+
1353
+ Validation rules:
1354
+
1355
+ - The element must be a wc-bindable custom element (`static wcBindable`, `protocol: "wc-bindable"`, `version: 1`). A non-wc-bindable element is rejected at attach time.
1356
+ - `<property>` must appear in `wcBindable.properties` — checked at **attach time** (fail-fast; needs only the class, not DOM connection).
1357
+ - `<tokenName>` must be declared in `$eventTokens` — checked at **fire time**. State is resolved from the element's live root node when the event fires, so the binding also works inside `for` / `if` blocks and after SSR hydration, where the node may still be detached at attach time.
1358
+ - Modifiers `#prevent` / `#stop` work as on any event binding: `eventToken.error#prevent: createFailed`.
1359
+
1360
+ ### Inside a Loop
1361
+
1362
+ When the emitter sits inside a `for` block, the enclosing loop indexes are appended after the event, exactly like an `on*` handler:
1363
+
1364
+ ```html
1365
+ <template data-wcs="for: rows">
1366
+ <my-row data-wcs="eventToken.failed: rowFailed"></my-row>
1367
+ </template>
1368
+ ```
1369
+
1370
+ ```javascript
1371
+ $on: {
1372
+ rowFailed(state, event, ...listIndexes) {
1373
+ const [i] = listIndexes; // index of the row that fired
1374
+ state.failedRows = state.failedRows.concat(i);
1375
+ }
1376
+ }
1377
+ ```
1378
+
1379
+ ### Fan-in and Chaining
1380
+
1381
+ Multiple elements can wire the same token (`eventToken.x: shared`) — every dispatch reaches the one `$on` handler, mirroring command-token fan-out. And because an `$on` handler receives `state`, it can re-emit a command token, chaining element → state → element:
1382
+
1383
+ ```javascript
1384
+ $commandTokens: ["doRefresh"],
1385
+ $eventTokens: ["completed"],
1386
+ $on: {
1387
+ completed(state) {
1388
+ state.$command.doRefresh.emit(); // event in → command out
1389
+ }
1390
+ }
1391
+ ```
1392
+
1393
+ ### Token API
1394
+
1395
+ Event tokens share the same `Token` pub/sub primitive as command tokens — `name` / `size` / `subscribe` / `unsubscribe` / `emit`, with subscribe-order preservation (see [Token API](#token-api)). The token is resolved from the registry on every event so a re-`setInitialState()` rebuild still reaches the latest `$on` subscribers. When the owning `<wcs-state>` is disconnected, the event-token registry is cleared.
1396
+
1250
1397
  ## Inputs and Attribute Mirror
1251
1398
 
1252
1399
  `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.