Skip to content

Commit dbe2181

Browse files
feat(devtools): enrich snapshots with helpers, options and batched emission
Expand the devtools snapshot payload: serialize values for structured clone (functions become name strings), collect per-field helper props and hooks/handlers, expose boolean form options, and batch emission through a per-form cache with a dirty set so connected panels receive a single snapshot per change cycle. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent dda0682 commit dbe2181

2 files changed

Lines changed: 197 additions & 21 deletions

File tree

‎src/devtools.ts‎

Lines changed: 192 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,31 @@ export type FormkitDevtoolsEvent =
88
| { type: "snapshot"; payload: FormkitDevtoolsSnapshot };
99

1010
export interface FormkitDevtoolsFieldSnapshot {
11+
key?: string;
1112
path?: string;
1213
name?: string;
14+
id?: string;
1315
type?: string;
1416
value?: any;
1517
default?: any;
1618
initial?: any;
1719
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;
1831
hasError?: boolean;
1932
isValid?: boolean;
2033
isDirty?: boolean;
2134
isPristine?: boolean;
35+
isDefault?: boolean;
2236
isEmpty?: boolean;
2337
disabled?: boolean;
2438
touched?: boolean;
@@ -27,6 +41,12 @@ export interface FormkitDevtoolsFieldSnapshot {
2741
changed?: boolean;
2842
deleted?: boolean;
2943
validating?: boolean;
44+
clearing?: boolean;
45+
resetting?: boolean;
46+
submitting?: boolean;
47+
size?: number;
48+
submitted?: boolean;
49+
validated?: boolean;
3050
fields?: FormkitDevtoolsFieldSnapshot[];
3151
}
3252

@@ -38,7 +58,23 @@ export interface FormkitDevtoolsSnapshot {
3858
validated?: boolean;
3959
submitting?: boolean;
4060
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;
4176
options?: Record<string, any>;
77+
helpers?: Record<string, any>;
4278
fields: FormkitDevtoolsFieldSnapshot[];
4379
}
4480

@@ -55,29 +91,115 @@ export interface FormkitDevtoolsHook {
5591
}
5692

5793
const HOOK_KEY = "__MOBX_FORMKIT_DEVTOOLS_HOOK__";
94+
5895
const FIELD_PROPS = [
59-
"path",
96+
"key",
97+
"id",
6098
"name",
99+
"path",
61100
"type",
62-
"value",
101+
"bindings",
102+
"options",
63103
"default",
64104
"initial",
65-
"error",
105+
"value",
106+
"label",
107+
"placeholder",
108+
"autoFocus",
109+
"related",
110+
"rules",
111+
"extra",
112+
"checked",
113+
"validators",
114+
"validatedWith",
115+
"clearing",
116+
"resetting",
66117
"hasError",
67118
"isValid",
68119
"isDirty",
69120
"isPristine",
121+
"isDefault",
70122
"isEmpty",
71123
"disabled",
124+
"deleted",
72125
"touched",
73126
"focused",
74127
"blurred",
75128
"changed",
76-
"deleted",
129+
"error",
130+
"size",
131+
"submitted",
132+
"validated",
133+
"submitting",
77134
"validating",
78135
] as const;
79136

80-
const FORM_PROPS = ["name", "size", "submitted", "validated", "submitting", "validating"] as const;
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+
};
81203

82204
const isArrayMap = (target: any) => Boolean(target && typeof target === "object" && target._isArrayMap);
83205

@@ -93,7 +215,7 @@ const pick = (source: any, props: readonly string[]) => {
93215
const out: Record<string, any> = {};
94216
for (const prop of props) {
95217
const value = source && typeof source === "object" ? source[prop] : undefined;
96-
if (value !== undefined) out[prop] = toJS(value);
218+
if (value !== undefined) out[prop] = serialize(value);
97219
}
98220
return out;
99221
};
@@ -108,39 +230,82 @@ const serializeField = (field: any, key?: string): FormkitDevtoolsFieldSnapshot
108230
return out;
109231
};
110232

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+
111268
const serializeForm = (key: string, form: any): FormkitDevtoolsSnapshot => {
112-
const out: FormkitDevtoolsSnapshot = { key, ...pick(form, FORM_PROPS) };
269+
const out = { key, ...pick(form, FORM_PROPS) } as FormkitDevtoolsSnapshot;
113270
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);
114273
return out;
115274
};
116275

