@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 +26 -0
- package/README.md +26 -0
- package/dist/index.d.ts +218 -2
- package/dist/index.esm.js +197 -30
- 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 +71 -71
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
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
|
|
1674
|
-
if (
|
|
1675
|
-
if (
|
|
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
|
-
|
|
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
|
|
1702
|
-
if (
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2977
|
-
|
|
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,
|
|
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
|
-
|
|
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
|