@ryupold/vode 1.7.4 → 1.8.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 +17 -4
- package/dist/{vode.amd.min.js → vode.cjs.min.js} +4 -15
- package/dist/vode.d.ts +106 -36
- package/dist/vode.es5.min.js +6 -17
- package/dist/vode.js +22 -81
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +22 -81
- package/index.ts +1 -0
- package/package.json +4 -4
- package/src/merge-props.ts +32 -0
- package/src/state-context.ts +7 -214
- package/tsconfig.json +17 -13
package/src/state-context.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { AnimatedPatch, DeepPartial, PatchableState, RenderPatch } from "./vode.
|
|
|
4
4
|
* State context for type-safe access and manipulation of nested state paths
|
|
5
5
|
* while still be able to access the parent state.
|
|
6
6
|
*/
|
|
7
|
-
export interface StateContext<S extends PatchableState, SubState> extends
|
|
7
|
+
export interface StateContext<S extends PatchableState, SubState> extends SubContext<SubState> {
|
|
8
8
|
/**
|
|
9
9
|
* parent state
|
|
10
10
|
* @see PatchableState<S>
|
|
@@ -15,7 +15,7 @@ export interface StateContext<S extends PatchableState, SubState> extends SubSta
|
|
|
15
15
|
/**
|
|
16
16
|
* State context for type-safe access and manipulation of nested sub-state values without knowledge of the parent state.
|
|
17
17
|
*/
|
|
18
|
-
export interface
|
|
18
|
+
export interface SubContext<SubState> {
|
|
19
19
|
/**
|
|
20
20
|
* Reads the current value of the substate if it exists.
|
|
21
21
|
*
|
|
@@ -42,14 +42,14 @@ export interface SubStateContext<SubState> {
|
|
|
42
42
|
|
|
43
43
|
export type ProxyStateContext<S extends PatchableState, SubState> = StateContext<S, SubState> & {
|
|
44
44
|
[K in keyof SubState]-?: SubState[K] extends object
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
? ProxyStateContext<S, SubState[K]>
|
|
46
|
+
: StateContext<S, SubState[K]>
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
export type ProxySubContext<SubState> =
|
|
49
|
+
export type ProxySubContext<SubState> = SubContext<SubState> & {
|
|
50
50
|
[K in keyof SubState]-?: SubState[K] extends object
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
? ProxySubContext<SubState[K]>
|
|
52
|
+
: SubContext<SubState[K]>
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -178,210 +178,3 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
|
|
|
178
178
|
put(value: SubState | DeepPartial<SubState> | null | undefined): void { throw 'implemented in ctor' }
|
|
179
179
|
patch(value: SubState | DeepPartial<SubState> | DeepPartial<SubState>[] | null | undefined): void { throw 'implemented in ctor' }
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Provides type-safe access to sub-state with fetch & store delegate functions.
|
|
184
|
-
* Implementer is responsible for reading/writing the sub-state correctly.
|
|
185
|
-
*
|
|
186
|
-
* **When to use:**
|
|
187
|
-
* - State structure is dynamic or complex
|
|
188
|
-
* - You need custom logic for accessing nested state
|
|
189
|
-
* - You want to encapsulate access logic outside of parent state
|
|
190
|
-
*
|
|
191
|
-
* **When to avoid:**
|
|
192
|
-
* - Simple, static state structures
|
|
193
|
-
* - You want automatic path-based access (use KeyStateContext instead)
|
|
194
|
-
* - Learning vode for the first time (start simpler)
|
|
195
|
-
*/
|
|
196
|
-
export class DelegateStateContext<S extends PatchableState, SubState>
|
|
197
|
-
implements StateContext<S, SubState> {
|
|
198
|
-
constructor(
|
|
199
|
-
public readonly state: S,
|
|
200
|
-
|
|
201
|
-
public readonly get: () => SubState | undefined,
|
|
202
|
-
|
|
203
|
-
public readonly put: (value: SubState | DeepPartial<SubState> | undefined | null) => void,
|
|
204
|
-
|
|
205
|
-
public readonly patch: (value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null) => void,
|
|
206
|
-
) {
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* @deprecated use proxy state context instead
|
|
213
|
-
* Generates dot-notation path strings for all nested properties in an object type.
|
|
214
|
-
*
|
|
215
|
-
* @example
|
|
216
|
-
* type User = { profile: { settings: { theme: string } } };
|
|
217
|
-
* type Paths = KeyPath<User>; // "profile" | "profile.settings" | "profile.settings.theme"
|
|
218
|
-
*/
|
|
219
|
-
export type KeyPath<ObjectType extends object> =
|
|
220
|
-
{ [Key in keyof ObjectType & (string | number)]:
|
|
221
|
-
NonNullable<ObjectType[Key]> extends object
|
|
222
|
-
? `${Key}` | `${Key}.${KeyPath<NonNullable<ObjectType[Key]>>}`
|
|
223
|
-
: `${Key}`
|
|
224
|
-
}[keyof ObjectType & (string | number)];
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @deprecated use proxy state context instead
|
|
228
|
-
* Extracts the value type at a given dot-notation path in an object type.
|
|
229
|
-
*
|
|
230
|
-
* @example
|
|
231
|
-
* type User = { profile: { settings: { theme: string } } };
|
|
232
|
-
* type Theme = PathValue<User, "profile.settings.theme">; // string
|
|
233
|
-
*/
|
|
234
|
-
export type PathValue<T, P extends string> =
|
|
235
|
-
P extends `${infer Key}.${infer Rest}`
|
|
236
|
-
? Key extends keyof T
|
|
237
|
-
? PathValue<NonNullable<T[Key]>, Rest>
|
|
238
|
-
: never
|
|
239
|
-
: P extends keyof T
|
|
240
|
-
? T[P]
|
|
241
|
-
: never;
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* @deprecated use proxy state context instead
|
|
245
|
-
* Maps valid paths in an object type to paths that resolve to a specific substate type.
|
|
246
|
-
* Used for type-safe path constraints in StateContext.
|
|
247
|
-
* Ensures exact type matching (not just compatibility).
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* type User = { profile: { name: string, settings: { theme: string } } };
|
|
251
|
-
* type SettingsPaths = KeyToSubState<User, { theme: string }>; // "profile.settings"
|
|
252
|
-
* type InvalidPath = KeyToSubState<User, { theme: string }, "profile">; // never (type mismatch)
|
|
253
|
-
*/
|
|
254
|
-
export type KeyToSubState<S extends object, Sub, K = KeyPath<S>> =
|
|
255
|
-
K extends KeyPath<S>
|
|
256
|
-
? [NonNullable<PathValue<S, K>>] extends [Sub]
|
|
257
|
-
? [Sub] extends [NonNullable<PathValue<S, K>>]
|
|
258
|
-
? K
|
|
259
|
-
: never
|
|
260
|
-
: never
|
|
261
|
-
: never;
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* @deprecated use {context()} instead
|
|
265
|
-
* Provides type-safe access to deeply nested state with path-based operations.
|
|
266
|
-
*
|
|
267
|
-
* **When to use:**
|
|
268
|
-
* - State is deeply nested
|
|
269
|
-
* - Multiple components access the same nested path
|
|
270
|
-
* - You need type safety for nested updates
|
|
271
|
-
*
|
|
272
|
-
* **When to avoid:**
|
|
273
|
-
* - Shallow state structures for main state
|
|
274
|
-
* - State structure changes frequently
|
|
275
|
-
* - Learning vode for the first time (start simpler)
|
|
276
|
-
*
|
|
277
|
-
* @template S - The root state type (must extend PatchableState)
|
|
278
|
-
* @template SubState - The type of the nested state being accessed
|
|
279
|
-
*
|
|
280
|
-
* @example
|
|
281
|
-
* ```typescript
|
|
282
|
-
* const state = createState({
|
|
283
|
-
* user: {
|
|
284
|
-
* profile: {
|
|
285
|
-
* settings: { theme: 'dark', lang: 'en' }
|
|
286
|
-
* }
|
|
287
|
-
* }
|
|
288
|
-
* });
|
|
289
|
-
* app(element, state, (s) => [DIV]);
|
|
290
|
-
*
|
|
291
|
-
* // Create a context for the nested settings
|
|
292
|
-
* const settingsCtx = new KeyStateContext(state, 'user.profile.settings');
|
|
293
|
-
*
|
|
294
|
-
* // Read current value
|
|
295
|
-
* const settings = settingsCtx.get(); // { theme: 'dark', lang: 'en' }
|
|
296
|
-
*
|
|
297
|
-
* // Update and trigger render
|
|
298
|
-
* settingsCtx.patch({ theme: 'light' });
|
|
299
|
-
*
|
|
300
|
-
* // Update without render (silent mutation)
|
|
301
|
-
* settingsCtx.put({ lang: 'de' });
|
|
302
|
-
* state.patch({}); // trigger render manually later
|
|
303
|
-
* ```
|
|
304
|
-
*/
|
|
305
|
-
export class KeyStateContext<S extends PatchableState, SubState>
|
|
306
|
-
implements StateContext<S, SubState> {
|
|
307
|
-
private readonly keys: string[];
|
|
308
|
-
|
|
309
|
-
constructor(
|
|
310
|
-
public readonly state: S,
|
|
311
|
-
public readonly path: KeyToSubState<S, SubState>
|
|
312
|
-
) {
|
|
313
|
-
this.keys = path.split('.');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
get(): SubState | undefined {
|
|
317
|
-
const keys = this.keys;
|
|
318
|
-
let raw = this.state ? (<any>this.state)[keys[0]] : undefined;
|
|
319
|
-
for (let i = 1; i < keys.length && !!raw; i++) {
|
|
320
|
-
raw = raw[keys[i]];
|
|
321
|
-
}
|
|
322
|
-
return raw;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
put(value: SubState | DeepPartial<SubState> | undefined | null) {
|
|
326
|
-
this.putDeep(value, this.state);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
patch(value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null) {
|
|
330
|
-
if (Array.isArray(value)) {
|
|
331
|
-
const animation: AnimatedPatch<S> = [];
|
|
332
|
-
for (const v of value) {
|
|
333
|
-
animation.push(this.createPatch(v));
|
|
334
|
-
}
|
|
335
|
-
this.state.patch(animation);
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
this.state.patch(this.createPatch(value as DeepPartial<SubState>));
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Creates a render-patch for the parent state by setting a nested sub-state value while creating necessary structure.
|
|
344
|
-
*
|
|
345
|
-
* @example
|
|
346
|
-
* ```typescript
|
|
347
|
-
* const ctx = new KeyStateContext(state, 'user.profile.settings');
|
|
348
|
-
* const patch = ctx.createPatch({ theme: 'light' });
|
|
349
|
-
* // patch is { user: { profile: { settings: { theme: 'light' } } } }
|
|
350
|
-
* ```
|
|
351
|
-
*
|
|
352
|
-
* @param value
|
|
353
|
-
* @returns {{key-path}:{...: value}} render-patch for the parent state
|
|
354
|
-
*/
|
|
355
|
-
createPatch(value: SubState | DeepPartial<SubState> | undefined | null): RenderPatch<S> {
|
|
356
|
-
const renderPatch: DeepPartial<S> = {};
|
|
357
|
-
this.putDeep(value, renderPatch);
|
|
358
|
-
return renderPatch;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private putDeep(value: SubState | DeepPartial<SubState> | undefined | null, target: S | DeepPartial<S>) {
|
|
362
|
-
const keys = this.keys;
|
|
363
|
-
if (keys.length > 1) {
|
|
364
|
-
let i = 0;
|
|
365
|
-
let raw = (<any>target)[keys[i]];
|
|
366
|
-
if (typeof raw !== "object" || raw === null) {
|
|
367
|
-
(<any>target)[keys[i]] = raw = {};
|
|
368
|
-
}
|
|
369
|
-
for (i = 1; i < keys.length - 1; i++) {
|
|
370
|
-
const p = raw;
|
|
371
|
-
raw = raw[keys[i]];
|
|
372
|
-
if (typeof raw !== "object" || raw === null) {
|
|
373
|
-
p[keys[i]] = raw = {};
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
raw[keys[i]] = value;
|
|
377
|
-
} else {
|
|
378
|
-
if (typeof (<any>target)[keys[0]] === "object" && typeof value === "object")
|
|
379
|
-
Object.assign((<any>target)[keys[0]], value);
|
|
380
|
-
else
|
|
381
|
-
(<any>target)[keys[0]] = value;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/** @deprecated Helper to unwrap undefined/null from optional properties */
|
|
387
|
-
type NonNullable<T> = T extends null | undefined ? never : T;
|
package/tsconfig.json
CHANGED
|
@@ -4,25 +4,29 @@
|
|
|
4
4
|
"module": "preserve",
|
|
5
5
|
"moduleResolution": "bundler",
|
|
6
6
|
"rootDir": ".",
|
|
7
|
-
"
|
|
8
|
-
"removeComments": true,
|
|
7
|
+
"outDir": "./dist",
|
|
9
8
|
"declaration": true,
|
|
10
9
|
"declarationDir": "./dist",
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
11
12
|
"strict": true,
|
|
12
|
-
"
|
|
13
|
-
"strictBindCallApply": true,
|
|
14
|
-
"strictFunctionTypes": true,
|
|
15
|
-
"strictPropertyInitialization": true,
|
|
16
|
-
"strictNullChecks": true,
|
|
17
|
-
"allowJs": false,
|
|
18
|
-
"skipLibCheck": true,
|
|
13
|
+
"noUncheckedSideEffectImports": true,
|
|
19
14
|
"noImplicitReturns": true,
|
|
20
15
|
"noFallthroughCasesInSwitch": true,
|
|
21
|
-
"
|
|
22
|
-
"
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"allowSyntheticDefaultImports": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"composite": true,
|
|
22
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
23
23
|
},
|
|
24
24
|
"include": [
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"index.ts",
|
|
26
|
+
"src/**/*"
|
|
27
|
+
],
|
|
28
|
+
"exclude": [
|
|
29
|
+
"node_modules",
|
|
30
|
+
"dist"
|
|
27
31
|
]
|
|
28
32
|
}
|