@ryupold/vode 1.2.0 → 1.3.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 +30 -0
- package/dist/vode.js +220 -92
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +219 -96
- package/index.ts +5 -1
- package/package.json +3 -3
- package/src/merge-class.ts +62 -0
- package/src/state-context.ts +228 -0
- package/src/vode.ts +140 -140
- package/tsconfig.json +1 -2
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { AnimatedPatch, DeepPartial, PatchableState, RenderPatch } from "./vode.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates dot-notation path strings for all nested properties in an object type.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* type User = { profile: { settings: { theme: string } } };
|
|
8
|
+
* type Paths = KeyPath<User>; // "profile" | "profile.settings" | "profile.settings.theme"
|
|
9
|
+
*/
|
|
10
|
+
export type KeyPath<ObjectType extends object> =
|
|
11
|
+
{ [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
|
12
|
+
? `${Key}` | `${Key}.${KeyPath<ObjectType[Key]>}`
|
|
13
|
+
: `${Key}`
|
|
14
|
+
}[keyof ObjectType & (string | number)];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts the value type at a given dot-notation path in an object type.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* type User = { profile: { settings: { theme: string } } };
|
|
21
|
+
* type Theme = PathValue<User, "profile.settings.theme">; // string
|
|
22
|
+
*/
|
|
23
|
+
export type PathValue<T, P extends string> =
|
|
24
|
+
P extends `${infer Key}.${infer Rest}`
|
|
25
|
+
? Key extends keyof T ? PathValue<T[Key], Rest> : never
|
|
26
|
+
: P extends keyof T ? T[P] : never;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps valid paths in an object type to paths that resolve to a specific substate type.
|
|
30
|
+
* Used for type-safe path constraints in StateContext.
|
|
31
|
+
* Ensures exact type matching (not just compatibility).
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* type User = { profile: { name: string, settings: { theme: string } } };
|
|
35
|
+
* type SettingsPaths = KeyToSubState<User, { theme: string }>; // "profile.settings"
|
|
36
|
+
* type InvalidPath = KeyToSubState<User, { theme: string }, "profile">; // never (type mismatch)
|
|
37
|
+
*/
|
|
38
|
+
export type KeyToSubState<S extends object, Sub, K = KeyPath<S>> =
|
|
39
|
+
K extends KeyPath<S> ? [PathValue<S, K>] extends [Sub] ? [Sub] extends [PathValue<S, K>] ? K
|
|
40
|
+
: never : never : never;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* State context for type-safe access and manipulation of nested state paths
|
|
44
|
+
* while still be able to access the parent state.
|
|
45
|
+
*/
|
|
46
|
+
export interface StateContext<S extends PatchableState, SubState> extends SubStateContext<SubState> {
|
|
47
|
+
/**
|
|
48
|
+
* parent state
|
|
49
|
+
* @see PatchableState<S>
|
|
50
|
+
*/
|
|
51
|
+
get state(): S;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* State context for type-safe access and manipulation of nested sub-state values without knowledge of the parent state.
|
|
56
|
+
*/
|
|
57
|
+
export interface SubStateContext<SubState> {
|
|
58
|
+
/**
|
|
59
|
+
* Reads the current value of the substate if it exists.
|
|
60
|
+
*
|
|
61
|
+
* @returns The current value, or undefined if the path doesn't exist
|
|
62
|
+
*/
|
|
63
|
+
get(): SubState | undefined;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Updates the nested sub-state value WITHOUT triggering a render.
|
|
67
|
+
* This performs a silent mutation of the parent state object.
|
|
68
|
+
*
|
|
69
|
+
* @param {DeepPartial<SubState>} value - The new value or partial update to apply
|
|
70
|
+
*/
|
|
71
|
+
put(value: SubState | DeepPartial<SubState> | undefined | null): void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Updates the nested sub-state value AND triggers a render.
|
|
75
|
+
* This is the recommended way to update nested state in most cases.
|
|
76
|
+
*
|
|
77
|
+
* @param value - The new value or partial update to apply
|
|
78
|
+
*/
|
|
79
|
+
patch(value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Provides type-safe access to deeply nested state with path-based operations.
|
|
84
|
+
*
|
|
85
|
+
* **When to use:**
|
|
86
|
+
* - State is deeply nested
|
|
87
|
+
* - Multiple components access the same nested path
|
|
88
|
+
* - You need type safety for nested updates
|
|
89
|
+
*
|
|
90
|
+
* **When to avoid:**
|
|
91
|
+
* - Shallow state structures for main state
|
|
92
|
+
* - State structure changes frequently
|
|
93
|
+
* - Learning vode for the first time (start simpler)
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const state = createState({
|
|
98
|
+
* user: {
|
|
99
|
+
* profile: {
|
|
100
|
+
* settings: { theme: 'dark', lang: 'en' }
|
|
101
|
+
* }
|
|
102
|
+
* }
|
|
103
|
+
* });
|
|
104
|
+
* app(element, state, (s) => [DIV]);
|
|
105
|
+
*
|
|
106
|
+
* // Create a context for the nested settings
|
|
107
|
+
* const settingsCtx = new StateContext(state, 'user.profile.settings');
|
|
108
|
+
*
|
|
109
|
+
* // Read current value
|
|
110
|
+
* const settings = settingsCtx.get(); // { theme: 'dark', lang: 'en' }
|
|
111
|
+
*
|
|
112
|
+
* // Update and trigger render
|
|
113
|
+
* settingsCtx.patch({ theme: 'light' });
|
|
114
|
+
*
|
|
115
|
+
* // Update without render (silent mutation)
|
|
116
|
+
* settingsCtx.put({ lang: 'de' });
|
|
117
|
+
* state.patch({}); // trigger render manually later
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @template S - The root state type (must extend PatchableState)
|
|
121
|
+
* @template SubState - The type of the nested state being accessed
|
|
122
|
+
*/
|
|
123
|
+
export class KeyStateContext<S extends PatchableState, SubState>
|
|
124
|
+
implements StateContext<S, SubState> {
|
|
125
|
+
private readonly keys: string[];
|
|
126
|
+
|
|
127
|
+
constructor(
|
|
128
|
+
public readonly state: S,
|
|
129
|
+
public readonly path: KeyToSubState<S, SubState>
|
|
130
|
+
) {
|
|
131
|
+
this.keys = path.split('.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get(): SubState | undefined {
|
|
135
|
+
const keys = this.keys;
|
|
136
|
+
let raw = this.state ? (<any>this.state)[keys[0]] : undefined;
|
|
137
|
+
for (let i = 1; i < keys.length && !!raw; i++) {
|
|
138
|
+
raw = raw[keys[i]];
|
|
139
|
+
}
|
|
140
|
+
return raw;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
put(value: SubState | DeepPartial<SubState> | undefined | null) {
|
|
144
|
+
this.putDeep(value, this.state);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
patch(value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null) {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const animation: AnimatedPatch<S> = [];
|
|
150
|
+
for(const v of value){
|
|
151
|
+
animation.push(this.createPatch(v));
|
|
152
|
+
}
|
|
153
|
+
this.state.patch(animation);
|
|
154
|
+
}
|
|
155
|
+
this.state.patch(this.createPatch(value as DeepPartial<SubState>));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates a render-patch for the parent state by setting a nested sub-state value while creating necessary structure.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const ctx = new StateContext(state, 'user.profile.settings');
|
|
164
|
+
* const patch = ctx.createPatch({ theme: 'light' });
|
|
165
|
+
* // patch is { user: { profile: { settings: { theme: 'light' } } } }
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* @param value
|
|
169
|
+
* @returns {{key-path}:{...: value}} render-patch for the parent state
|
|
170
|
+
*/
|
|
171
|
+
createPatch(value: SubState | DeepPartial<SubState> | undefined | null): RenderPatch<S> {
|
|
172
|
+
const renderPatch: DeepPartial<S> = {};
|
|
173
|
+
this.putDeep(value, renderPatch);
|
|
174
|
+
return renderPatch;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private putDeep(value: SubState | DeepPartial<SubState> | undefined | null, target: S | DeepPartial<S>) {
|
|
178
|
+
const keys = this.keys;
|
|
179
|
+
if (keys.length > 1) {
|
|
180
|
+
let i = 0;
|
|
181
|
+
let raw = (<any>target)[keys[i]];
|
|
182
|
+
if (typeof raw !== "object" || raw === null) {
|
|
183
|
+
(<any>target)[keys[i]] = raw = {};
|
|
184
|
+
}
|
|
185
|
+
for (i = 1; i < keys.length - 1; i++) {
|
|
186
|
+
const p = raw;
|
|
187
|
+
raw = raw[keys[i]];
|
|
188
|
+
if (typeof raw !== "object" || raw === null) {
|
|
189
|
+
p[keys[i]] = raw = {};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
raw[keys[i]] = value;
|
|
193
|
+
} else {
|
|
194
|
+
if (typeof (<any>target)[keys[0]] === "object" && typeof value === "object")
|
|
195
|
+
Object.assign((<any>target)[keys[0]], value);
|
|
196
|
+
else
|
|
197
|
+
(<any>target)[keys[0]] = value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Provides type-safe access to sub-state with fetch & store delegate functions.
|
|
204
|
+
* Implementer is responsible for reading/writing the sub-state correctly.
|
|
205
|
+
*
|
|
206
|
+
* **When to use:**
|
|
207
|
+
* - State structure is dynamic or complex
|
|
208
|
+
* - You need custom logic for accessing nested state
|
|
209
|
+
* - You want to encapsulate access logic outside of parent state
|
|
210
|
+
*
|
|
211
|
+
* **When to avoid:**
|
|
212
|
+
* - Simple, static state structures
|
|
213
|
+
* - You want automatic path-based access (use KeyStateContext instead)
|
|
214
|
+
* - Learning vode for the first time (start simpler)
|
|
215
|
+
*/
|
|
216
|
+
export class DelegateStateContext<S extends PatchableState, SubState>
|
|
217
|
+
implements StateContext<S, SubState> {
|
|
218
|
+
constructor(
|
|
219
|
+
public readonly state: S,
|
|
220
|
+
|
|
221
|
+
public readonly get: () => SubState | undefined,
|
|
222
|
+
|
|
223
|
+
public readonly put: (value: SubState | DeepPartial<SubState> | undefined | null) => void,
|
|
224
|
+
|
|
225
|
+
public readonly patch: (value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null) => void,
|
|
226
|
+
) {
|
|
227
|
+
}
|
|
228
|
+
}
|