@wcstack/state 1.9.0 → 1.10.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.md CHANGED
@@ -1051,7 +1051,7 @@ customElements.define("my-component", MyComponent);
1051
1051
 
1052
1052
  Property binding (`state.message: user.name`) covers data flowing into a component, but it does not cover **invoking a method on a component from state** — `<wcs-fetch>.fetch()`, `<wcs-dialog>.open()`, and so on. **Command tokens** fill that gap with a typed pub/sub channel:
1053
1053
 
1054
- - The element subscribes via `command.<methodName>: <tokenPath>`
1054
+ - The element subscribes via `command.<methodName>: $command.<tokenName>`
1055
1055
  - State emits via `this.$command.<tokenName>.emit(...args)`
1056
1056
  - Arguments passed to `emit` are forwarded to the element's method
1057
1057
  - One token can fan out to multiple elements; the subscriber order is preserved
@@ -1077,8 +1077,8 @@ This keeps the path contract intact: state never holds a reference to the elemen
1077
1077
  </wcs-state>
1078
1078
 
1079
1079
  <!-- Subscribers — must be wc-bindable custom elements -->
1080
- <wcs-fetch data-wcs="command.fetch: fetchUsers"></wcs-fetch>
1081
- <wcs-fetch data-wcs="command.fetch: refreshOrders"></wcs-fetch>
1080
+ <wcs-fetch data-wcs="command.fetch: $command.fetchUsers"></wcs-fetch>
1081
+ <wcs-fetch data-wcs="command.fetch: $command.refreshOrders"></wcs-fetch>
1082
1082
 
1083
1083
  <button data-wcs="onclick: onClickFetch">Fetch users</button>
1084
1084
  <button data-wcs="onclick: onClickRefresh">Refresh orders</button>
@@ -1104,25 +1104,46 @@ export default {
1104
1104
  - Duplicate entries throw an error at initialization
1105
1105
  - The reserved name `$command` itself cannot appear in the array
1106
1106
  - 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
1107
- - Accessing an undeclared name on `$command` (e.g. `this.$command.typo`) throwstypos are caught at access time
1107
+ - 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 a "requires a CommandToken value" error at binding time
1108
1108
 
1109
1109
  ### `command.<methodName>:` Binding
1110
1110
 
1111
1111
  ```html
1112
- <wcs-fetch data-wcs="command.fetch: fetchUsers"></wcs-fetch>
1112
+ <wcs-fetch data-wcs="command.fetch: $command.fetchUsers"></wcs-fetch>
1113
1113
  ```
1114
1114
 
1115
1115
  | Part | Description |
1116
1116
  |---|---|
1117
1117
  | `command.` | Fixed prefix |
1118
- | `<methodName>` | The element's method to invoke. Must be listed in `static wcBindable.commands` |
1119
- | `<tokenPath>` | A path that resolves to a `CommandToken` (typically a name from `$commandTokens`) |
1118
+ | `<methodName>` | The element's method to invoke. The name must appear as `{ name: "<methodName>" }` in `static wcBindable.commands` |
1119
+ | `$command.<tokenName>` | Explicit namespace path that resolves to a `CommandToken`. `<tokenName>` must be a name declared in `$commandTokens` |
1120
+
1121
+ The right-hand side must be written as `$command.<tokenName>` — the bare-name shorthand (`fetchUsers`) is not supported. Going through the `$command.` namespace makes the binding's intent explicit in the HTML and keeps the top-level state namespace free of token names.
1122
+
1123
+ `wcBindable.commands` follows the wc-bindable v1 spec shape — an array of `{ name: string; async?: boolean }`:
1124
+
1125
+ ```javascript
1126
+ class MyFetcher extends HTMLElement {
1127
+ static wcBindable = {
1128
+ protocol: "wc-bindable", version: 1,
1129
+ properties: [],
1130
+ commands: [
1131
+ { name: "fetch", async: true },
1132
+ { name: "reset" },
1133
+ ],
1134
+ };
1135
+ fetch(url) { /* ... */ }
1136
+ reset() { /* ... */ }
1137
+ }
1138
+ ```
1139
+
1140
+ > **Breaking change since v1.9.1**: the `commands` field is now an array of `{ name, async? }` objects. The earlier `commands: ["fetch"]` plain-string form is no longer accepted — bindings against such declarations throw `Command "<name>" is not declared in wcBindable.commands`. There is no legacy fallback; update the declaration to the object form.
1120
1141
 
1121
1142
  Validation rules (enforced at binding time):
1122
1143
 
1123
1144
  - The element must be a custom element exposing `static wcBindable` with `protocol: "wc-bindable"` and `version: 1`
1124
- - `methodName` must be present in `wcBindable.commands`
1125
- - The bound value must be a `CommandToken` (assigning a non-token value throws)
1145
+ - `methodName` must appear (by `name`) in `wcBindable.commands`
1146
+ - The bound value must be a `CommandToken` (assigning a non-token value throws — for example, an undeclared name like `$command.typo` resolves to `undefined` and is rejected here)
1126
1147
 
1127
1148
  ### Token API
1128
1149
 
@@ -1151,6 +1172,50 @@ this.$command.fetchUsers.emit(url, options);
1151
1172
  // → element.fetch(url, options) on every subscriber
1152
1173
  ```
1153
1174
 
1175
+ ## Inputs and Attribute Mirror
1176
+
1177
+ `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.
1178
+
1179
+ ```javascript
1180
+ class MyChip extends HTMLElement {
1181
+ static wcBindable = {
1182
+ protocol: "wc-bindable", version: 1,
1183
+ properties: [],
1184
+ inputs: [
1185
+ { name: "data", attribute: "data" }, // property name === attribute name
1186
+ { name: "labelText", attribute: "label-text" }, // kebab-case mirror
1187
+ { name: "internal" }, // no mirror, property-only
1188
+ ],
1189
+ };
1190
+ }
1191
+ ```
1192
+
1193
+ ```html
1194
+ <my-chip data-wcs="data: chip.payload; labelText: chip.title"></my-chip>
1195
+ ```
1196
+
1197
+ When state updates the value, both the property and the attribute are written:
1198
+
1199
+ ```text
1200
+ chip.payload = { id: 1 } → element.data = { id: 1 } and setAttribute("data", '{"id":1}')
1201
+ chip.title = "新着" → element.labelText = "新着" and setAttribute("label-text", "新着")
1202
+ chip.payload = null → element.data = null and removeAttribute("data")
1203
+ ```
1204
+
1205
+ Attribute value encoding:
1206
+
1207
+ | Value type | Mirrored attribute |
1208
+ |---|---|
1209
+ | `string` / `number` / `boolean` / `bigint` | `String(value)` |
1210
+ | `null` / `undefined` | attribute removed |
1211
+ | `object` / `array` | `JSON.stringify(value)` (falls back to `String(value)` on circular references) |
1212
+
1213
+ Notes:
1214
+
1215
+ - `inputs` entries **without** `attribute` are property-only — the value is written to the property but no attribute is touched
1216
+ - Mirror is best-effort: a `setAttribute` failure is swallowed (with a `debug` warning) and does not block the property write
1217
+ - Native HTML elements ignore `inputs` entirely — the mirror only activates for custom elements that expose `static wcBindable`
1218
+
1154
1219
  ## Declarative Custom Components (DCC)
1155
1220
 
1156
1221
  Define custom elements **entirely in HTML** — no JavaScript class definition needed. Using `data-wc-definition` and Declarative Shadow DOM (`<template shadowrootmode>`), you can declare reusable components with reactive state inline.
package/dist/index.esm.js CHANGED
@@ -59,7 +59,7 @@ function setConfig(partialConfig) {
59
59
  }
60
60
  }
