@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 +74 -9
- package/dist/index.esm.js +98 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
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>:
|
|
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`)
|
|
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.
|
|
1119
|
-
|
|
|
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
|
|
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.
|
|
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 $
|
|
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.
|
|
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)) {
|