117276
const createHook = (): FormkitDevtoolsHook => {
118277
const registry = new Map<string, any>();
119278
const listeners = new Set<(event: FormkitDevtoolsEvent) => void>();
120279
const disposers = new Map<string, () => void>();
280+
const caches = new Map<string, FormkitDevtoolsSnapshot>();
281+
const dirty = new Set<string>();
121282
let connected = false;
122-
let pending = false;
283+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
123284

124285
const emit: FormkitDevtoolsHook["emit"] = (event) => {
125286
for (const listener of listeners) listener(event);
126287
};
127288

128289
const flush = () => {
129-
pending = false;
130-
for (const [key, form] of registry) {
131-
emit({ type: "snapshot", payload: serializeForm(key, form) });
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 });
132296
}
133297
};
134298

135-
const requestFlush = () => {
136-
if (pending) return;
137-
pending = true;
138-
setTimeout(flush, 0);
299+
const scheduleFlush = (key: string) => {
300+
dirty.add(key);
301+
if (flushTimer == null) flushTimer = setTimeout(flush, 0);
139302
};
140303

141304
const dispose = (key: string) => {
142305
disposers.get(key)?.();
143306
disposers.delete(key);
307+
dirty.delete(key);
308+
caches.delete(key);
144309
};
145310

146311
const setup = (key: string) => {
@@ -150,12 +315,20 @@ const createHook = (): FormkitDevtoolsHook => {
150315
if (!form) return;
151316
disposers.set(
152317
key,
153-
autorun(() => requestFlush())
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+
})
154325
);
155326
};
156327

157328
return {
158-
connected,
329+
get connected() {
330+
return connected;
331+
},
159332
registry,
160333
emit,
161334
subscribe(listener) {
@@ -189,7 +362,7 @@ const createHook = (): FormkitDevtoolsHook => {
189362
},
190363
requestSnapshot() {
191364
for (const [key, form] of registry) {
192-
emit({ type: "snapshot", payload: serializeForm(key, form) });
365+
emit({ type: "snapshot", payload: caches.get(key) ?? serializeForm(key, form) });
193366
}
194367
},
195368
};
@@ -216,4 +389,4 @@ hook.subscribe((event) => {
216389
if (event.type === "form:new" && !hook.registry.has(event.key)) {
217390
hook.register(event.key || event.form?.name || `form-${hook.registry.size + 1}`, event.form);
218391
}
219-
});
392+
});

‎tests/unit/test.devtools.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,18 @@ describe("devtools hook", () => {
2929
const events: FormkitDevtoolsEvent[] = [];
3030
hook.subscribe((event) => events.push(event));
3131

32-
new Form({ email: {} }, { name: "login" });
32+
new Form({ fields: { email: {} } }, { name: "login" });
3333

3434
expect(hook.registry.has("login")).to.equal(true);
3535
expect(hook.registry.get("login")).to.be.instanceOf(Form);
3636
expect(events.some((event) => event.type === "register" && event.key === "login")).to.equal(true);
3737
});
3838

3939
it("emits snapshots on connect and when values change", (done) => {
40-
const form = new Form({ email: { value: "a@b.c" }, meta: { firstName: "" } }, { name: "contact" });
40+
const form = new Form(
41+
{ fields: { email: { value: "a@b.c" }, meta: { firstName: "" } } },
42+
{ name: "contact" }
43+
);
4144

4245
hook.register("contact", form);
4346
hook.connect();

0 commit comments

Comments
 (0)