@wcstack/state 1.3.19 → 1.5.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
@@ -75,6 +75,7 @@ CDNのスクリプトはカスタム要素の定義を登録するだけで、
75
75
  - **複数の状態ソース** — JSON, JS モジュール, インラインスクリプト, API, 属性
76
76
  - **SVG サポート** — `<svg>` 要素内でのフルバインディング対応
77
77
  - **ライフサイクルフック** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`、Web Component 用 `$stateReadyCallback`
78
+ - **TypeScript サポート** — `defineState()` によるドットパス自動補完付き型付き状態定義([詳細](docs/define-state.ja.md))
78
79
  - **依存ゼロ** — ランタイム依存なし
79
80
 
80
81
  ## インストール
@@ -1091,6 +1092,31 @@ bootstrapState({
1091
1092
  | `debug` | `false` | デバッグモード |
1092
1093
  | `enableMustache` | `true` | `{{ }}` 構文の有効化 |
1093
1094
 
1095
+ ## TypeScript サポート
1096
+
1097
+ `defineState()` で状態オブジェクトをラップすると、メソッドや getter 内の `this` に型補完が効きます。ランタイムコストはゼロ(アイデンティティ関数)です。
1098
+
1099
+ ```typescript
1100
+ import { defineState } from '@wcstack/state';
1101
+
1102
+ export default defineState({
1103
+ count: 0,
1104
+ users: [] as { name: string; age: number }[],
1105
+
1106
+ increment() {
1107
+ this.count++; // ✅ number
1108
+ this["users.*.name"]; // ✅ string(ドットパス型解決)
1109
+ this.$getAll("users.*.age", []); // ✅ API メソッド
1110
+ },
1111
+
1112
+ get "users.*.ageCategory"() {
1113
+ return this["users.*.age"] < 25 ? "Young" : "Adult";
1114
+ }
1115
+ });
1116
+ ```
1117
+
1118
+ ユーティリティ型 `WcsPaths<T>` と `WcsPathValue<T, P>` もエクスポートされます。詳細は [docs/define-state.ja.md](docs/define-state.ja.md) を参照してください。
1119
+
1094
1120
  ## API リファレンス
1095
1121
 
1096
1122
  ### `bootstrapState()`
package/README.md CHANGED
@@ -75,6 +75,7 @@ That's it. No build, no bootstrap code, no framework.
75
75
  - **Multiple state sources** — JSON, JS module, inline script, API, attribute
76
76
  - **SVG support** — full binding support inside `<svg>` elements
77
77
  - **Lifecycle hooks** — `$connectedCallback` / `$disconnectedCallback` / `$updatedCallback`, plus `$stateReadyCallback` for Web Components
78
+ - **TypeScript support** — `defineState()` for typed state definitions with dot-path autocompletion ([details](docs/define-state.md))
78
79
  - **Zero dependencies** — no runtime dependencies
79
80
 
80
81
  ## Installation
@@ -1091,6 +1092,31 @@ All options with defaults:
1091
1092
  | `debug` | `false` | Debug mode |
1092
1093
  | `enableMustache` | `true` | Enable `{{ }}` syntax |
1093
1094
 
1095
+ ## TypeScript Support
1096
+
1097
+ `defineState()` wraps your state object and provides type-safe `this` inside methods and getters — with zero runtime cost (identity function).
1098
+
1099
+ ```typescript
1100
+ import { defineState } from '@wcstack/state';
1101
+
1102
+ export default defineState({
1103
+ count: 0,
1104
+ users: [] as { name: string; age: number }[],
1105
+
1106
+ increment() {
1107
+ this.count++; // ✅ number
1108
+ this["users.*.name"]; // ✅ string (dot-path resolution)
1109
+ this.$getAll("users.*.age", []); // ✅ API method
1110
+ },
1111
+
1112
+ get "users.*.ageCategory"() {
1113
+ return this["users.*.age"] < 25 ? "Young" : "Adult";
1114
+ }
1115
+ });
1116
+ ```
1117
+
1118
+ Utility types `WcsPaths<T>` and `WcsPathValue<T, P>` are also exported for advanced use cases. See [docs/define-state.md](docs/define-state.md) for full documentation.
1119
+
1094
1120
  ## API Reference
1095
1121
 
1096
1122
  ### `bootstrapState()`
package/dist/index.d.ts CHANGED
@@ -16,5 +16,221 @@ interface IWritableConfig {
16
16
 
17
17
  declare function bootstrapState(config?: IWritableConfig): void;
18
18
 
19
- export { bootstrapState };
20
- export type { IWritableConfig, IWritableTagNames };
19
+ /**
20
+ * defineState.ts
21
+ *
22
+ * 状態オブジェクトに型付けを提供するためのユーティリティ。
23
+ * defineState() はアイデンティティ関数で、ThisType<> を付与することで
24
+ * メソッド・computed getter 内の this に型補完を提供する。
25
+ *
26
+ * テンプレートリテラル型によるドットパスの型解決:
27
+ * - WcsPaths<T> : T から生成される全ドットパスの union
28
+ * - WcsPathValue<T,P>: パス P に対応する値の型
29
+ * - WcsPathAccessor<T>: ブラケットアクセス用マップ型
30
+ */
31
+ /**
32
+ * `any` 型を検出する。
33
+ * `0 extends (1 & T)` は T が `any` の場合のみ true になる。
34
+ */
35
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
36
+ /**
37
+ * T がドットパス再帰の対象となる「プレーンなデータオブジェクト」かどうかを判定する。
38
+ * プリミティブ、組み込みオブジェクト (Date, Map 等)、関数、配列、any は除外。
39
+ */
40
+ type IsPlainObject<T> = IsAny<T> extends true ? false : T extends string | number | boolean | null | undefined | symbol | bigint | Function | Date | RegExp | Error | Map<any, any> | Set<any> | WeakMap<any, any> | WeakSet<any> | Promise<any> | readonly any[] ? false : T extends Record<string, any> ? true : false;
41
+ /**
42
+ * T のキーのうち、関数でないもの(データプロパティ・computed getter)を抽出する。
43
+ * メソッド(イベントハンドラ等)はドットパスの対象外。
44
+ * any 型のプロパティは除外せず保持する。
45
+ */
46
+ type DataKeys<T> = {
47
+ [K in keyof T & string]: IsAny<T[K]> extends true ? K : T[K] extends Function ? never : K;
48
+ }[keyof T & string];
49
+ /**
50
+ * 型 T から生成される全てのドットパスの union。
51
+ * 配列プロパティはワイルドカード `*` を使用: `items.*.name`
52
+ *
53
+ * 再帰の深さは最大4レベルに制限(コンパイル性能の確保)。
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * type S = {
58
+ * count: number;
59
+ * users: { name: string; age: number }[];
60
+ * cart: { items: { price: number }[] };
61
+ * };
62
+ * type P = WcsPaths<S>;
63
+ * // = "count" | "users" | "users.*" | "users.*.name" | "users.*.age"
64
+ * // | "cart" | "cart.items" | "cart.items.*" | "cart.items.*.price"
65
+ * ```
66
+ */
67
+ type WcsPaths<T, Depth extends readonly any[] = []> = Depth["length"] extends 4 ? never : {
68
+ [K in DataKeys<T>]: K | (T[K] extends readonly (infer E)[] ? IsPlainObject<E> extends true ? `${K}.*` | WcsSubPaths<E, `${K}.*.`, [...Depth, 0]> : `${K}.*` : IsPlainObject<T[K]> extends true ? WcsSubPaths<T[K], `${K}.`, [...Depth, 0]> : never);
69
+ }[DataKeys<T>];
70
+ /** @internal プレフィックス付きサブパスの生成ヘルパー */
71
+ type WcsSubPaths<T, Prefix extends string, Depth extends readonly any[]> = WcsPaths<T, Depth> extends infer P extends string ? `${Prefix}${P}` : never;
72
+ /**
73
+ * ドットパス P に対応する値の型を T から解決する。
74
+ *
75
+ * 解決順序:
76
+ * 1. T の直接キー(computed getter 含む)
77
+ * 2. `K.*` → 配列要素型
78
+ * 3. `K.rest` → オブジェクト/配列のネストを再帰的に辿る
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * type S = { cart: { items: { price: number; qty: number }[] } };
83
+ * type V1 = WcsPathValue<S, "cart.items.*.price">; // number
84
+ * type V2 = WcsPathValue<S, "cart.items.*">; // { price: number; qty: number }
85
+ * type V3 = WcsPathValue<S, "cart">; // { items: ... }
86
+ * ```
87
+ */
88
+ type WcsPathValue<T, P extends string> = P extends keyof T ? T[P] : P extends `${infer K}.*` ? K extends keyof T ? T[K] extends readonly (infer E)[] ? E : never : never : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? T[K] extends readonly (infer E)[] ? Rest extends `*.${infer SubRest}` ? WcsPathValue<E, SubRest> : Rest extends "*" ? E : never : T[K] extends Record<string, any> ? WcsPathValue<T[K], Rest> : never : never : never;
89
+ /**
90
+ * 全ドットパスに対する型付きブラケットアクセスを提供するマップ型。
91
+ *
92
+ * `this["users.*.name"]` のようなアクセスに対して、
93
+ * WcsPaths で生成されたパスに対応する値の型を返す。
94
+ */
95
+ type WcsPathAccessor<T> = {
96
+ [P in WcsPaths<T>]: WcsPathValue<T, P>;
97
+ };
98
+ /**
99
+ * `<wcs-state>` の Proxy 経由で提供されるAPIメソッド。
100
+ * state定義オブジェクト内のメソッド・getter で `this.` 経由で利用可能。
101
+ */
102
+ interface WcsStateApi {
103
+ /**
104
+ * ワイルドカードを含むパスにマッチする全要素を配列で取得する。
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * get "cart.totalPrice"() {
109
+ * return this.$getAll("cart.items.*.price", []).reduce((sum, v) => sum + v, 0);
110
+ * }
111
+ * ```
112
+ */
113
+ $getAll<V = any>(path: string, defaultValue?: V[]): V[];
114
+ /**
115
+ * 指定パスの更新を手動でトリガーする。
116
+ * Proxy の set トラップを経由せずに内部状態を変更した場合に使用。
117
+ */
118
+ $postUpdate(path: string): void;
119
+ /**
120
+ * パスとインデックス配列を指定して、ワイルドカードを解決した値を取得・設定する。
121
+ *
122
+ * @param path - ワイルドカードを含むパス
123
+ * @param indexes - 各ワイルドカード階層のインデックス
124
+ * @param value - 設定する値(省略時は取得)
125
+ */
126
+ $resolve(path: string, indexes: number[], value?: any): any;
127
+ /**
128
+ * 指定パスへの依存関係を明示的に登録する。
129
+ * computed getter 内で動的にパスを組み立てる場合に使用。
130
+ */
131
+ $trackDependency(path: string): void;
132
+ /** `<wcs-state>` 要素への参照 */
133
+ readonly $stateElement: HTMLElement;
134
+ readonly $1: number;
135
+ readonly $2: number;
136
+ readonly $3: number;
137
+ readonly $4: number;
138
+ readonly $5: number;
139
+ readonly $6: number;
140
+ readonly $7: number;
141
+ readonly $8: number;
142
+ readonly $9: number;
143
+ }
144
+ /**
145
+ * state定義オブジェクト内の `this` の型。
146
+ *
147
+ * - `T` のプロパティに型付きでアクセス可能(直接キー)
148
+ * - `WcsPathAccessor<T>` によるネストされたドットパスの型付きアクセス
149
+ * - `WcsStateApi` のメソッド ($getAll, $postUpdate 等) にアクセス可能
150
+ * - 動的パス (`this[\`items.${i}.name\`]`) は型チェック対象外(キャストが必要)
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * defineState({
155
+ * count: 0,
156
+ * users: [] as { name: string; age: number }[],
157
+ * increment() {
158
+ * this.count++; // number
159
+ * this["users.*.name"]; // string (パス型解決)
160
+ * this.$getAll("path", []); // API
161
+ * }
162
+ * });
163
+ * ```
164
+ */
165
+ type WcsThis<T> = T & WcsStateApi & WcsPathAccessor<T>;
166
+ /**
167
+ * `<wcs-state>` 用の型付き状態オブジェクトを定義する。
168
+ *
169
+ * ランタイムではアイデンティティ関数(引数をそのまま返す)として動作し、
170
+ * コストはゼロ。TypeScript の `ThisType<>` を利用して、メソッド・getter 内の
171
+ * `this` に型補完を提供する。
172
+ *
173
+ * ### 基本的な使い方 (TypeScript)
174
+ * ```ts
175
+ * import { defineState } from '@wcstack/state';
176
+ *
177
+ * export default defineState({
178
+ * count: 0,
179
+ * users: [] as { name: string; age: number }[],
180
+ *
181
+ * increment() {
182
+ * this.count++; // ✅ number
183
+ * this["users.*.name"]; // ✅ string (ドットパス型解決)
184
+ * },
185
+ *
186
+ * get "users.*.ageCategory"() {
187
+ * return this["users.*.age"] < 25 ? "Young" : "Adult";
188
+ * }
189
+ * });
190
+ * ```
191
+ *
192
+ * ### JavaScript (JSDoc)
193
+ * ```js
194
+ * import { defineState } from '@wcstack/state';
195
+ *
196
+ * export default defineState({
197
+ * count: 0,
198
+ * increment() {
199
+ * this.count++; // ✅ JSDoc + tsconfig checkJs で型補完
200
+ * }
201
+ * });
202
+ * ```
203
+ *
204
+ * ### HTML インラインスクリプト
205
+ * ```html
206
+ * <wcs-state>
207
+ * <script type="module">
208
+ * import { defineState } from '@wcstack/state';
209
+ * export default defineState({
210
+ * count: 0,
211
+ * increment() { this.count++; }
212
+ * });
213
+ * </script>
214
+ * </wcs-state>
215
+ * ```
216
+ *
217
+ * ### ライフサイクルコールバック
218
+ * ```ts
219
+ * export default defineState({
220
+ * data: null,
221
+ * async $connectedCallback() {
222
+ * this.data = await fetch('/api/data').then(r => r.json());
223
+ * },
224
+ * $disconnectedCallback() {
225
+ * // cleanup
226
+ * },
227
+ * $updatedCallback() {
228
+ * // called after DOM update
229
+ * }
230
+ * });
231
+ * ```
232
+ */
233
+ declare function defineState<T extends Record<string, any>>(definition: T & ThisType<WcsThis<T>>): T;
234
+
235
+ export { bootstrapState, defineState };
236
+ export type { IWritableConfig, IWritableTagNames, WcsPathValue, WcsPaths, WcsStateApi, WcsThis };
package/dist/index.esm.js CHANGED
@@ -1121,6 +1121,7 @@ function parseFilterArgs(argsText) {
1121
1121
  const args = [];
1122
1122
  let current = '';
1123
1123
  let inQuote = null;
1124
+ let hasQuote = false;
1124
1125
  for (let i = 0; i < argsText.length; i++) {
1125
1126
  const char = argsText[i];
1126
1127
  if (inQuote) {
@@ -1133,17 +1134,20 @@ function parseFilterArgs(argsText) {
1133
1134
  }
1134
1135
  else if (char === '"' || char === "'") {
1135
1136
  inQuote = char;
1137
+ hasQuote = true;
1136
1138
  }
1137
1139
  else if (char === ',') {
1138
1140
  args.push(current.trim());
1139
1141
  current = '';
1142
+ hasQuote = false;
1140
1143
  }
1141
1144
  else {
1142
1145
  current += char;
1143
1146
  }
1144
1147
  }
1145
- if (current.trim()) {
1146
- args.push(current.trim());
1148
+ const last = current.trim();
1149
+ if (last || hasQuote) {
1150
+ args.push(last);
1147
1151
  }
1148
1152
  return args;
1149
1153
  }
@@ -1670,10 +1674,9 @@ function isPossibleTwoWay(node, propName) {
1670
1674
  if (typeof customClass === "undefined") {
1671
1675
  raiseError(`Custom element <${customTagName}> is not defined. Cannot determine if property "${propName}" is suitable for two-way binding.`);
1672
1676
  }
1673
- const reactivityInfo = customClass.wcsReactivity;
1674
- if (reactivityInfo) {
1675
- if (reactivityInfo.properties?.includes(propName)
1676
- || (reactivityInfo.propertyMap?.[propName] ?? null) !== null) {
1677
+ const bindable = customClass.wcBindable;
1678
+ if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
1679
+ if (bindable.properties.some(p => p.name === propName)) {
1677
1680
  return true;
1678
1681
  }
1679
1682
  }
@@ -1683,28 +1686,27 @@ function isPossibleTwoWay(node, propName) {
1683
1686
 
1684
1687
  const handlerByHandlerKey$2 = new Map();
1685
1688
  const bindingSetByHandlerKey$2 = new Map();
1686
- function getHandlerKey$2(binding, eventName) {
1689
+ const DEFAULT_GETTER = (e) => e.detail;
1690
+ function getHandlerKey$2(binding, eventName, hasGetter) {
1687
1691
  const filterKey = binding.inFilters.map(f => f.filterName + '(' + f.args.join(',') + ')').join('|');
1688
- return `${binding.stateName}::${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}`;
1692
+ return `${binding.stateName}::${binding.propName}::${binding.statePathName}::${eventName}::${filterKey}::${hasGetter ? 'g' : 'n'}`;
1689
1693
  }
1690
1694
  function getEventName$2(binding) {
1691
1695
  const tagName = binding.node.tagName.toLowerCase();
1692
1696
  // 1.default event name
1693
1697
  let eventName = (tagName === 'select') ? 'change' : 'input';
1694
- // 2.protocol
1698
+ // 2.wcBindable protocol
1695
1699
  const customTagName = getCustomElement(binding.node);
1696
1700
  if (customTagName !== null) {
1697
1701
  const customClass = customElements.get(customTagName);
1698
1702
  if (typeof customClass === "undefined") {
1699
1703
  raiseError(`Custom element <${customTagName}> is not defined. Cannot determine event name for two-way binding.`);
1700
1704
  }
1701
- const reactivityInfo = customClass.wcsReactivity;
1702
- if (reactivityInfo) {
1703
- if (reactivityInfo.properties?.includes(binding.propName)) {
1704
- eventName = reactivityInfo.defaultEvent;
1705
- }
1706
- if (reactivityInfo.propertyMap?.[binding.propName]) {
1707
- eventName = reactivityInfo.propertyMap[binding.propName];
1705
+ const bindable = customClass.wcBindable;
1706
+ if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
1707
+ const propDesc = bindable.properties.find(p => p.name === binding.propName);
1708
+ if (propDesc) {
1709
+ eventName = propDesc.event;
1708
1710
  }
1709
1711
  }
1710
1712
  }
@@ -1716,17 +1718,39 @@ function getEventName$2(binding) {
1716
1718
  }
1717
1719
  return eventName;
1718
1720
  }
1719
- const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilters) => (event) => {
1721
+ function getValueGetter(binding) {
1722
+ const customTagName = getCustomElement(binding.node);
1723
+ if (customTagName !== null) {
1724
+ const customClass = customElements.get(customTagName);
1725
+ if (customClass) {
1726
+ const bindable = customClass.wcBindable;
1727
+ if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
1728
+ const propDesc = bindable.properties.find(p => p.name === binding.propName);
1729
+ if (propDesc) {
1730
+ return propDesc.getter ?? DEFAULT_GETTER;
1731
+ }
1732
+ }
1733
+ }
1734
+ }
1735
+ return null;
1736
+ }
1737
+ const twowayEventHandlerFunction = (stateName, propName, statePathName, inFilters, valueGetter) => (event) => {
1720
1738
  const node = event.target;
1721
1739
  if (node === null) {
1722
1740
  console.warn(`[@wcstack/state] event.target is null.`);
1723
1741
  return;
1724
1742
  }
1725
- if (!(propName in node)) {
1726
- console.warn(`[@wcstack/state] Property "${propName}" does not exist on target element.`);
1727
- return;
1743
+ let newValue;
1744
+ if (valueGetter !== null) {
1745
+ newValue = valueGetter(event);
1746
+ }
1747
+ else {
1748
+ if (!(propName in node)) {
1749
+ console.warn(`[@wcstack/state] Property "${propName}" does not exist on target element.`);
1750
+ return;
1751
+ }
1752
+ newValue = node[propName];
1728
1753
  }
1729
- const newValue = node[propName];
1730
1754
  let filteredNewValue = newValue;
1731
1755
  for (const filter of inFilters) {
1732
1756
  filteredNewValue = filter.filterFn(filteredNewValue);
@@ -1756,10 +1780,11 @@ function attachTwowayEventHandler(binding) {
1756
1780
  }
1757
1781
  if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf('ro') === -1) {
1758
1782
  const eventName = getEventName$2(binding);
1759
- const key = getHandlerKey$2(binding, eventName);
1783
+ const valueGetter = getValueGetter(binding);
1784
+ const key = getHandlerKey$2(binding, eventName, valueGetter !== null);
1760
1785
  let twowayEventHandler = handlerByHandlerKey$2.get(key);
1761
1786
  if (typeof twowayEventHandler === "undefined") {
1762
- twowayEventHandler = twowayEventHandlerFunction(binding.stateName, binding.propName, binding.statePathName, binding.inFilters);
1787
+ twowayEventHandler = twowayEventHandlerFunction(binding.stateName, binding.propName, binding.statePathName, binding.inFilters, valueGetter);
1763
1788
  handlerByHandlerKey$2.set(key, twowayEventHandler);
1764
1789
  }
1765
1790
  binding.node.addEventListener(eventName, twowayEventHandler);
@@ -2855,7 +2880,18 @@ function applyChangeToProperty(binding, _context, newValue) {
2855
2880
  if (propSegments.length === 1) {
2856
2881
  const firstSegment = propSegments[0];
2857
2882
  if (element[firstSegment] !== newValue) {
2858
- element[firstSegment] = newValue;
2883
+ try {
2884
+ element[firstSegment] = newValue;
2885
+ }
2886
+ catch (error) {
2887
+ if (config.debug) {
2888
+ console.warn(`Failed to set property '${firstSegment}' on element.`, {
2889
+ element,
2890
+ newValue,
2891
+ error
2892
+ });
2893
+ }
2894
+ }
2859
2895
  }
2860
2896
  return;
2861
2897
  }
@@ -2881,7 +2917,20 @@ function applyChangeToProperty(binding, _context, newValue) {
2881
2917
  }
2882
2918
  return;
2883
2919
  }
2884
- subObject[propSegments[propSegments.length - 1]] = newValue;
2920
+ try {
2921
+ subObject[propSegments[propSegments.length - 1]] = newValue;
2922
+ }
2923
+ catch (error) {
2924
+ if (config.debug) {
2925
+ console.warn(`Failed to set property on sub-object.`, {
2926
+ element,
2927
+ propSegments,
2928
+ oldValue,
2929
+ newValue,
2930
+ error
2931
+ });
2932
+ }
2933
+ }
2885
2934
  }
2886
2935
  }
2887
2936
 
@@ -2965,19 +3014,50 @@ const applyChangeByBindingType = {
2965
3014
  "radio": applyChangeToRadio,
2966
3015
  "checkbox": applyChangeToCheckbox,
2967
3016
  };
3017
+ const fnByBinding = new WeakMap();
3018
+ const deferredSelectBindingByBinding = new WeakMap();
2968
3019
  function _applyChange(binding, context) {
2969
3020
  const value = getValue(context.state, binding);
2970
3021
  const filteredValue = getFilteredValue(value, binding.outFilters);
2971
- let fn = applyChangeByBindingType[binding.bindingType];
3022
+ if (deferredSelectBindingByBinding.get(binding) === true) {
3023
+ context.deferredSelectBindings.push({ binding, value: filteredValue });
3024
+ return;
3025
+ }
3026
+ let fn = fnByBinding.get(binding);
3027
+ if (typeof fn !== 'undefined') {
3028
+ fn(binding, context, filteredValue);
3029
+ return;
3030
+ }
3031
+ if (fnByBinding.has(binding)) {
3032
+ if (isWebComponentComplete(binding.replaceNode, context.stateElement)) {
3033
+ fn = applyChangeToWebComponent;
3034
+ fnByBinding.set(binding, fn); // 確定したのでキャッシュ
3035
+ }
3036
+ else {
3037
+ fn = applyChangeToProperty;
3038
+ }
3039
+ fn(binding, context, filteredValue);
3040
+ return;
3041
+ }
3042
+ fn = applyChangeByBindingType[binding.bindingType];
2972
3043
  if (typeof fn === 'undefined') {
2973
3044
  const firstSegment = binding.propSegments[0];
2974
3045
  fn = applyChangeByFirstSegment[firstSegment];
3046
+ fnByBinding.set(binding, fn);
2975
3047
  if (typeof fn === 'undefined') {
2976
- if (isWebComponentComplete(binding.replaceNode, context.stateElement)) {
2977
- fn = applyChangeToWebComponent;
3048
+ const customTag = getCustomElement(binding.replaceNode);
3049
+ if (customTag) {
3050
+ if (isWebComponentComplete(binding.replaceNode, context.stateElement)) {
3051
+ fn = applyChangeToWebComponent;
3052
+ fnByBinding.set(binding, fn); // 確定したのでキャッシュ
3053
+ }
3054
+ else {
3055
+ fn = applyChangeToProperty;
3056
+ }
2978
3057
  }
2979
3058
  else {
2980
3059
  fn = applyChangeToProperty;
3060
+ fnByBinding.set(binding, fn);
2981
3061
  }
2982
3062
  }
2983
3063
  }
@@ -2987,6 +3067,7 @@ function _applyChange(binding, context) {
2987
3067
  const propName = binding.propSegments[0];
2988
3068
  if (propName === 'value' || propName === 'selectedIndex') {
2989
3069
  context.deferredSelectBindings.push({ binding, value: filteredValue });
3070
+ deferredSelectBindingByBinding.set(binding, true);
2990
3071
  return;
2991
3072
  }
2992
3073
  }
@@ -5532,7 +5613,7 @@ class State extends HTMLElement {
5532
5613
  else {
5533
5614
  const script = this.querySelector('script[type="module"]');
5534
5615
  if (script) {
5535
- this._state = await loadFromInnerScript(script, `state#${this._name}`);
5616
+ this._state = await loadFromInnerScript(script, `${this._name}`);
5536
5617
  }
5537
5618
  else {
5538
5619
  const timerId = setTimeout(() => {
@@ -5764,5 +5845,91 @@ function bootstrapState(config) {
5764
5845
  registerComponents();
5765
5846
  }
5766
5847
 
5767
- export { bootstrapState };
5848
+ /**
5849
+ * defineState.ts
5850
+ *
5851
+ * 状態オブジェクトに型付けを提供するためのユーティリティ。
5852
+ * defineState() はアイデンティティ関数で、ThisType<> を付与することで
5853
+ * メソッド・computed getter 内の this に型補完を提供する。
5854
+ *
5855
+ * テンプレートリテラル型によるドットパスの型解決:
5856
+ * - WcsPaths<T> : T から生成される全ドットパスの union
5857
+ * - WcsPathValue<T,P>: パス P に対応する値の型
5858
+ * - WcsPathAccessor<T>: ブラケットアクセス用マップ型
5859
+ */
5860
+ // ============================================================
5861
+ // defineState — 型付き状態定義関数
5862
+ // ============================================================
5863
+ /**
5864
+ * `<wcs-state>` 用の型付き状態オブジェクトを定義する。
5865
+ *
5866
+ * ランタイムではアイデンティティ関数(引数をそのまま返す)として動作し、
5867
+ * コストはゼロ。TypeScript の `ThisType<>` を利用して、メソッド・getter 内の
5868
+ * `this` に型補完を提供する。
5869
+ *
5870
+ * ### 基本的な使い方 (TypeScript)
5871
+ * ```ts
5872
+ * import { defineState } from '@wcstack/state';
5873
+ *
5874
+ * export default defineState({
5875
+ * count: 0,
5876
+ * users: [] as { name: string; age: number }[],
5877
+ *
5878
+ * increment() {
5879
+ * this.count++; // ✅ number
5880
+ * this["users.*.name"]; // ✅ string (ドットパス型解決)
5881
+ * },
5882
+ *
5883
+ * get "users.*.ageCategory"() {
5884
+ * return this["users.*.age"] < 25 ? "Young" : "Adult";
5885
+ * }
5886
+ * });
5887
+ * ```
5888
+ *
5889
+ * ### JavaScript (JSDoc)
5890
+ * ```js
5891
+ * import { defineState } from '@wcstack/state';
5892
+ *
5893
+ * export default defineState({
5894
+ * count: 0,
5895
+ * increment() {
5896
+ * this.count++; // ✅ JSDoc + tsconfig checkJs で型補完
5897
+ * }
5898
+ * });
5899
+ * ```
5900
+ *
5901
+ * ### HTML インラインスクリプト
5902
+ * ```html
5903
+ * <wcs-state>
5904
+ * <script type="module">
5905
+ * import { defineState } from '@wcstack/state';
5906
+ * export default defineState({
5907
+ * count: 0,
5908
+ * increment() { this.count++; }
5909
+ * });
5910
+ * </script>
5911
+ * </wcs-state>
5912
+ * ```
5913
+ *
5914
+ * ### ライフサイクルコールバック
5915
+ * ```ts
5916
+ * export default defineState({
5917
+ * data: null,
5918
+ * async $connectedCallback() {
5919
+ * this.data = await fetch('/api/data').then(r => r.json());
5920
+ * },
5921
+ * $disconnectedCallback() {
5922
+ * // cleanup
5923
+ * },
5924
+ * $updatedCallback() {
5925
+ * // called after DOM update
5926
+ * }
5927
+ * });
5928
+ * ```
5929
+ */
5930
+ function defineState(definition) {
5931
+ return definition;
5932
+ }
5933
+
5934
+ export { bootstrapState, defineState };
5768
5935
  //# sourceMappingURL=index.esm.js.map