@wcstack/state 1.10.4 → 1.11.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
@@ -380,6 +380,55 @@ property[#modifier]: path[@state][|filter[|filter(args)...]]
380
380
 
381
381
  内部的にはコメントベースのバインディング(`<!--@@:expression-->`)に変換されます。
382
382
 
383
+ ### Spread バインディング (`...`)
384
+
385
+ `wc-bindable` プロトコルを宣言したカスタム要素に対して、`...: target` を使うと要素の **properties + inputs を 1 行で一括配線**できます:
386
+
387
+ ```html
388
+ <wcs-fetch data-wcs="...: usersFetch"></wcs-fetch>
389
+ ```
390
+
391
+ ```js
392
+ export default {
393
+ usersFetch: {
394
+ url: "/api/users",
395
+ method: "GET",
396
+ value: null,
397
+ loading: false,
398
+ error: null,
399
+ status: null,
400
+ }
401
+ }
402
+ ```
403
+
404
+ ランタイムが `customClass.wcBindable.properties + inputs` を読み取り、各 name を個別バインディング(`usersFetch.value`、`usersFetch.url`、...)に展開します。
405
+
406
+ **対象範囲**:spread は *データサーフェス*(properties + inputs)のみを対象とします。`commands` や event token は **対象外** — pub/sub の発火点が HTML から読めるように、明示配線を維持してください。
407
+
408
+ **for ループ内**:`...: items.*`(推奨)または dot ショートカット `...: .` を使います:
409
+
410
+ ```html
411
+ <template data-wcs="for: storesFetches">
412
+ <wcs-fetch data-wcs="...: storesFetches.*"></wcs-fetch>
413
+ </template>
414
+ ```
415
+
416
+ **後勝ち上書き** — spread の後ろに同名 prop を書くと、明示側が優先されます:
417
+
418
+ ```html
419
+ <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
420
+ ```
421
+
422
+ **制約事項**:
423
+
424
+ - spread 右辺へのフィルタ(`...: target|filter`)はエラー
425
+ - 右辺パスの途中に `*` を含めても OK(例:`...: stores.*.fetch`)
426
+ - `@stateName` 修飾子は各展開エントリへ伝播(`...: fetchX@store`)
427
+ - カスタム要素クラスが未登録の場合、`customElements.whenDefined(tag)` 解決時に遅延展開される(autoloader による遅延ロードに対応)
428
+ - `wcBindable` 宣言**のない**要素はエラー(明示配線で書いてください)。spread は何を展開すべきかを契約から読み取るため
429
+
430
+ **Composite shell**(wc-bindable Composition Profile)はそのままサポートされます:composite shell は標準の `target.constructor.wcBindable` を通じて synthesized declaration を露出するため、`"s3.progress"` のような composed name はフラットな要素メンバーキーとして扱われます。state を composed 構造に合わせて (`{ s3: { progress: 0 } }`) 持てば、`...: pipeline` が自動的に nested state path へ展開されます。
431
+
383
432
  ## 構造ディレクティブ
384
433
 
385
434
  構造ディレクティブは `<template>` 要素で使用します:
@@ -1045,6 +1094,201 @@ customElements.define("my-component", MyComponent);
1045
1094
  </template>
1046
1095
  ```
1047
1096
 
1097
+ ## Command Token(メソッドバインディング)
1098
+
1099
+ プロパティバインディング(`state.message: user.name`)はコンポーネントへ流れ込むデータを扱いますが、**state からコンポーネントのメソッドを起動する**こと —— `<wcs-fetch>.fetch()`、`<wcs-dialog>.open()` など —— はカバーしません。**command token** は型付きの pub/sub チャネルでこの隙間を埋めます:
1100
+
1101
+ - 要素は `command.<methodName>: $command.<tokenName>` で購読する
1102
+ - state は `this.$command.<tokenName>.emit(...args)` で emit する
1103
+ - `emit` に渡した引数はそのまま要素のメソッドへ転送される
1104
+ - 1つの token は複数の要素へファンアウトでき、subscribe 順は保持される
1105
+
1106
+ これによりパス契約は保たれます。state は要素への参照を一切保持せず、要素も state から何もインポートしません。共有されるオブジェクトは token のみです。
1107
+
1108
+ ### 基本的な使い方
1109
+
1110
+ ```html
1111
+ <wcs-state>
1112
+ <script type="module">
1113
+ export default {
1114
+ $commandTokens: ["fetchUsers", "refreshOrders"],
1115
+
1116
+ onClickFetch() {
1117
+ this.$command.fetchUsers.emit("/api/users", { method: "GET" });
1118
+ },
1119
+ onClickRefresh() {
1120
+ this.$command.refreshOrders.emit();
1121
+ }
1122
+ };
1123
+ </script>
1124
+ </wcs-state>
1125
+
1126
+ <!-- 購読者 — wc-bindable なカスタム要素であること -->
1127
+ <wcs-fetch data-wcs="command.fetch: $command.fetchUsers"></wcs-fetch>
1128
+ <wcs-fetch data-wcs="command.fetch: $command.refreshOrders"></wcs-fetch>
1129
+
1130
+ <button data-wcs="onclick: onClickFetch">Fetch users</button>
1131
+ <button data-wcs="onclick: onClickRefresh">Refresh orders</button>
1132
+ ```
1133
+
1134
+ `onClickFetch` が実行されると、`fetchUsers` token を購読しているすべての要素の `fetch(...)` メソッドが転送された引数とともに呼び出されます。
1135
+
1136
+ ### `$commandTokens` 宣言
1137
+
1138
+ `$commandTokens` 配列は、state 上の `$command` 名前空間に公開するチャネルを宣言します。token は `this.$command.<name>` でアクセスでき、memo 化されます —— 同じ名前は常に同一の token インスタンスを返します。
1139
+
1140
+ ```javascript
1141
+ export default {
1142
+ $commandTokens: ["fetchUsers", "refreshOrders"],
1143
+
1144
+ click() {
1145
+ this.$command.fetchUsers.emit("/api/users");
1146
+ }
1147
+ };
1148
+ ```
1149
+
1150
+ - エントリは空でない文字列であること
1151
+ - 重複するエントリは初期化時にエラーになる
1152
+ - 予約名 `$command` 自体は配列に含められない
1153
+ - token は `$command` 配下にまとめられるためトップレベルの state 名前空間を汚さない。token と同名のリアクティブプロパティが共存できる
1154
+ - `$command` 上の未宣言の名前にアクセスする(例: `this.$command.typo`)と `undefined` が返る。typo はその後の `.emit()` 呼び出しで `TypeError` として、あるいはバインディングの右辺で使った場合は「CommandToken 値が必要」エラーとしてバインディング時に表面化する
1155
+
1156
+ ### `command.<methodName>:` バインディング
1157
+
1158
+ ```html
1159
+ <wcs-fetch data-wcs="command.fetch: $command.fetchUsers"></wcs-fetch>
1160
+ ```
1161
+
1162
+ | 部位 | 説明 |
1163
+ |---|---|
1164
+ | `command.` | 固定の prefix |
1165
+ | `<methodName>` | 起動する要素のメソッド。名前は `static wcBindable.commands` に `{ name: "<methodName>" }` として現れること |
1166
+ | `$command.<tokenName>` | `CommandToken` に解決される明示的な名前空間パス。`<tokenName>` は `$commandTokens` で宣言された名前であること |
1167
+
1168
+ 右辺は `$command.<tokenName>` と書く必要があります —— ベア名の省略形(`fetchUsers`)は非対応です。`$command.` 名前空間を経由することでバインディングの意図が HTML 上で明示され、トップレベルの state 名前空間を token 名で汚さずに済みます。
1169
+
1170
+ `wcBindable.commands` は wc-bindable v1 仕様の形 —— `{ name: string; async?: boolean }` の配列 —— に従います:
1171
+
1172
+ ```javascript
1173
+ class MyFetcher extends HTMLElement {
1174
+ static wcBindable = {
1175
+ protocol: "wc-bindable", version: 1,
1176
+ properties: [],
1177
+ commands: [
1178
+ { name: "fetch", async: true },
1179
+ { name: "reset" },
1180
+ ],
1181
+ };
1182
+ fetch(url) { /* ... */ }
1183
+ reset() { /* ... */ }
1184
+ }
1185
+ ```
1186
+
1187
+ > **v1.9.1 以降の破壊的変更**: `commands` フィールドは `{ name, async? }` オブジェクトの配列になりました。以前の `commands: ["fetch"]` という素の文字列形式はもう受け付けられません —— そのような宣言に対するバインディングは `Command "<name>" is not declared in wcBindable.commands` を throw します。レガシーフォールバックはありません。宣言をオブジェクト形式に更新してください。
1188
+
1189
+ 検証ルール(バインディング時に強制):
1190
+
1191
+ - 要素は `protocol: "wc-bindable"` かつ `version: 1` の `static wcBindable` を公開するカスタム要素であること
1192
+ - `methodName` は `wcBindable.commands` に(`name` で)現れること
1193
+ - バインドされる値は `CommandToken` であること(token 以外の値の代入は throw する —— 例えば未宣言の名前 `$command.typo` は `undefined` に解決され、ここで拒否される)
1194
+
1195
+ ### Token API
1196
+
1197
+ ```typescript
1198
+ interface CommandToken {
1199
+ readonly name: string;
1200
+ readonly size: number; // 現在の購読者数
1201
+ subscribe(fn: (...args) => unknown): () => void; // unsubscribe を返す
1202
+ unsubscribe(fn: (...args) => unknown): boolean;
1203
+ emit(...args: unknown[]): unknown[]; // subscribe 順に購読者の戻り値を返す
1204
+ }
1205
+ ```
1206
+
1207
+ `emit` は各購読者の戻り値の配列を(subscribe 順で)返します。`Promise` を返すメソッドは `Promise.all(token.emit(...))` でラップしてすべてを待ち受けてください。
1208
+
1209
+ ### 購読のライフサイクル
1210
+
1211
+ - 購読者は要素を `WeakRef` で保持するため、token の購読者セットに残っていても、取り外された要素はガベージコレクト可能
1212
+ - `emit` 時、WeakRef が回収済みか要素が接続されていない(`isConnected === false`)場合、購読は自動的に破棄される(lazy purge)
1213
+ - 所有する `<wcs-state>` が disconnect されると、token レジストリ全体がクリアされる
1214
+
1215
+ 要素のメソッドは `emit` の引数で呼び出されます:
1216
+
1217
+ ```javascript
1218
+ this.$command.fetchUsers.emit(url, options);
1219
+ // → すべての購読者で element.fetch(url, options)
1220
+ ```
1221
+
1222
+ ### DOM イベントから command を emit する
1223
+
1224
+ command token は state コードから emit する必要はありません。DOM イベントバインディングの右辺を、state メソッド名ではなく `$command.<name>` パスに向けることで、直接 emit できます:
1225
+
1226
+ ```html
1227
+ <button data-wcs="onclick: $command.refreshList">Refresh</button>
1228
+ ```
1229
+
1230
+ | 形式 | 右辺 | イベント時の動作 |
1231
+ |---|---|---|
1232
+ | `onclick: someMethod` | state メソッド名 | `state.someMethod(event, ...listIndexes)` |
1233
+ | `onclick: $command.someToken` | `$command.<name>` パス | `state.$command.someToken.emit(event, ...listIndexes)` |
1234
+
1235
+ これは純粋な配線です。イベント端点を command token 端点に接続するだけで、間にロジックは入りません。`emit` の引数はハンドラ呼び出しとまったく同じく透過されます —— まず DOM の `Event`、続いて内包するリストインデックス —— なので購読者は `(event, ...listIndexes)` を受け取ります。購読者の中で必要なものをイベントから取り出してください(`event.target.value`、`event.detail` など)。
1236
+
1237
+ - 右辺は `$command.<name>` であり、`<name>` は `$commandTokens` で宣言されていること。`CommandToken` に解決されないパス(例: typo)はイベント時に throw する。
1238
+ - 修飾子はそのまま機能する: `onclick#prevent: $command.someToken` は emit の前に `preventDefault()` を呼ぶ(`#stop` も同様)。
1239
+ - これは state が emit するのと同じ token を emit するので、`command.<method>: $command.someToken` で配線された要素の購読者は、誰がトリガを引いたかに関わらず受け取る。
1240
+
1241
+ ```html
1242
+ <!-- click が command を全購読者へファンアウトする。state メソッドは不要 -->
1243
+ <button data-wcs="onclick: $command.reset">Reset all</button>
1244
+ <my-field data-wcs="command.clear: $command.reset"></my-field>
1245
+ <my-list data-wcs="command.reset: $command.reset"></my-list>
1246
+ ```
1247
+
1248
+ ## Inputs と属性ミラー
1249
+
1250
+ `wcBindable.inputs` は一方向のプロパティ入力(state → 要素)を宣言します。エントリに `attribute` を設定すると、フレームワークはプロパティを書き込むたびにその値を当該 HTML 属性へも書き込むため、`attributeChangedCallback`・CSS の属性セレクタ・DevTools がすべてプロパティ値と同期し続けます。
1251
+
1252
+ ```javascript
1253
+ class MyChip extends HTMLElement {
1254
+ static wcBindable = {
1255
+ protocol: "wc-bindable", version: 1,
1256
+ properties: [],
1257
+ inputs: [
1258
+ { name: "data", attribute: "data" }, // プロパティ名 === 属性名
1259
+ { name: "labelText", attribute: "label-text" }, // kebab-case ミラー
1260
+ { name: "internal" }, // ミラーなし、プロパティのみ
1261
+ ],
1262
+ };
1263
+ }
1264
+ ```
1265
+
1266
+ ```html
1267
+ <my-chip data-wcs="data: chip.payload; labelText: chip.title"></my-chip>
1268
+ ```
1269
+
1270
+ state が値を更新すると、プロパティと属性の両方が書き込まれます:
1271
+
1272
+ ```text
1273
+ chip.payload = { id: 1 } → element.data = { id: 1 } かつ setAttribute("data", '{"id":1}')
1274
+ chip.title = "新着" → element.labelText = "新着" かつ setAttribute("label-text", "新着")
1275
+ chip.payload = null → element.data = null かつ removeAttribute("data")
1276
+ ```
1277
+
1278
+ 属性値のエンコード:
1279
+
1280
+ | 値の型 | ミラーされる属性 |
1281
+ |---|---|
1282
+ | `string` / `number` / `boolean` / `bigint` | `String(value)` |
1283
+ | `null` / `undefined` | 属性を削除 |
1284
+ | `object` / `array` | `JSON.stringify(value)`(循環参照時は `String(value)` にフォールバック) |
1285
+
1286
+ 補足:
1287
+
1288
+ - `attribute` を**持たない** `inputs` エントリはプロパティのみ —— 値はプロパティに書き込まれるが属性には触れない
1289
+ - ミラーはベストエフォート: `setAttribute` の失敗は握りつぶされ(`debug` 警告付き)、プロパティ書き込みをブロックしない
1290
+ - ネイティブ HTML 要素は `inputs` を完全に無視する —— ミラーは `static wcBindable` を公開するカスタム要素でのみ有効になる
1291
+
1048
1292
  ## 宣言的カスタムコンポーネント (DCC)
1049
1293
 
1050
1294
  JavaScript のクラス定義なしで、**HTML だけ**でカスタム要素を定義できます。`data-wc-definition` と Declarative Shadow DOM (`<template shadowrootmode>`) を使い、リアクティブな状態を持つ再利用可能なコンポーネントをインラインで宣言します。
package/README.md CHANGED
@@ -381,6 +381,55 @@ When `enableMustache` is `true` (default), `{{ expression }}` in text nodes is s
381
381
 
382
382
  Internally converted to comment-based bindings (`<!--@@:expression-->`).
383
383
 
384
+ ### Spread Binding (`...`)
385
+
386
+ For custom elements that declare the [`wc-bindable` protocol](#wcbindable-protocol), `...: target` wires all of the element's **properties + inputs** to a single state object in one line:
387
+
388
+ ```html
389
+ <wcs-fetch data-wcs="...: usersFetch"></wcs-fetch>
390
+ ```
391
+
392
+ ```js
393
+ export default {
394
+ usersFetch: {
395
+ url: "/api/users",
396
+ method: "GET",
397
+ value: null,
398
+ loading: false,
399
+ error: null,
400
+ status: null,
401
+ }
402
+ }
403
+ ```
404
+
405
+ Runtime reads `customClass.wcBindable.properties + inputs` and expands each name into an individual binding (`usersFetch.value`, `usersFetch.url`, ...).
406
+
407
+ **Scope**: spread covers the *data surfaces* (properties + inputs). `commands` and event tokens are intentionally **not** included — wire them explicitly so the pub/sub points remain visible in HTML.
408
+
409
+ **Inside a for loop**: use `...: items.*` (recommended) or the dot shortcut `...: .`:
410
+
411
+ ```html
412
+ <template data-wcs="for: storesFetches">
413
+ <wcs-fetch data-wcs="...: storesFetches.*"></wcs-fetch>
414
+ </template>
415
+ ```
416
+
417
+ **Last-wins override** — explicit binding after `...` overrides the spread:
418
+
419
+ ```html
420
+ <wcs-fetch data-wcs="...: usersFetch; status: alternateStatus"></wcs-fetch>
421
+ ```
422
+
423
+ **Constraints**:
424
+
425
+ - Filters on the spread target (`...: target|filter`) are rejected.
426
+ - The right-hand path may contain `*` anywhere (e.g. `...: stores.*.fetch`).
427
+ - `@stateName` propagates to every expanded entry (`...: fetchX@store`).
428
+ - If the custom element class is not yet registered, expansion is deferred until `customElements.whenDefined(tag)` resolves — autoloader-style late registration is supported.
429
+ - Elements **without** a `wcBindable` declaration are rejected (write bindings explicitly). Spread requires the contract to know what to expand.
430
+
431
+ **Composite shells** (wc-bindable Composition Profile) are supported transparently: a composite shell exposes its synthesized declaration through the standard `target.constructor.wcBindable` surface, and composed names like `"s3.progress"` are kept as flat element member keys. Mirror the composed structure in state (`{ s3: { progress: 0 } }`) and `...: pipeline` expands into nested state paths automatically.
432
+
384
433
  ## Structural Directives
385
434
 
386
435
  Structural directives use `<template>` elements:
@@ -1172,6 +1221,32 @@ this.$command.fetchUsers.emit(url, options);
1172
1221
  // → element.fetch(url, options) on every subscriber
1173
1222
  ```
1174
1223
 
1224
+ ### Emitting a Command from a DOM Event
1225
+
1226
+ A command token does not have to be emitted from state code. A DOM event binding can emit one directly by pointing its right-hand side at a `$command.<name>` path instead of a state method name:
1227
+
1228
+ ```html
1229
+ <button data-wcs="onclick: $command.refreshList">Refresh</button>
1230
+ ```
1231
+
1232
+ | Form | Right-hand side | Behavior on event |
1233
+ |---|---|---|
1234
+ | `onclick: someMethod` | a state method name | `state.someMethod(event, ...listIndexes)` |
1235
+ | `onclick: $command.someToken` | a `$command.<name>` path | `state.$command.someToken.emit(event, ...listIndexes)` |
1236
+
1237
+ This is pure wiring: the event endpoint is connected to a command-token endpoint, with no logic in between. The `emit` arguments are passed through exactly like a handler call — the DOM `Event` first, then any enclosing list indexes — so subscribers receive `(event, ...listIndexes)`. Inside a subscriber, pull what you need from the event (`event.target.value`, `event.detail`, …).
1238
+
1239
+ - The right-hand side must be `$command.<name>` with `<name>` declared in `$commandTokens`. A path that does not resolve to a `CommandToken` (e.g. a typo) throws at event time.
1240
+ - Modifiers work unchanged: `onclick#prevent: $command.someToken` calls `preventDefault()` before emitting (`#stop` likewise).
1241
+ - This emits the same token the state emits, so element subscribers wired with `command.<method>: $command.someToken` receive it regardless of who pulled the trigger.
1242
+
1243
+ ```html
1244
+ <!-- click fans the command out to every subscriber, no state method needed -->
1245
+ <button data-wcs="onclick: $command.reset">Reset all</button>
1246
+ <my-field data-wcs="command.clear: $command.reset"></my-field>
1247
+ <my-list data-wcs="command.reset: $command.reset"></my-list>
1248
+ ```
1249
+
1175
1250
  ## Inputs and Attribute Mirror
1176
1251
 
1177
1252
  `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.