mobx-formkit 1.0.0 → 1.1.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/AGENTS.md +1 -1
- package/README.md +77 -117
- package/lib/Form.d.ts.map +1 -1
- package/lib/Form.js +1 -0
- package/lib/Form.js.map +1 -1
- package/lib/devtools.d.ts +103 -0
- package/lib/devtools.d.ts.map +1 -0
- package/lib/devtools.js +299 -0
- package/lib/devtools.js.map +1 -0
- package/lib/esm/Form.js +1 -0
- package/lib/esm/Form.js.map +1 -1
- package/lib/esm/devtools.js +294 -0
- package/lib/esm/devtools.js.map +1 -0
- package/package.json +9 -1
- package/src/Form.ts +2 -0
- package/src/devtools.ts +392 -0
- package/umd/MobxFormkit.umd.js +1 -0
- package/umd/MobxFormkit.umd.js.map +1 -1
- package/umd/MobxFormkit.umd.min.js +1 -1
- package/umd/MobxFormkit.umd.min.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobx-formkit",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"author": "Claudio Savino <claudio.savino@me.com> (https://twitter.com/foxhound87)",
|
|
6
6
|
"description": "Reactive MobX Form State Management",
|
|
7
7
|
"homepage": "https://github.com/foxhound87/mobx-formkit#readme",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"lib/composer": [
|
|
18
18
|
"./lib/composer.d.ts"
|
|
19
19
|
],
|
|
20
|
+
"devtools": [
|
|
21
|
+
"./lib/devtools.d.ts"
|
|
22
|
+
],
|
|
20
23
|
"validators/*": [
|
|
21
24
|
"./lib/validators/*.d.ts"
|
|
22
25
|
],
|
|
@@ -39,6 +42,11 @@
|
|
|
39
42
|
"import": "./lib/esm/composer.js",
|
|
40
43
|
"require": "./lib/composer.js"
|
|
41
44
|
},
|
|
45
|
+
"./devtools": {
|
|
46
|
+
"types": "./lib/devtools.d.ts",
|
|
47
|
+
"import": "./lib/esm/devtools.js",
|
|
48
|
+
"require": "./lib/devtools.js"
|
|
49
|
+
},
|
|
42
50
|
"./validators/*": {
|
|
43
51
|
"types": "./lib/validators/*.d.ts",
|
|
44
52
|
"import": "./lib/esm/validators/*.js",
|
package/src/Form.ts
CHANGED
|
@@ -70,6 +70,8 @@ export default class Form<F extends Record<string, any> = Record<string, any>> e
|
|
|
70
70
|
runInAction(() => (this.$hooks = hooks));
|
|
71
71
|
runInAction(() => (this.$handlers = handlers));
|
|
72
72
|
|
|
73
|
+
(globalThis as any).__MOBX_FORMKIT_DEVTOOLS_HOOK__?.emit?.({ type: "form:new", key: this.name, form: this });
|
|
74
|
+
|
|
73
75
|
// load data from initializers methods
|
|
74
76
|
const initial = each(
|
|
75
77
|
{
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { autorun, toJS, values } from "mobx";
|
|
2
|
+
|
|
3
|
+
export type FormkitDevtoolsEvent =
|
|
4
|
+
| { type: "connected"; connected: boolean }
|
|
5
|
+
| { type: "form:new"; key: string; form: any }
|
|
6
|
+
| { type: "register"; key: string; form: any }
|
|
7
|
+
| { type: "unregister"; key: string }
|
|
8
|
+
| { type: "snapshot"; payload: FormkitDevtoolsSnapshot };
|
|
9
|
+
|
|
10
|
+
export interface FormkitDevtoolsFieldSnapshot {
|
|
11
|
+
key?: string;
|
|
12
|
+
path?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
value?: any;
|
|
17
|
+
default?: any;
|
|
18
|
+
initial?: any;
|
|
19
|
+
error?: any;
|
|
20
|
+
label?: string;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
autoFocus?: boolean;
|
|
23
|
+
related?: any;
|
|
24
|
+
rules?: any;
|
|
25
|
+
options?: any;
|
|
26
|
+
bindings?: any;
|
|
27
|
+
extra?: any;
|
|
28
|
+
checked?: any;
|
|
29
|
+
validators?: any;
|
|
30
|
+
validatedWith?: any;
|
|
31
|
+
hasError?: boolean;
|
|
32
|
+
isValid?: boolean;
|
|
33
|
+
isDirty?: boolean;
|
|
34
|
+
isPristine?: boolean;
|
|
35
|
+
isDefault?: boolean;
|
|
36
|
+
isEmpty?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
touched?: boolean;
|
|
39
|
+
focused?: boolean;
|
|
40
|
+
blurred?: boolean;
|
|
41
|
+
changed?: boolean;
|
|
42
|
+
deleted?: boolean;
|
|
43
|
+
validating?: boolean;
|
|
44
|
+
clearing?: boolean;
|
|
45
|
+
resetting?: boolean;
|
|
46
|
+
submitting?: boolean;
|
|
47
|
+
size?: number;
|
|
48
|
+
submitted?: boolean;
|
|
49
|
+
validated?: boolean;
|
|
50
|
+
fields?: FormkitDevtoolsFieldSnapshot[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface FormkitDevtoolsSnapshot {
|
|
54
|
+
key: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
size?: number;
|
|
57
|
+
submitted?: boolean;
|
|
58
|
+
validated?: boolean;
|
|
59
|
+
submitting?: boolean;
|
|
60
|
+
validating?: boolean;
|
|
61
|
+
clearing?: boolean;
|
|
62
|
+
resetting?: boolean;
|
|
63
|
+
hasError?: boolean;
|
|
64
|
+
isValid?: boolean;
|
|
65
|
+
isDirty?: boolean;
|
|
66
|
+
isPristine?: boolean;
|
|
67
|
+
isDefault?: boolean;
|
|
68
|
+
isEmpty?: boolean;
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
deleted?: boolean;
|
|
71
|
+
touched?: boolean;
|
|
72
|
+
focused?: boolean;
|
|
73
|
+
blurred?: boolean;
|
|
74
|
+
changed?: boolean;
|
|
75
|
+
error?: any;
|
|
76
|
+
options?: Record<string, any>;
|
|
77
|
+
helpers?: Record<string, any>;
|
|
78
|
+
fields: FormkitDevtoolsFieldSnapshot[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface FormkitDevtoolsHook {
|
|
82
|
+
connected: boolean;
|
|
83
|
+
registry: Map<string, any>;
|
|
84
|
+
emit(event: FormkitDevtoolsEvent): void;
|
|
85
|
+
subscribe(listener: (event: FormkitDevtoolsEvent) => void): () => void;
|
|
86
|
+
register(key: string, form: any): void;
|
|
87
|
+
unregister(key: string): void;
|
|
88
|
+
connect(): void;
|
|
89
|
+
disconnect(): void;
|
|
90
|
+
requestSnapshot(): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const HOOK_KEY = "__MOBX_FORMKIT_DEVTOOLS_HOOK__";
|
|
94
|
+
|
|
95
|
+
const FIELD_PROPS = [
|
|
96
|
+
"key",
|
|
97
|
+
"id",
|
|
98
|
+
"name",
|
|
99
|
+
"path",
|
|
100
|
+
"type",
|
|
101
|
+
"bindings",
|
|
102
|
+
"options",
|
|
103
|
+
"default",
|
|
104
|
+
"initial",
|
|
105
|
+
"value",
|
|
106
|
+
"label",
|
|
107
|
+
"placeholder",
|
|
108
|
+
"autoFocus",
|
|
109
|
+
"related",
|
|
110
|
+
"rules",
|
|
111
|
+
"extra",
|
|
112
|
+
"checked",
|
|
113
|
+
"validators",
|
|
114
|
+
"validatedWith",
|
|
115
|
+
"clearing",
|
|
116
|
+
"resetting",
|
|
117
|
+
"hasError",
|
|
118
|
+
"isValid",
|
|
119
|
+
"isDirty",
|
|
120
|
+
"isPristine",
|
|
121
|
+
"isDefault",
|
|
122
|
+
"isEmpty",
|
|
123
|
+
"disabled",
|
|
124
|
+
"deleted",
|
|
125
|
+
"touched",
|
|
126
|
+
"focused",
|
|
127
|
+
"blurred",
|
|
128
|
+
"changed",
|
|
129
|
+
"error",
|
|
130
|
+
"size",
|
|
131
|
+
"submitted",
|
|
132
|
+
"validated",
|
|
133
|
+
"submitting",
|
|
134
|
+
"validating",
|
|
135
|
+
] as const;
|
|
136
|
+
|
|
137
|
+
const FORM_PROPS = [
|
|
138
|
+
"name",
|
|
139
|
+
"size",
|
|
140
|
+
"submitted",
|
|
141
|
+
"validated",
|
|
142
|
+
"submitting",
|
|
143
|
+
"validating",
|
|
144
|
+
"clearing",
|
|
145
|
+
"resetting",
|
|
146
|
+
"hasError",
|
|
147
|
+
"isValid",
|
|
148
|
+
"isDirty",
|
|
149
|
+
"isPristine",
|
|
150
|
+
"isDefault",
|
|
151
|
+
"isEmpty",
|
|
152
|
+
"disabled",
|
|
153
|
+
"deleted",
|
|
154
|
+
"touched",
|
|
155
|
+
"focused",
|
|
156
|
+
"blurred",
|
|
157
|
+
"changed",
|
|
158
|
+
"error",
|
|
159
|
+
] as const;
|
|
160
|
+
|
|
161
|
+
const FIELD_HELPER_PROPS = [
|
|
162
|
+
"error",
|
|
163
|
+
"label",
|
|
164
|
+
"placeholder",
|
|
165
|
+
"default",
|
|
166
|
+
"initial",
|
|
167
|
+
"type",
|
|
168
|
+
"disabled",
|
|
169
|
+
"checked",
|
|
170
|
+
"related",
|
|
171
|
+
"rules",
|
|
172
|
+
"options",
|
|
173
|
+
"extra",
|
|
174
|
+
"bindings",
|
|
175
|
+
"validators",
|
|
176
|
+
"validatedWith",
|
|
177
|
+
] as const;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Recursively convert a value into a plain, structured-clone-safe shape:
|
|
181
|
+
* observable structures become plain objects/arrays and functions become
|
|
182
|
+
* name strings (functions cannot cross the postMessage boundary).
|
|
183
|
+
*/
|
|
184
|
+
const serialize = (value: any, depth: number = 0): any => {
|
|
185
|
+
if (depth > 8) return undefined;
|
|
186
|
+
if (typeof value === "function") {
|
|
187
|
+
return `[Function${value.name ? `: ${value.name}` : ""}]`;
|
|
188
|
+
}
|
|
189
|
+
if (value == null) return value;
|
|
190
|
+
const plain = toJS(value);
|
|
191
|
+
if (Array.isArray(plain)) {
|
|
192
|
+
return plain.map((item: any) => serialize(item, depth + 1));
|
|
193
|
+
}
|
|
194
|
+
if (typeof plain === "object") {
|
|
195
|
+
const out: Record<string, any> = {};
|
|
196
|
+
for (const key of Object.keys(plain)) {
|
|
197
|
+
out[key] = serialize(plain[key], depth + 1);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
return plain;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const isArrayMap = (target: any) => Boolean(target && typeof target === "object" && target._isArrayMap);
|
|
205
|
+
|
|
206
|
+
const pairsOf = (target: any): [string, any][] => {
|
|
207
|
+
if (isArrayMap(target)) return Array.from((target as any).entries());
|
|
208
|
+
if (Array.isArray(target)) return target.map((value: any, index: number) => [String(index), value]);
|
|
209
|
+
const entries = values(target);
|
|
210
|
+
if (Array.isArray(entries)) return entries.map((value: any, index: number) => [String(index), value]);
|
|
211
|
+
return Object.entries(target);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const pick = (source: any, props: readonly string[]) => {
|
|
215
|
+
const out: Record<string, any> = {};
|
|
216
|
+
for (const prop of props) {
|
|
217
|
+
const value = source && typeof source === "object" ? source[prop] : undefined;
|
|
218
|
+
if (value !== undefined) out[prop] = serialize(value);
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const serializeField = (field: any, key?: string): FormkitDevtoolsFieldSnapshot => {
|
|
224
|
+
const out: FormkitDevtoolsFieldSnapshot = pick(field, FIELD_PROPS);
|
|
225
|
+
if (key !== undefined && out.name === undefined) out.name = key;
|
|
226
|
+
if (key !== undefined && out.path === undefined) out.path = key;
|
|
227
|
+
if (isArrayMap(field)) {
|
|
228
|
+
out.fields = pairsOf(field).map(([childKey, entry]: [string, any]) => serializeField(entry, childKey));
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const fieldKey = (field: FormkitDevtoolsFieldSnapshot, index: number): string =>
|
|
234
|
+
(field as any).key ?? field.path ?? field.name ?? `field-${index}`;
|
|
235
|
+
|
|
236
|
+
const collectProp = (fields: FormkitDevtoolsFieldSnapshot[], prop: string): Record<string, any> => {
|
|
237
|
+
const out: Record<string, any> = {};
|
|
238
|
+
fields.forEach((field, index) => {
|
|
239
|
+
const key = fieldKey(field, index);
|
|
240
|
+
out[key] = field.fields && field.fields.length
|
|
241
|
+
? collectProp(field.fields, prop)
|
|
242
|
+
: (field as any)[prop];
|
|
243
|
+
});
|
|
244
|
+
return out;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const collectHelpers = (form: any, fields: FormkitDevtoolsFieldSnapshot[]): Record<string, any> => {
|
|
248
|
+
const out: Record<string, any> = {};
|
|
249
|
+
for (const prop of FIELD_HELPER_PROPS) {
|
|
250
|
+
out[prop] = collectProp(fields, prop);
|
|
251
|
+
}
|
|
252
|
+
out.hooks = serialize(form.$hooks);
|
|
253
|
+
out.handlers = serialize(form.$handlers);
|
|
254
|
+
return out;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const collectBooleanOptions = (form: any): Record<string, boolean> => {
|
|
258
|
+
const out: Record<string, boolean> = {};
|
|
259
|
+
const options = form?.state?.options?.options;
|
|
260
|
+
if (!options || typeof options !== "object") return out;
|
|
261
|
+
for (const key of Object.keys(options)) {
|
|
262
|
+
const value = options[key];
|
|
263
|
+
if (typeof value === "boolean") out[key] = value;
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const serializeForm = (key: string, form: any): FormkitDevtoolsSnapshot => {
|
|
269
|
+
const out = { key, ...pick(form, FORM_PROPS) } as FormkitDevtoolsSnapshot;
|
|
270
|
+
out.fields = pairsOf(form.fields).map(([fieldKey, field]: [string, any]) => serializeField(field, fieldKey));
|
|
271
|
+
out.helpers = collectHelpers(form, out.fields);
|
|
272
|
+
out.options = collectBooleanOptions(form);
|
|
273
|
+
return out;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const createHook = (): FormkitDevtoolsHook => {
|
|
277
|
+
const registry = new Map<string, any>();
|
|
278
|
+
const listeners = new Set<(event: FormkitDevtoolsEvent) => void>();
|
|
279
|
+
const disposers = new Map<string, () => void>();
|
|
280
|
+
const caches = new Map<string, FormkitDevtoolsSnapshot>();
|
|
281
|
+
const dirty = new Set<string>();
|
|
282
|
+
let connected = false;
|
|
283
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
284
|
+
|
|
285
|
+
const emit: FormkitDevtoolsHook["emit"] = (event) => {
|
|
286
|
+
for (const listener of listeners) listener(event);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const flush = () => {
|
|
290
|
+
flushTimer = null;
|
|
291
|
+
const keys = [...dirty];
|
|
292
|
+
dirty.clear();
|
|
293
|
+
for (const key of keys) {
|
|
294
|
+
const snapshot = caches.get(key);
|
|
295
|
+
if (snapshot) emit({ type: "snapshot", payload: snapshot });
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const scheduleFlush = (key: string) => {
|
|
300
|
+
dirty.add(key);
|
|
301
|
+
if (flushTimer == null) flushTimer = setTimeout(flush, 0);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const dispose = (key: string) => {
|
|
305
|
+
disposers.get(key)?.();
|
|
306
|
+
disposers.delete(key);
|
|
307
|
+
dirty.delete(key);
|
|
308
|
+
caches.delete(key);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const setup = (key: string) => {
|
|
312
|
+
dispose(key);
|
|
313
|
+
if (!connected) return;
|
|
314
|
+
const form = registry.get(key);
|
|
315
|
+
if (!form) return;
|
|
316
|
+
disposers.set(
|
|
317
|
+
key,
|
|
318
|
+
autorun(() => {
|
|
319
|
+
// Reading observables inside serializeForm makes the autorun re-run
|
|
320
|
+
// on every field/form change; the result is cached and emitted once
|
|
321
|
+
// (batched) by flush.
|
|
322
|
+
caches.set(key, serializeForm(key, form));
|
|
323
|
+
scheduleFlush(key);
|
|
324
|
+
})
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
get connected() {
|
|
330
|
+
return connected;
|
|
331
|
+
},
|
|
332
|
+
registry,
|
|
333
|
+
emit,
|
|
334
|
+
subscribe(listener) {
|
|
335
|
+
listeners.add(listener);
|
|
336
|
+
return () => {
|
|
337
|
+
listeners.delete(listener);
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
register(key, form) {
|
|
341
|
+
registry.set(key, form);
|
|
342
|
+
emit({ type: "register", key, form });
|
|
343
|
+
setup(key);
|
|
344
|
+
},
|
|
345
|
+
unregister(key) {
|
|
346
|
+
if (!registry.has(key)) return;
|
|
347
|
+
dispose(key);
|
|
348
|
+
registry.delete(key);
|
|
349
|
+
emit({ type: "unregister", key });
|
|
350
|
+
},
|
|
351
|
+
connect() {
|
|
352
|
+
if (connected) return;
|
|
353
|
+
connected = true;
|
|
354
|
+
for (const key of registry.keys()) setup(key);
|
|
355
|
+
emit({ type: "connected", connected: true });
|
|
356
|
+
},
|
|
357
|
+
disconnect() {
|
|
358
|
+
if (!connected) return;
|
|
359
|
+
connected = false;
|
|
360
|
+
for (const key of [...disposers.keys()]) dispose(key);
|
|
361
|
+
emit({ type: "connected", connected: false });
|
|
362
|
+
},
|
|
363
|
+
requestSnapshot() {
|
|
364
|
+
for (const [key, form] of registry) {
|
|
365
|
+
emit({ type: "snapshot", payload: caches.get(key) ?? serializeForm(key, form) });
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const globalRef = globalThis as any;
|
|
372
|
+
|
|
373
|
+
export const getHook = (): FormkitDevtoolsHook | undefined => globalRef[HOOK_KEY];
|
|
374
|
+
|
|
375
|
+
export const installHook = (): FormkitDevtoolsHook => {
|
|
376
|
+
if (!globalRef[HOOK_KEY]) {
|
|
377
|
+
globalRef[HOOK_KEY] = createHook();
|
|
378
|
+
}
|
|
379
|
+
return globalRef[HOOK_KEY];
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export const registerForm = (key: string, form: any) => installHook().register(key, form);
|
|
383
|
+
|
|
384
|
+
export const unregisterForm = (key: string) => installHook().unregister(key);
|
|
385
|
+
|
|
386
|
+
const hook = installHook();
|
|
387
|
+
|
|
388
|
+
hook.subscribe((event) => {
|
|
389
|
+
if (event.type === "form:new" && !hook.registry.has(event.key)) {
|
|
390
|
+
hook.register(event.key || event.form?.name || `form-${hook.registry.size + 1}`, event.form);
|
|
391
|
+
}
|
|
392
|
+
});
|
package/umd/MobxFormkit.umd.js
CHANGED
|
@@ -2931,6 +2931,7 @@
|
|
|
2931
2931
|
this.extra = extra;
|
|
2932
2932
|
mobx.runInAction(() => (this.$hooks = hooks));
|
|
2933
2933
|
mobx.runInAction(() => (this.$handlers = handlers));
|
|
2934
|
+
globalThis.__MOBX_FORMKIT_DEVTOOLS_HOOK__?.emit?.({ type: "form:new", key: this.name, form: this });
|
|
2934
2935
|
// load data from initializers methods
|
|
2935
2936
|
const initial = lodash.each({
|
|
2936
2937
|
setup,
|