Formit Usage Guide
New to Formit?
Start with the Overview for a quick introduction, then use this page for implementation details.
Basic Usage
ts
const form = createForm({
defaultValues: {
email: '',
password: '',
profile: { age: 0, name: '' },
},
validators: {
email: (v) => (!String(v).includes('@') ? 'Invalid email' : undefined),
password: (v) => (String(v).length < 8 ? 'Min 8 chars' : undefined),
},
});
form.set('profile.age', 30);
form.patch({ profile: { name: 'Alice' } });
const email = form.get('email');
const values = form.values();Validation
Field validators run per field. Form validator runs on full validation only.
ts
const form = createForm({
defaultValues: { confirmPassword: '', password: '' },
validators: {
password: (v) => (String(v).length < 8 ? 'Min 8 chars' : undefined),
},
validator: (values) => {
if (values['password'] !== values['confirmPassword']) {
return { confirmPassword: 'Passwords must match' };
}
return undefined;
},
});
await form.validateField('password');
await form.validate();
await form.validate({ fields: ['password'] });
await form.validate({ onlyTouched: true });
const controller = new AbortController();
await form.validate({ signal: controller.signal });
controller.abort();Schema adapter:
ts
import { z } from 'zod';
const schema = z.object({
age: z.number().min(18, 'Must be 18+'),
email: z.string().email('Invalid email'),
});
const form = createForm({
defaultValues: { age: 0, email: '' },
...fromSchema(schema),
});Submission
ts
try {
await form.submit(async (values) => {
await fetch('/api/submit', {
body: JSON.stringify(values),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
});
});
} catch (error) {
if (error instanceof FormValidationError) {
console.log(error.errors);
} else if (error instanceof SubmitError) {
console.log('Submission already in progress');
} else {
throw error;
}
}
await form.submit(saveDraft, { skipValidation: true });
await form.submit(onSubmit, { fields: ['email', 'password'] });Subscriptions
ts
const stopForm = form.subscribe((state) => {
console.log(state.isValid, state.isDirty, state.errors);
});
const stopEmail = form.watch('email', (field) => {
console.log(field.value, field.error, field.touched, field.dirty);
});
form.watch('email', () => {}, { immediate: false });
stopEmail();
stopForm();Bind
ts
const binding = form.bind('email');
input.name = binding.name;
input.value = String(binding.value ?? '');
input.onblur = binding.onBlur;
input.oninput = binding.onChange;
const fileBinding = form.bind('attachment', {
touchOnBlur: true,
validateOnBlur: true,
valueExtractor: (e) => (e as { target: { files?: FileList } }).target.files?.[0] ?? null,
});Reset
ts
const form = createForm({ defaultValues: { email: '', name: '' } });
form.reset();
form.reset({ email: 'guest@example.com', name: 'Guest' });
form.resetField('name');reset(newValues) updates both current values and the baseline used for dirty tracking.
Dirty and Touched State
ts
form.touch('email');
form.touch('email', 'password');
form.touchAll();
form.untouch('email');
form.untouchAll();
console.log(form.isDirty, form.isTouched);
console.log(form.isFieldDirty('email'));
console.log(form.isFieldTouched('email'));
console.log(form.getError('email'));Arrays and Multi-select
ts
const form = createForm({ defaultValues: { tags: ['a'] } });
form.appendField('tags', 'b');
form.removeField('tags', 0);
form.moveField('tags', 0, 1);File Uploads
ts
const form = createForm({
defaultValues: { attachment: null as File | null, name: '' },
});
form.set('attachment', fileInput.files?.[0] ?? null);
await form.submit(async () => {
await fetch('/upload', {
body: form.toFormData(),
method: 'POST',
});
});
const fd = toFormData(form.values());Advanced Patterns
ts
// Wizard step validation
await form.validate({ fields: ['email'] });
// Keep untouched-field errors while validating only touched fields
await form.validate({ onlyTouched: true });
// Autosave without validation
await form.submit(saveDraft, { skipValidation: true });Best Practices
- Keep validators pure and deterministic.
- Prefer
watch(name, ...)over fullsubscribe(...)when rendering one field. - Use
reset(newValues)after loading server data to keep dirty baseline accurate. - Use
skipValidationonly for intentional flows like autosave. - Call
dispose()when form lifecycle ends.