61
61
 
62
- var version$1 = "1.9.0";
62
+ var version$1 = "1.10.0";
63
63
  var pkg = {
64
64
  version: version$1};
65
65
 
@@ -2063,7 +2063,7 @@ function getWcBindable(element) {
2063
2063
  }
2064
2064
  function applyChangeToCommand(binding, _context, newValue) {
2065
2065
  if (!isCommandToken(newValue)) {
2066
- raiseError(`command binding requires a CommandToken value (use $commandToken or $commandTokens declaration).`);
2066
+ raiseError(`command binding requires a CommandToken value (use $command.<tokenName> with a name declared in $commandTokens).`);
2067
2067
  }
2068
2068
  const token = newValue;
2069
2069
  const existing = subscribedBindings.get(binding);
@@ -2080,7 +2080,7 @@ function applyChangeToCommand(binding, _context, newValue) {
2080
2080
  if (bindable === null) {
2081
2081
  raiseError(`command binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
2082
2082
  }
2083
- if (!Array.isArray(bindable.commands) || !bindable.commands.includes(methodName)) {
2083
+ if (!Array.isArray(bindable.commands) || !bindable.commands.some((c) => c.name === methodName)) {
2084
2084
  raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.`);
2085
2085
  }
2086
2086
  // ここまで来たら旧解除して新 subscribe に切り替える。
@@ -2975,6 +2975,65 @@ function applyChangeToIf(bindingInfo, context, rawNewValue) {
2975
2975
  }
2976
2976
  }
2977
2977
 
2978
+ /**
2979
+ * 要素 `element` の `propName` プロパティ書き込みに対して、
2980
+ * wc-bindable inputs の `attribute` ミラー先属性名を返す。
2981
+ *
2982
+ * - wc-bindable でないネイティブ要素や、inputs 未宣言、attribute フィールド無しは null
2983
+ * - inputs に同名宣言があっても `attribute` を持たないものはミラー対象外
2984
+ *
2985
+ * 戻り値の string がそのまま `setAttribute(name, value)` の name となる。
2986
+ */
2987
+ function getInputAttributeMirror(element, propName) {
2988
+ const customTagName = getCustomElement(element);
2989
+ if (customTagName === null) {
2990
+ return null;
2991
+ }
2992
+ const customClass = customElements.get(customTagName);
2993
+ if (typeof customClass === "undefined") {
2994
+ return null;
2995
+ }
2996
+ const bindable = customClass.wcBindable;
2997
+ if (bindable?.protocol !== "wc-bindable" || bindable?.version !== 1) {
2998
+ return null;
2999
+ }
3000
+ const inputs = bindable.inputs;
3001
+ if (!Array.isArray(inputs)) {
3002
+ return null;
3003
+ }
3004
+ for (const input of inputs) {
3005
+ if (input.name === propName && typeof input.attribute === "string" && input.attribute.length > 0) {
3006
+ return input.attribute;
3007
+ }
3008
+ }
3009
+ return null;
3010
+ }
3011
+ /**
3012
+ * mirror 属性値の表現を決める。
3013
+ * - null / undefined → 属性削除
3014
+ * - object / array → JSON.stringify (失敗時は String(value))
3015
+ * - その他 (string / number / boolean / bigint) → String(value)
3016
+ */
3017
+ function applyMirrorAttribute(element, attributeName, value) {
3018
+ if (value === null || typeof value === "undefined") {
3019
+ element.removeAttribute(attributeName);
3020
+ return;
3021
+ }
3022
+ let formatted;
3023
+ if (typeof value === "object") {
3024
+ try {
3025
+ formatted = JSON.stringify(value);
3026
+ }
3027
+ catch {
3028
+ formatted = String(value);
3029
+ }
3030
+ }
3031
+ else {
3032
+ formatted = String(value);
3033
+ }
3034
+ element.setAttribute(attributeName, formatted);
3035
+ }
3036
+
2978
3037
  /**
2979
3038
  * SSR 時に HTML 属性で表現できないプロパティバインディングを蓄積するストア。
2980
3039
  * ハイドレーション時にクライアント側で復元する。
@@ -3057,8 +3116,10 @@ function applyChangeToProperty(binding, _context, newValue) {
3057
3116
  if (propSegments.length === 1) {
3058
3117
  const firstSegment = propSegments[0];
3059
3118
  if (element[firstSegment] !== newValue) {
3119
+ let propertyWriteSucceeded = false;
3060
3120
  try {
3061
3121
  element[firstSegment] = newValue;
3122
+ propertyWriteSucceeded = true;
3062
3123
  }
3063
3124
  catch (error) {
3064
3125
  if (config.debug) {
@@ -3069,6 +3130,27 @@ function applyChangeToProperty(binding, _context, newValue) {
3069
3130
  });
3070
3131
  }
3071
3132
  }
3133
+ // wc-bindable inputs[].attribute ミラー。プロパティ書き込みが成功したときだけ
3134
+ // 属性へ反映する。setter が値を拒否した場合に属性だけ進んでしまうと
3135
+ // property と attribute が乖離し、attributeChangedCallback や CSS セレクタが
3136
+ // 実際のプロパティ値と矛盾した状態で発火するため、ここでガードする。
3137
+ if (propertyWriteSucceeded) {
3138
+ const mirrorAttr = getInputAttributeMirror(element, firstSegment);
3139
+ if (mirrorAttr !== null) {
3140
+ try {
3141
+ applyMirrorAttribute(element, mirrorAttr, newValue);
3142
+ }
3143
+ catch (error) {
3144
+ if (config.debug) {
3145
+ console.warn(`Failed to mirror attribute '${mirrorAttr}' on element.`, {
3146
+ element,
3147
+ newValue,
3148
+ error
3149
+ });
3150
+ }
3151
+ }
3152
+ }
3153
+ }
3072
3154
  }
3073
3155
  if (inSsr()) {
3074
3156
  const attrHandler = SSR_ATTR_PROPS[firstSegment];
@@ -5411,6 +5493,19 @@ function checkDependency(handler, address) {
5411
5493
  * - finallyでキャッシュへの格納を保証
5412
5494
  */
5413
5495
  function _getByAddress(target, address, receiver, handler, stateElement) {
5496
+ if (address.pathInfo.segments[0] === STATE_COMMAND_NAMESPACE_NAME) {
5497
+ // $command 名前空間配下のパスは raw state を持たないため、proxy の get トラップと
5498
+ // 同じ namespace を辿る。1セグメント目は namespace 本体、2セグメント目以降は
5499
+ // namespace 上のキー (= 宣言済み command token 名) を順に走査する。
5500
+ let value = getCommandNamespace(stateElement);
5501
+ for (let i = 1; i < address.pathInfo.segments.length; i++) {
5502
+ if (value == null) {
5503
+ return undefined;
5504
+ }
5505
+ value = Reflect.get(value, address.pathInfo.segments[i]);
5506
+ }
5507
+ return value;
5508
+ }
5414
5509
  if (address.pathInfo.path in target) {
5415
5510
  // getterの中で参照の可能性があるので、addressをプッシュする
5416
5511
  if (stateElement.getterPaths.has(address.pathInfo.path)) {