Usage Guide
Complete guide to installing and using Formit in your projects.
💡 API Reference
This guide covers API usage and basic patterns. For complete application examples, see Examples.
Table of Contents
Basic Usage
Creating a Form
Three ways to initialize fields:
Plain Values:
typescript
const form = createForm({
fields: {
name: '',
email: '',
age: 0,
},
});Nested Objects:
typescript
const form = createForm({
fields: {
user: {
name: 'Alice',
profile: {
age: 25,
city: 'NYC',
},
},
},
});
// Access with dot notation
form.get('user.profile.age'); // '25'With Validators:
typescript
const form = createForm({
fields: {
email: {
value: '',
validators: (v) => !String(v).includes('@') && 'Invalid email',
},
},
});Reading and Writing Values
typescript
// Get value
const email = form.get('email');
// Set value
form.set('email', 'user@example.com');
// Set multiple values
form.set({
email: 'user@example.com',
name: 'Alice',
});
// Get all values
const all = form.values();
// Get FormData
const formData = form.data();Validation
Field-Level Validators
Single or multiple validators per field:
typescript
const form = createForm({
fields: {
email: {
value: '',
validators: [
(v) => !v && 'Email is required',
(v) => v && !String(v).includes('@') && 'Invalid email format',
(v) => v && String(v).length > 100 && 'Email too long',
],
},
password: {
value: '',
validators: (v) => {
if (!v) return 'Password is required';
if (String(v).length < 8) return 'Min 8 characters';
if (!/[A-Z]/.test(String(v))) return 'Must contain uppercase';
if (!/[0-9]/.test(String(v))) return 'Must contain number';
},
},
},
});Form-Level Validators
Cross-field validation:
typescript
const form = createForm({
fields: {
password: '',
confirmPassword: '',
startDate: '',
endDate: '',
},
validate: (formData) => {
const errors = new Map();
// Password matching
if (formData.get('password') !== formData.get('confirmPassword')) {
errors.set('confirmPassword', 'Passwords must match');
}
// Date range validation
const start = new Date(String(formData.get('startDate')));
const end = new Date(String(formData.get('endDate')));
if (start > end) {
errors.set('endDate', 'End date must be after start date');
}
return errors;
},
});Async Validation
typescript
const form = createForm({
fields: {
username: {
value: '',
validators: async (value) => {
if (!value) return 'Username required';
const response = await fetch(`/api/check-username?username=${value}`);
const { exists } = await response.json();
if (exists) return 'Username already taken';
},
},
},
});Manual Validation
typescript
// Validate specific field
const error = await form.validate('email');
// Validate all fields
const errors = await form.validate();
// Validate only touched fields
const errors = await form.validate({ onlyTouched: true });
// Validate specific fields
const errors = await form.validate({ fields: ['email', 'password'] });File Uploads
Single File
typescript
const form = createForm({
fields: {
avatar: null,
title: '',
},
});
// Handle file input
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (e) => {
form.set('avatar', e.target.files[0]);
});
// Submit
await form.submit(async (formData) => {
await fetch('/api/upload', {
method: 'POST',
body: formData, // FormData ready with file
});
});Multiple Files
typescript
const form = createForm({
fields: {
documents: [],
},
});
// Handle FileList
form.set('documents', fileInput.files);
// Or manually build array
form.set('documents', Array.from(fileInput.files));File Validation
typescript
const form = createForm({
fields: {
avatar: {
value: null,
validators: (value) => {
if (!value) return 'Avatar is required';
const file = value as File;
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) return 'File too large (max 5MB)';
if (!file.type.startsWith('image/')) return 'Must be an image';
},
},
},
});Arrays and Multi-Select
Basic Array Handling
typescript
const form = createForm({
fields: {
tags: ['javascript', 'typescript'],
interests: [],
},
});
// Get array
const tags = form.get('tags'); // ['javascript', 'typescript']
// Set array
form.set('tags', ['vue', 'react']);
// Empty arrays work correctly
form.set('tags', []);
console.log(form.get('tags')); // [] not undefinedMulti-Select Inputs
typescript
const form = createForm({
fields: {
skills: {
value: [],
validators: (v) => Array.isArray(v) && v.length === 0 && 'Select at least one'
}
}
});
// Bind to select element
<select multiple {...form.bind('skills')}>
<option value="js">JavaScript</option>
<option value="ts">TypeScript</option>
<option value="react">React</option>
</select>Checkboxes
typescript
const form = createForm({
fields: {
interests: []
}
});
// Handle checkbox change
function handleCheckbox(value: string, checked: boolean) {
const current = form.get('interests') || [];
const updated = checked
? [...current, value]
: current.filter(v => v !== value);
form.set('interests', updated);
}
// Usage
<input
type="checkbox"
value="coding"
checked={(form.get('interests') || []).includes('coding')}
onChange={(e) => handleCheckbox('coding', e.target.checked)}
/>Array Validation
typescript
const form = createForm({
fields: {
emails: {
value: [],
validators: (value) => {
if (!Array.isArray(value)) return 'Must be an array';
if (value.length === 0) return 'At least one email required';
if (value.length > 10) return 'Maximum 10 emails';
// Validate each item
for (let i = 0; i < value.length; i++) {
const email = String(value[i]);
if (!email.includes('@')) {
return `Email #${i + 1} is invalid`;
}
}
},
},
},
});Framework Integration
React
Basic Example:
tsx
import { createForm } from '@vielzeug/formit';
import { useEffect, useState } from 'react';
function ContactForm() {
const [form] = useState(() =>
createForm({
fields: {
name: '',
email: {
value: '',
validators: (v) => !String(v).includes('@') && 'Invalid email',
},
message: '',
},
}),
);
const [state, setState] = useState(form.snapshot());
useEffect(() => form.subscribe(setState), [form]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.submit(async (formData) => {
await fetch('/api/contact', { method: 'POST', body: formData });
});
}}>
<input {...form.bind('name')} placeholder="Name" />
<input {...form.bind('email')} placeholder="Email" />
{state.errors.get('email') && <span>{state.errors.get('email')}</span>}
<textarea {...form.bind('message')} />
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'Sending...' : 'Send'}
</button>
</form>
);
}Custom Hook:
tsx
import { useEffect, useState } from 'react';
import type { FormInit } from '@vielzeug/formit';
import { createForm } from '@vielzeug/formit';
function useForm(init: FormInit) {
const [form] = useState(() => createForm(init));
const [state, setState] = useState(form.snapshot());
useEffect(() => form.subscribe(setState), [form]);
return { form, state };
}
// Usage
function MyForm() {
const { form, state } = useForm({
fields: {
email: {
value: '',
validators: (v) => !String(v).includes('@') && 'Invalid',
},
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.submit(onSubmit);
}}>
<input {...form.bind('email')} />
{state.errors.get('email') && <span>{state.errors.get('email')}</span>}
<button disabled={state.isSubmitting}>Submit</button>
</form>
);
}Vue 3
Basic Example:
vue
<script setup>
import { createForm } from '@vielzeug/formit';
import { ref, onMounted, onUnmounted } from 'vue';
const form = createForm({
fields: {
email: {
value: '',
validators: (v) => !String(v).includes('@') && 'Invalid email',
},
password: '',
},
});
const state = ref(form.snapshot());
let unsubscribe;
onMounted(() => (unsubscribe = form.subscribe((s) => (state.value = s))));
onUnmounted(() => unsubscribe?.());
const handleSubmit = async () => {
await form.submit(async (formData) => {
await fetch('/api/login', { method: 'POST', body: formData });
});
};
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-bind="form.bind('email')" type="email" />
<span v-if="state.errors.get('email')">
{{ state.errors.get('email') }}
</span>
<input v-bind="form.bind('password')" type="password" />
<button type="submit" :disabled="state.isSubmitting">
{{ state.isSubmitting ? 'Logging in...' : 'Login' }}
</button>
</form>
</template>Composable:
typescript
import { ref, onMounted, onUnmounted } from 'vue';
import { createForm, type FormInit } from '@vielzeug/formit';
export function useForm(init: FormInit) {
const form = createForm(init);
const state = ref(form.snapshot());
let unsubscribe;
onMounted(() => (unsubscribe = form.subscribe((s) => (state.value = s))));
onUnmounted(() => unsubscribe?.());
return { form, state };
}Svelte
svelte
<script>
import { createForm } from '@vielzeug/formit';
import { writable } from 'svelte/store';
import { onMount, onDestroy } from 'svelte';
const form = createForm({
fields: {
email: {
value: '',
validators: (v) => !String(v).includes('@') && 'Invalid email'
},
password: ''
}
});
const state = writable(form.snapshot());
let unsubscribe;
onMount(() => unsubscribe = form.subscribe(s => state.set(s)));
onDestroy(() => unsubscribe?.());
async function handleSubmit() {
await form.submit(async (formData) => {
await fetch('/api/login', { method: 'POST', body: formData });
});
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<input {...form.bind('email')} type="email" />
{#if $state.errors.get('email')}
<span>{$state.errors.get('email')}</span>
{/if}
<input {...form.bind('password')} type="password" />
<button type="submit" disabled={$state.isSubmitting}>
{$state.isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>Advanced Patterns
Multi-Step Forms
typescript
const form = createForm({
fields: {
// Step 1
name: '',
email: '',
// Step 2
address: '',
city: '',
// Step 3
cardNumber: '',
},
});
let currentStep = 1;
async function validateStep(step: number) {
const stepFields = {
1: ['name', 'email'],
2: ['address', 'city'],
3: ['cardNumber'],
};
const errors = await form.validate({ fields: stepFields[step] });
return errors.size === 0;
}
async function nextStep() {
const isValid = await validateStep(currentStep);
if (isValid) {
currentStep++;
}
}
async function submitForm() {
const isValid = await validateStep(currentStep);
if (isValid) {
await form.submit(async (formData) => {
await fetch('/api/complete', { method: 'POST', body: formData });
});
}
}Dynamic Fields
typescript
const form = createForm({
fields: {
items: [] as Array<{ name: string; quantity: number }>,
},
});
function addItem() {
const items = form.get('items') || [];
form.set('items', [...items, { name: '', quantity: 0 }]);
}
function removeItem(index: number) {
const items = form.get('items') || [];
form.set(
'items',
items.filter((_, i) => i !== index),
);
}
function updateItem(index: number, field: 'name' | 'quantity', value: any) {
const items = form.get('items') || [];
const updated = [...items];
updated[index] = { ...updated[index], [field]: value };
form.set('items', updated);
}Conditional Validation
typescript
const form = createForm({
fields: {
accountType: 'personal',
companyName: '',
vatNumber: '',
},
});
form.subscribe((state) => {
const accountType = form.get('accountType');
if (accountType === 'business') {
// Add business field validation
if (!form.get('companyName')) {
form.error('companyName', 'Company name required');
}
if (!form.get('vatNumber')) {
form.error('vatNumber', 'VAT number required');
}
} else {
// Clear business field errors
form.error('companyName', '');
form.error('vatNumber', '');
}
});Dirty State Warning
typescript
const form = createForm({
fields: { name: '', email: '' },
});
window.addEventListener('beforeunload', (e) => {
const state = form.snapshot();
if (state.dirty.size > 0) {
e.preventDefault();
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
}
});Auto-Save
typescript
const form = createForm({
fields: { content: '' },
});
let saveTimeout;
form.subscribe((state) => {
if (state.dirty.size > 0) {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
await form.submit(
async (formData) => {
await fetch('/api/auto-save', { method: 'POST', body: formData });
},
{ validate: false },
);
}, 1000);
}
});Reset to Initial Values
typescript
const form = createForm({
fields: { name: '', email: '' },
});
// Reset to initial
form.reset();
// Reset to new values
form.reset({ name: 'Guest', email: '' });Form Cloning
typescript
const form1 = createForm({
fields: { name: 'Alice', email: 'alice@example.com' },
});
// Clone FormData
const formData = form1.clone();
// Create new form with cloned data
const form2 = createForm({ fields: {} });
form2.set(formData);Best Practices
1. Always Subscribe in Framework Effects
tsx
// ✅ Good
useEffect(() => form.subscribe(setState), [form]);
// ❌ Bad – creates memory leak
form.subscribe(setState);2. Use Field Binding for Inputs
tsx
// ✅ Good
<input {...form.bind('email')} />
// ❌ Verbose
<input
name="email"
value={form.get('email')}
onChange={(e) => form.set('email', e.target.value)}
onBlur={() => form.touch('email', true)}
/>3. Handle Validation Errors
typescript
// ✅ Good
try {
await form.submit(onSubmit);
} catch (error) {
if (error instanceof ValidationError) {
// Handle validation errors
} else {
// Handle other errors
}
}
// ❌ Bad – swallows validation errors
form.submit(onSubmit).catch(console.error);4. Use Nested Objects for Organization
typescript
// ✅ Good – organized
fields: {
user: {
name: '',
email: ''
},
address: {
street: '',
city: ''
}
}
// ❌ Flat – harder to manage
fields: {
userName: '',
userEmail: '',
addressStreet: '',
addressCity: ''
}5. Validate on Submit, Show Errors on Blur
typescript
const binding = form.bind('email', {
markTouchedOnBlur: true,
});
// Show error only if touched
{
state.touched.has('email') && state.errors.get('email');
}Next Steps
💡 Continue Learning
- API Reference – Complete API documentation
- Examples – Practical code examples
- Interactive REPL – Try it in your browser