rune-form
v0.1.5
Published
Type-safe reactive form builder for Svelte 5
Downloads
216
Maintainers
Readme
RuneForm
The most powerful reactive form library for Svelte 5
Documentation • Quick Start • Examples • API Reference
✨ Why RuneForm?
RuneForm is a next-generation form library designed specifically for Svelte 5's rune system. It provides automatic reactivity, type safety, and powerful features with minimal boilerplate.
🎯 Key Benefits
- Zero Configuration: Works out of the box with sensible defaults
- Automatic Everything: Touched tracking, validation, error handling - all automatic
- Type-Safe: Full TypeScript support with perfect type inference
- Memory Efficient: Built-in memory management and resource disposal
- Performance Optimized: Intelligent caching, debounced validation, minimal re-renders
- Developer Friendly: Intuitive API that feels natural to use
🚀 Features
Core Features
- 🎯 Svelte 5 Runes - Built with latest runes for optimal reactivity
- 🔒 Type-Safe Validation - Zod schemas with full TypeScript support
- ⚡ Auto Touched Tracking - Automatic field modification detection
- 🔄 Real-time Validation - Debounced validation with error handling
- 🌳 Deep Nesting - Full support for complex nested structures
- 📋 Array Operations - Rich array manipulation with state sync
Advanced Features
- 🧠 Smart Memory Management - Automatic disposal with
Symbol.dispose - 💾 Intelligent Caching - Path compilation and field caching
- 🔧 Custom Validators - Support for custom validation functions
- 🎨 Flexible API - Multiple ways to interact with form data
- ⚡ Performance Optimized - Minimal re-renders and efficient updates
- 🔍 Developer Experience - Excellent debugging and error messages
📦 Installation
npm install rune-formRequirements
- Svelte: ^5.0.0
- Zod: ^3.0.0 or ^4.0.0 (optional, for schema validation)
- TypeScript: Recommended for best experience
🎓 Quick Start
Basic Form with Zod Validation
<script lang="ts">
import { RuneForm } from 'rune-form';
import { z } from 'zod';
// Define your schema
const schema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters'),
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'Must be at least 18 years old')
});
// Create form instance
const form = RuneForm.fromSchema(schema);
// Handle form submission
async function handleSubmit() {
if (!form.isValid) return;
console.log('Submitting:', form.data);
// Your submission logic here
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<label>
Username
<input bind:value={form.data.username} />
{#if form.touched.username && form.errors.username}
<span class="error">{form.errors.username[0]}</span>
{/if}
</label>
<label>
Email
<input type="email" bind:value={form.data.email} />
{#if form.touched.email && form.errors.email}
<span class="error">{form.errors.email[0]}</span>
{/if}
</label>
<label>
Age
<input type="number" bind:value={form.data.age} />
{#if form.touched.age && form.errors.age}
<span class="error">{form.errors.age[0]}</span>
{/if}
</label>
<button type="submit" disabled={!form.isValid || form.isValidating}>
{form.isValidating ? 'Validating...' : 'Submit'}
</button>
</form>📖 Documentation
Form Creation
With Zod Schema (Recommended)
import { RuneForm } from 'rune-form';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
profile: z.object({
bio: z.string(),
avatar: z.string().url()
})
});
// Create with schema
const form = RuneForm.fromSchema(schema);
// With initial data
const form = RuneForm.fromSchema(schema, {
name: 'John Doe',
email: '[email protected]'
});With Custom Validators
import { RuneForm, createCustomValidator } from 'rune-form';
const customValidators = {
username: (value) => {
if (!value || value.length < 3) {
return ['Username must be at least 3 characters'];
}
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
return ['Username can only contain letters, numbers, and underscores'];
}
return [];
},
email: async (value) => {
// Async validation example
const exists = await checkEmailExists(value);
return exists ? ['Email already taken'] : [];
}
};
const form = new RuneForm(createCustomValidator(customValidators), { username: '', email: '' });Automatic Touched State Tracking
RuneForm automatically tracks which fields have been modified. No manual markTouched calls needed!
<script>
const form = RuneForm.fromSchema(schema);
</script>
<!-- Touched state is automatically set when user modifies the field -->
<input bind:value={form.data.name} />
{#if form.touched.name && form.errors.name}
<span class="error">{form.errors.name[0]}</span>
{/if}
<!-- Works with nested objects -->
<input bind:value={form.data.address.street} />
{#if form.touched['address.street'] && form.errors['address.street']}
<span class="error">{form.errors['address.street'][0]}</span>
{/if}
<!-- And arrays too -->
<input bind:value={form.data.items[0].name} />
{#if form.touched['items.0.name'] && form.errors['items.0.name']}
<span class="error">{form.errors['items.0.name'][0]}</span>
{/if}Field Access Patterns
RuneForm provides multiple ways to access and work with form fields:
Direct Data Binding (Simplest)
<input bind:value={form.data.name} />Using getField (Advanced Features)
<script>
// Get field object with additional metadata
const nameField = form.getField('name');
const addressField = form.getField('address.street');
</script>
<input bind:value={nameField.value} />
{#if nameField.touched && nameField.error}
<span>{nameField.error}</span>
{/if}
<!-- Field object provides: -->
<!-- - value: current value -->
<!-- - error: first error message -->
<!-- - errors: all error messages -->
<!-- - touched: boolean -->
<!-- - constraints: validation constraints -->
<!-- - isValidating: boolean -->Dynamic Arrays
RuneForm provides powerful array manipulation with automatic state synchronization:
<script>
const schema = z.object({
todos: z.array(
z.object({
text: z.string().min(1),
completed: z.boolean()
})
)
});
const form = RuneForm.fromSchema(schema, {
todos: [{ text: 'First task', completed: false }]
});
function addTodo() {
form.push('todos', { text: '', completed: false });
}
function removeTodo(index: number) {
form.splice('todos', index, 1);
}
function moveTodoUp(index: number) {
if (index > 0) {
form.swap('todos', index, index - 1);
}
}
</script>
{#each form.data.todos as todo, i (i)}
<div class="todo-item">
<input bind:value={todo.text} placeholder="Todo text" />
<input type="checkbox" bind:checked={todo.completed} />
<button on:click={() => moveTodoUp(i)} disabled={i === 0}> ↑ </button>
<button on:click={() => removeTodo(i)}> Remove </button>
</div>
{/each}
<button on:click={addTodo}>Add Todo</button>Array Operation Methods
// Add items to the end
form.push('items', newItem);
form.push('nested.array', item1, item2, item3);
// Remove items
form.splice('items', startIndex, deleteCount);
// Insert items
form.splice('items', index, 0, newItem1, newItem2);
// Replace items
form.splice('items', index, 1, replacementItem);
// Swap items
form.swap('items', index1, index2);
// Direct array mutations (also tracked!)
form.data.items.push(newItem);
form.data.items[0] = updatedItem;
form.data.items.splice(1, 1);Validation
Automatic Validation
Validation runs automatically with debouncing (100ms default):
<script>
const form = RuneForm.fromSchema(schema);
// Validation happens automatically as user types
</script>
{#if form.isValidating}
<p>Validating...</p>
{/if}
{#if form.isValid}
<p>✓ Form is valid</p>
{/if}Manual Validation
// Validate entire form
await form.validateSchema();
// Custom error handling
form.setCustomError('email', 'This email is already taken');
form.setCustomErrors('password', ['Password is too weak', 'Must contain special characters']);Form State Management
// Check form state
form.isValid; // boolean - true if all validations pass
form.isValidating; // boolean - true during async validation
form.errors; // Record<string, string[]> - validation errors
form.touched; // Record<string, boolean> - touched fields
// Reset form
form.reset(); // Clear all data, errors, and touched state
// Mark fields as touched/pristine
form.markTouched('email');
form.markFieldAsPristine('email');
form.markAllTouched();
form.markAllAsPristine();Memory Management
RuneForm automatically manages memory to prevent leaks:
<script>
import { onDestroy } from 'svelte';
const form = RuneForm.fromSchema(schema);
// Manual disposal (optional - happens automatically)
onDestroy(() => {
form.dispose();
});
</script>Symbol.dispose Support
// Automatic disposal in using blocks (TC39 proposal)
{
using form = RuneForm.fromSchema(schema);
// Form is automatically disposed when leaving scope
}🎯 Advanced Examples
Complex Nested Form
<script lang="ts">
import { RuneForm } from 'rune-form';
import { z } from 'zod';
const schema = z.object({
company: z.object({
name: z.string().min(2),
address: z.object({
street: z.string().min(5),
city: z.string().min(2),
country: z.string().length(2),
coordinates: z.object({
lat: z.number().min(-90).max(90),
lng: z.number().min(-180).max(180)
})
}),
employees: z
.array(
z.object({
name: z.string().min(2),
role: z.string().min(2),
skills: z.array(z.string())
})
)
.min(1)
})
});
const form = RuneForm.fromSchema(schema);
</script>
<!-- Deep nesting with automatic tracking -->
<input bind:value={form.data.company.address.coordinates.lat} type="number" step="0.0001" />
<!-- Array within nested object -->
{#each form.data.company.employees as employee, i (i)}
<div>
<input bind:value={employee.name} />
<input bind:value={employee.role} />
<!-- Nested array -->
{#each employee.skills as skill, j (j)}
<input bind:value={employee.skills[j]} />
{/each}
</div>
{/each}Conditional Validation
const schema = z
.object({
accountType: z.enum(['personal', 'business']),
companyName: z.string().optional(),
taxId: z.string().optional()
})
.refine(
(data) => {
if (data.accountType === 'business') {
return data.companyName && data.taxId;
}
return true;
},
{
message: 'Company name and tax ID required for business accounts',
path: ['companyName']
}
);Async Validation
const schema = z.object({
username: z
.string()
.min(3)
.refine(
async (username) => {
const response = await fetch(`/api/check-username/${username}`);
return response.ok;
},
{ message: 'Username already taken' }
)
});📊 Performance Optimizations
RuneForm is designed for maximum performance:
- Intelligent Caching: Path compilation and field objects are cached
- Debounced Validation: Prevents excessive validation during typing
- Minimal Re-renders: Uses Svelte 5 runes for optimal reactivity
- Memory Management: Automatic cleanup and disposal
- Lazy Evaluation: Only computes what's needed
🔧 API Reference
RuneForm Class
class RuneForm<T extends Record<string, unknown>> {
// Properties
data: T; // Reactive form data
errors: Record<string, string[]>; // Validation errors
touched: Record<string, boolean>; // Touched fields
isValid: boolean; // Form validity
isValidating: boolean; // Validation in progress
// Constructor
constructor(validator: Validator<T>, initialData?: Partial<T>);
// Static factory
static fromSchema<S extends ZodObject>(
schema: S,
initialData?: Partial<z.infer<S>>
): RuneForm<z.infer<S>>;
// Field access
getField<K extends Paths<T>>(path: K): FieldObject;
// Array operations
push<K extends ArrayPaths<T>>(path: K, ...values: PathValue<T, `${K}.${number}`>[]): void;
splice<K extends ArrayPaths<T>>(
path: K,
start: number,
deleteCount?: number,
...items: PathValue<T, `${K}.${number}`>[]
): void;
swap<K extends ArrayPaths<T>>(path: K, i: number, j: number): void;
// State management
markTouched(path: Paths<T>): void;
markFieldAsPristine(path: Paths<T>): void;
markAllTouched(): void;
markAllAsPristine(): void;
reset(): void;
// Validation
validateSchema(): Promise<void>;
setCustomError(path: Paths<T>, message: string): void;
setCustomErrors(path: Paths<T>, messages: string[]): void;
// Cleanup
dispose(): void;
[Symbol.dispose](): void;
}FieldObject Interface
interface FieldObject {
value: any; // Current field value
error: string | undefined; // First error message
errors: string[]; // All error messages
touched: boolean; // Field touched state
constraints: Record<string, any>; // Validation constraints
isValidating: boolean; // Field validation in progress
}Validator Interface
interface Validator<T> {
parse(data: unknown): T;
safeParse(data: unknown): SafeParseResult<T>;
safeParseAsync?(data: unknown): Promise<SafeParseResult<T>>;
resolveDefaults?(data: Partial<T>): T;
getPaths?(): string[];
getInputAttributes?(path: string): Record<string, unknown>;
}🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
📄 License
MIT © Anton Pavlenkov
🙏 Acknowledgments
📚 Resources
Made with ❤️ for the Svelte Community
