Thin ergonomic layer over form — RHF-style API with JSON-Schema resolver.
Module formKit | Source packages/front/fw/src/dom/utils/formKit.js | Deps form, valid, reactiveBind, signal | Worker-safe no
formKit is a thin wrapper around the existing form module.
It does not reimplement form state, dirty/touched tracking, array handling,
async validation, or submit — those live entirely in form. What it adds is:
- A JSON-Schema resolver compiled once via
valid.compile(schema).
- A declarative
register(name) helper returning input props (value + event handlers)
without calling form.attach manually.
- A
watch(name?) subscription backed by signal that fires
only for the requested field, or for any change when no name is given.
- A
dispose() that tears down all subscriptions and DOM bindings in one call.
Resolve
const formKit = runtime.resolve('formKit');
// Returns: { create }
API
| Parameter |
Type |
Description |
spec.schema |
object |
Optional JSON Schema; compiled via valid.compile(schema). |
spec.defaultValues |
object |
Initial field values keyed by field name. |
Returns a FormKitInstance.
| Method |
Signature |
Returns |
register |
(name: string) => FieldProps |
Props to spread on an <input> (value, onInput, onChange, name). |
watch |
(name?: string) => WatchHandle |
Subscribe to one field or the whole form. |
reset |
(values?: object) => void |
Reset to defaults or given values; clears manual errors. |
setValue |
(name: string, value: *) => void |
Set one field value. |
getValues |
(name?: string) => * | object |
Read one field or all values. |
setError |
(name: string, message: string) => void |
Set a manual error on a field. |
array |
(name: string) => FormKitArrayAccessor |
RHF-style array-field accessor. |
submit |
(onValid, onInvalid?) => Promise<void> |
Submit; calls onValid(values) or onInvalid(errors). |
form |
FormController |
Escape hatch: the underlying form.create() controller. |
dispose |
() => void |
Detach all subscriptions and DOM bindings (idempotent). |
FieldProps (returned by register)
| Property |
Type |
Description |
name |
string |
The field name. |
value |
* |
Live getter — current field value. |
onInput |
Function |
Handler for the input event (text inputs). |
onChange |
Function |
Handler for the change event (checkboxes). |
WatchHandle (returned by watch)
| Property |
Type |
Description |
value |
* |
Current value (field snapshot or full values object). |
subscribe |
Function |
Register an additional subscriber; returns unsubscribe. |
unsubscribe |
Function |
Detach this watch subscription. |
| Property/Method |
Description |
fields |
Defensive copy of the array (alias for values). |
values |
Defensive copy of the current array. |
length |
Current array length. |
push(item) |
Append an item; returns its index. |
remove(idx) |
Remove at index; false when out of range. |
move(from, to) |
Move item; false when out of range. |
set(idx, item) |
Replace at index; false when out of range. |
clear() |
Empty the array. |
get(idx) |
Read a single item by index. |
Examples
const formKit = runtime.resolve('formKit');
const fk = formKit.create({
schema: {
type: 'object',
required: ['email'],
properties: {
email: { type: 'string', minLength: 1 },
username: { type: 'string', minLength: 3 },
},
},
defaultValues: { email: '', username: '' },
});
// --- In a component render callback ---
// Spread onto your input element.
const emailProps = fk.register('email');
// emailProps = { name: 'email', value: '', onInput: fn, onChange: fn }
// Wire the real DOM element (optional — for full two-way DOM sync):
fk.form.attach('email', document.querySelector('#email'));
// Watch a single field for live validation feedback.
const emailWatch = fk.watch('email');
emailWatch.subscribe((v) => console.log('email changed to', v));
// Submit
await fk.submit(
(values) => { /* POST values to the server */ },
(errors) => { /* display errors[fieldName] */ },
);
// Cleanup on unmount.
fk.dispose();
Array fields
const fk = formKit.create({
schema: {
type: 'object',
properties: { tags: { type: 'array', items: { type: 'string' } } },
},
defaultValues: { tags: [] },
});
const arr = fk.array('tags');
arr.push('alpha');
arr.push('beta');
arr.move(0, 1); // ['beta', 'alpha']
arr.remove(1); // ['beta']
console.log(arr.length); // 1
console.log(arr.fields); // ['beta']
Manual errors (e.g. from server responses)
await fk.submit(async (values) => {
const resp = await fetch('/api/save', { method: 'POST', body: JSON.stringify(values) });
if (!resp.ok) {
const { field, message } = await resp.json();
fk.setError(field, message); // surfaces in fk.form.errors[field]
}
});
Notes
- The resolver is
valid.compile(schema) — no new validation engine. The same
JSON Schema dialect supported by valid applies
(type, required, properties, format, minLength, etc.).
watch is subscription-based (explicit), not transparent — it does not auto-track
reads. Use subscribe on the returned handle to react to changes.
setError persists until the field is changed (triggering re-validation) or
reset() is called.
dispose() is idempotent; calling it multiple times is safe.
See also
form — underlying form state machine (values, dirty, touched, errors, array, submit).
valid — JSON-Schema validator; valid.compile(schema) is the resolver.
reactiveBind — signal → DOM binding controller used internally.
signal — reactive signals powering watch.