@shelchin/formstate
v0.0.1
Published
Interface-first form state management with headless components for Svelte 5
Maintainers
Readme
@shelchin/formstate
A powerful, type-safe form state management library with support for recursive nesting, dynamic fields, and async validation.
Features
- Interface-first design - Program against interfaces for easy extension and testing
- Framework adapters - Core logic is framework-agnostic, with Svelte 5 adapter included
- Svelte 5 Runes - Uses the latest Svelte 5 reactive API
- Recursive nesting - Deep object and array paths (
user.addresses[0].street) - Dynamic fields - Add/remove fields at runtime
- Async validation - Support for async validators with auto-debounce and cancellation
- Composable UI - Headless components + optional UI components
- Schema-driven - Support for configuration-driven form rendering
- TypeScript - Full type support
- Batch updates - Efficient batch updates to prevent multiple re-renders
- Serialization - Safe serialization supporting BigInt, Date, Map, Set
Installation
npm install @shelchin/formstate
# or
bun add @shelchin/formstateQuick Start
Basic Usage with FormField
<script lang="ts">
import { useFormState, Form, FormField, Validators } from '@shelchin/formstate';
const form = useFormState({
fields: {
email: {
defaultValue: '',
validator: Validators.compose(Validators.required(), Validators.email())
},
password: {
defaultValue: '',
validator: Validators.compose(Validators.required(), Validators.minLength(8))
}
}
});
async function handleSubmit(values: Record<string, unknown>) {
console.log('Submitted:', values);
}
</script>
<Form {form} onSubmit={handleSubmit}>
<FormField name="email" label="Email">
{#snippet children({ value, onInput, onBlur })}
<input type="email" {value} oninput={(e) => onInput(e.currentTarget.value)} onblur={onBlur} />
{/snippet}
</FormField>
<FormField name="password" label="Password">
{#snippet children({ value, onInput, onBlur })}
<input
type="password"
{value}
oninput={(e) => onInput(e.currentTarget.value)}
onblur={onBlur}
/>
{/snippet}
</FormField>
<button type="submit" disabled={!form.isValid}>Submit</button>
</Form>Headless Components (Full UI Control)
<script lang="ts">
import {
useFormState,
Form,
Field,
FieldLabel,
FieldErrorDisplay,
FieldValidating
} from '@shelchin/formstate';
const form = useFormState();
</script>
<Form {form}>
<Field name="username">
{#snippet children({ value, error, touched, validating, setValue, setTouched })}
<div class="my-custom-field">
<FieldLabel required>Username</FieldLabel>
<input
type="text"
{value}
class:error={touched && error}
oninput={(e) => setValue(e.currentTarget.value)}
onblur={() => setTouched()}
/>
<FieldErrorDisplay {error} show={touched} />
<FieldValidating show={validating} />
</div>
{/snippet}
</Field>
</Form>Dynamic Array Fields
<Form {form}>
<FieldArray name="addresses">
{#snippet children({ fields, append, remove })}
{#each fields as field (field.key)}
<div class="address-item">
<FormField name="{field.name}.street" label="Street">
{#snippet children({ value, onInput, onBlur })}
<input {value} oninput={(e) => onInput(e.currentTarget.value)} onblur={onBlur} />
{/snippet}
</FormField>
<button type="button" onclick={() => remove(field.index)}>Remove</button>
</div>
{/each}
<button type="button" onclick={() => append({ street: '', city: '' })}>Add Address</button>
{/snippet}
</FieldArray>
</Form>Core API
FormStateManager
The core state manager handles all form logic.
import { FormStateManager, Validators } from '@shelchin/formstate';
const manager = new FormStateManager({
validateOnChange: true,
validateOnBlur: true,
fields: {
email: {
defaultValue: '',
validator: Validators.email()
}
}
});
// Set values
manager.setValue('email', '[email protected]');
// Get values
manager.getValue('email'); // '[email protected]'
manager.getValues(); // { email: '[email protected]' }
// Validation
await manager.validateField('email');
await manager.validateForm();
// State queries
manager.isDirty(); // true if any field changed
manager.isValid(); // true if no errors
manager.isValidating(); // true if validation in progress
manager.getErrors(); // { field: 'error message', ... }
manager.getDirtyFields(); // ['email', ...]
manager.getDirtyValues(); // { email: '...' }
// Submit
const success = await manager.submit(async (values) => {
await saveToServer(values);
});
// Reset
manager.reset();
manager.reset({ email: '[email protected]' }); // Reset with new initial valuesBatch Updates
Batch multiple updates to prevent multiple re-renders:
manager.batchUpdate(() => {
manager.setValue('firstName', 'John', false);
manager.setValue('lastName', 'Doe', false);
manager.setValue('email', '[email protected]', false);
}); // Only triggers one UI updateValidators
Built-in validators and composition:
import { Validators, createValidator, createCustomValidator } from '@shelchin/formstate';
// Built-in validators
Validators.required('Custom message');
Validators.email();
Validators.minLength(3);
Validators.maxLength(100);
Validators.min(0);
Validators.max(100);
Validators.pattern(/^[A-Z]+$/);
// Compose validators
const emailValidator = Validators.compose(
Validators.required(),
Validators.email(),
Validators.maxLength(255)
);
// Custom validator
const passwordMatch = createValidator((value, allValues) => {
return value === allValues.password ? null : 'Passwords must match';
});
// Async custom validator
const usernameAvailable = createCustomValidator(async (value) => {
const res = await fetch(`/api/check-username?name=${value}`);
return (await res.json()).available;
}, 'Username is already taken');
// Use with dependencies
const form = useFormState({
fields: {
confirmPassword: {
validator: passwordMatch,
dependencies: ['password'] // Re-validate when password changes
}
}
});Transformers
Transform values on input:
import { Transformers } from '@shelchin/formstate';
const form = useFormState({
fields: {
username: {
transformer: Transformers.trim
},
code: {
transformer: Transformers.toUpperCase
},
amount: {
transformer: Transformers.toNumber
},
email: {
transformer: Transformers.compose(Transformers.trim, Transformers.toLowerCase)
}
}
});PathUtils
Utility for working with nested paths:
import { PathUtils } from '@shelchin/formstate';
const obj = { user: { addresses: [{ city: 'NYC' }] } };
PathUtils.get(obj, 'user.addresses[0].city'); // 'NYC'
PathUtils.set(obj, 'user.addresses[0].city', 'LA'); // Immutable update
PathUtils.delete(obj, 'user.addresses[0]'); // Remove array element
PathUtils.push(obj, 'user.addresses', { city: 'Boston' }); // Add to array
PathUtils.move(obj, 'user.addresses', 0, 1); // Reorder arraySerialization
Safe serialization with BigInt, Date, Map, Set support:
import { safeStringify, safeParse, cloneViaSerialization } from '@shelchin/formstate';
const data = {
amount: 123456789012345678901234567890n,
createdAt: new Date(),
tags: new Set(['a', 'b']),
metadata: new Map([['key', 'value']])
};
const json = safeStringify(data);
const restored = safeParse(json);
// Deep clone
const clone = cloneViaSerialization(data);Debounced Validation
Validate after input completion:
const form = useFormState({
fields: {
search: {
validateOnComplete: true,
debounceMs: 300,
completeCondition: (value) => value.length >= 3,
validator: async (value) => {
// Only runs after user stops typing for 300ms
// and input is at least 3 characters
}
}
}
});Components
| Component | Description |
| ------------------- | ------------------------------ |
| Form | Form root, provides context |
| Field | Headless field component |
| FormField | Styled field component |
| FieldArray | Dynamic array field management |
| SchemaRenderer | Schema-driven form renderer |
| FieldLabel | Label component |
| FieldErrorDisplay | Error message display |
| FieldDescription | Help text/description |
| FieldValidating | Loading indicator during async |
Testing
# Run tests
bun run test
# Watch mode
bun run test:watchThe package includes comprehensive tests for:
- FormStateManager (69 tests)
- Validators (34 tests)
- Transformers (18 tests)
- PathUtils (27 tests)
- Serialization (25 tests)
Architecture
packages/formstate/
├── src/
│ ├── core/ # Framework-agnostic core
│ │ ├── interfaces.ts # Type definitions
│ │ ├── FormStateManager.ts # State management
│ │ ├── Validators.ts # Validation system
│ │ └── Transformers.ts # Value transformers
│ ├── utils/
│ │ ├── PathUtils.ts # Path utilities
│ │ └── serialize.ts # Safe serialization
│ └── adapters/
│ └── svelte/ # Svelte 5 adapter
│ ├── useFormState.svelte.ts
│ ├── schema.ts
│ └── components/
└── examples/ # Example implementationsLicense
MIT
