Overview
reatomField creates a reactive atom representing a form field with built-in support for:
- Value change handling with filtering
- Focus and blur tracking
- Validation (sync and async)
- Dirty state management
- State/value transformations
Type Signature
function reatomField<State, Value = State>(
initState: State,
options?: FieldOptions<State, Value>
): FieldAtom<State, Value>
Parameters
State
required
The initial value of the field
FieldOptions<State, Value> | string
Field configuration options or debug name string
Show properties
Show properties
string
Debug name for the field and related atoms
ValidateOption | StandardSchemaV1
Validation function or Standard Schema. Can be sync or async.
Receives
{ value, state, focus } and should return error message or throw.boolean
default:"false"
Trigger validation when field value changes
boolean
default:"false"
Trigger validation when field loses focus
boolean
default:"false"
Trigger validation when field is first subscribed to
boolean
default:"true"
Keep validation error visible when value changes
boolean
default:"false"
Keep previous error during async validation
(newValue, prevValue) => boolean
Filter function to prevent certain value changes. Return
false to skip update.(newValue, prevValue) => boolean
Custom function to determine if field is dirty. Defaults to deep equality check.
(state, self) => Value
Transform internal state to user-facing value. Enables reactive computed values.
(value, self) => State
Transform user input to internal state. Can throw
throwAbort() to cancel update.Return Value
FieldAtom<State, Value>
Field atom with the following properties:
Show properties
Show properties
Atom<Value>
Computed atom with the field’s value (transformed via
fromState if provided)Action<[Value], Value>
Action to update the field value
Computed<FocusState>
Focus state:
{ active: boolean, dirty: boolean, touched: boolean }focus.in()- Mark field as focusedfocus.out()- Mark field as blurred
Computed<ValidationState>
Validation state:
{ error?: string, triggered: boolean, validating?: Promise }validation.trigger()- Manually trigger validationvalidation.errors()- Array of all validation errorsvalidation.clearErrors(source?)- Clear validation errors
Atom<boolean>
Whether the field is disabled. Disabled fields are excluded from validation and focus tracking.
Action
Reset field to initial state
Atom<State>
The initial state atom. Can be updated to change the reset target.
Atom<HTMLElement | null>
Reference to the associated DOM element (useful for autofocus)
Atom<FieldOptions>
Field options that can be dynamically updated
Examples
Basic Field
import { reatomField } from '@reatom/framework'
const emailField = reatomField('', 'emailField')
// Change value
emailField.change('user@example.com')
// Read value
console.log(emailField.value()) // 'user@example.com'
console.log(emailField()) // 'user@example.com' (state)
// Focus tracking
emailField.focus.in()
console.log(emailField.focus().active) // true
emailField.focus.out()
console.log(emailField.focus().touched) // true
Field with Validation
import { reatomField } from '@reatom/framework'
const usernameField = reatomField('', {
name: 'username',
validate: ({ value }) => {
if (value.length < 3) return 'Username too short'
if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Invalid characters'
},
validateOnChange: true,
})
usernameField.change('ab')
console.log(usernameField.validation().error) // 'Username too short'
usernameField.change('alice')
console.log(usernameField.validation().error) // undefined
Async Validation
import { reatomField } from '@reatom/framework'
import { wrap } from '@reatom/framework'
const emailField = reatomField('', {
name: 'email',
validate: async ({ value }) => {
if (!value.includes('@')) return 'Invalid email'
// Check if email is available
const response = await wrap(
fetch(`/api/check-email?email=${value}`)
)
const { available } = await response.json()
if (!available) throw new Error('Email already taken')
},
validateOnBlur: true,
})
emailField.change('test@example.com')
emailField.focus.out()
// Check validation state
console.log(emailField.validation().validating) // Promise
await wrap(emailField.validation().validating)
console.log(emailField.validation().error) // 'Email already taken' or undefined
Standard Schema Validation
import { reatomField } from '@reatom/framework'
import { z } from 'zod'
const ageField = reatomField(0, {
name: 'age',
validate: z.number().min(18, 'Must be 18 or older'),
validateOnChange: true,
})
ageField.change(15)
console.log(ageField.validation().error) // 'Must be 18 or older'
ageField.change(25)
console.log(ageField.validation().error) // undefined
State/Value Transformation
import { reatomField } from '@reatom/framework'
// Store as number, display as formatted string
const priceField = reatomField<number, string>(100.5, {
name: 'price',
fromState: (state) => state.toFixed(2), // number -> string
toState: (value) => parseFloat(value), // string -> number
})
console.log(priceField()) // 100.5 (state)
console.log(priceField.value()) // '100.50' (value)
priceField.change('99.99')
console.log(priceField()) // 99.99
console.log(priceField.value()) // '99.99'
Reactive Transformations
import { atom, reatomField } from '@reatom/framework'
const decimalPlaces = atom(2, 'decimalPlaces')
const priceField = reatomField<number, string>(100.5, {
name: 'price',
fromState: (state) => state.toFixed(decimalPlaces()),
toState: (value) => {
if (!value) return 0
const parsed = parseFloat(value)
const multiplier = Math.pow(10, decimalPlaces())
return Math.round(parsed * multiplier) / multiplier
},
})
priceField.change('100.12345')
console.log(priceField.value()) // '100.12'
decimalPlaces.set(4)
console.log(priceField.value()) // '100.1200'
Aborting Invalid Updates
import { reatomField, throwAbort } from '@reatom/framework'
const numberField = reatomField(0, {
name: 'number',
fromState: (state) => state.toString(),
toState: (value: string) => {
const parsed = Number(value)
// Abort if invalid, keeping previous value
return isNaN(parsed) ? throwAbort() : parsed
},
})
numberField.change('123')
console.log(numberField()) // 123
numberField.change('invalid')
console.log(numberField()) // 123 (unchanged)
Custom Filter
import { reatomField } from '@reatom/framework'
const uppercaseField = reatomField('', {
name: 'uppercase',
filter: (newValue, prevValue) => {
// Only allow uppercase changes
return newValue === newValue.toUpperCase()
},
})
uppercaseField.change('HELLO')
console.log(uppercaseField.value()) // 'HELLO'
uppercaseField.change('hello')
console.log(uppercaseField.value()) // 'HELLO' (unchanged)
Dynamic Validation
import { reatomField } from '@reatom/framework'
const passwordField = reatomField('', 'password')
const confirmField = reatomField('', {
name: 'confirm',
validateOnChange: true,
validate: () => {
if (
!passwordField.validation().error &&
passwordField.value() !== confirmField.value()
) {
return 'Passwords do not match'
}
},
})
passwordField.change('secret123')
confirmField.change('secret456')
console.log(confirmField.validation().error) // 'Passwords do not match'
confirmField.change('secret123')
console.log(confirmField.validation().error) // undefined
Conditional Validation
import { reatomField } from '@reatom/framework'
import { z } from 'zod'
const field = reatomField(90, {
name: 'field',
validateOnChange: true,
validate: ({ focus }) => {
// Only validate if field has been modified
if (focus.dirty) {
return z.number().min(100, 'Must be at least 100')
}
},
})
// No validation on first render
console.log(field.validation().error) // undefined
// Validation triggers after change
field.change(91)
console.log(field.validation().error) // 'Must be at least 100'
Disabled State
import { reatomField } from '@reatom/framework'
const field = reatomField('', {
validate: () => 'Error',
validateOnChange: true,
})
field.change('value')
console.log(field.validation().error) // 'Error'
console.log(field.focus().dirty) // true
// Disable field
field.disabled.set(true)
console.log(field.validation().error) // undefined
console.log(field.focus().dirty) // false
// Re-enable
field.disabled.set(false)
console.log(field.validation().error) // 'Error'
Element Reference
import { reatomField } from '@reatom/framework'
const field = reatomField('', 'field')
// In your component
const inputElement = document.querySelector('input')
field.elementRef.set(inputElement)
// Later, focus programmatically
field.elementRef()?.focus()
Reset with Custom State
import { reatomField } from '@reatom/framework'
const field = reatomField(100, 'field')
field.change(200)
console.log(field()) // 200
// Reset to initial
field.reset()
console.log(field()) // 100
// Reset to custom value
field.reset(300)
console.log(field()) // 300
console.log(field.initState()) // 300 (updated)
Related
- reatomForm - Create complete forms
- reatomFieldArray - Manage dynamic field arrays
- withField - Extend existing atoms with field capabilities