npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rune-form

v0.1.5

Published

Type-safe reactive form builder for Svelte 5

Downloads

216

Readme

RuneForm

npm version License TypeScript Svelte 5 Zod Compatible

The most powerful reactive form library for Svelte 5

DocumentationQuick StartExamplesAPI 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-form

Requirements

  • 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

  • Built for Svelte 5
  • Validation powered by Zod
  • Inspired by modern form libraries

📚 Resources


Made with ❤️ for the Svelte Community

⬆ Back to top