Field Arrays
Demonstrates dynamic list of form inputs with add/remove and per-item validation.
See interactive examples below.
1import { createSignal, createMemo } from '@barefootjs/dom'2import { Input } from '@/components/ui/input'3import { Button } from '@/components/ui/button'45type EmailField = {6 id: number7 value: string8 touched: boolean9}1011const [fields, setFields] = createSignal<EmailField[]>([12 { id: 1, value: '', touched: false }13])14const [nextId, setNextId] = createSignal(2)1516const validateEmail = (email: string): string => {17 if (email.trim() === '') return 'Email is required'18 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Invalid email format'19 return ''20}2122const handleAdd = () => {23 setFields([...fields(), { id: nextId(), value: '', touched: false }])24 setNextId(nextId() + 1)25}2627const handleRemove = (id: number) => {28 if (fields().length > 1) {29 setFields(fields().filter(f => f.id !== id))30 }31}3233const handleChange = (id: number, value: string) => {34 setFields(fields().map(f => f.id === id ? { ...f, value } : f))35}3637{fields().map((field, index) => (38 <div key={field.id}>39 <Input40 inputValue={field.value}41 onInput={(e) => handleChange(field.id, e.target.value)}42 />43 <Button onClick={() => handleRemove(field.id)}>Remove</Button>44 </div>45))}46<Button onClick={handleAdd}>+ Add Email</Button>#Pattern Overview
Field arrays in BarefootJS use a createSignal containing an array of field objects. Each field has a unique ID for proper list reconciliation, and its own value and touched state.
Key concepts:
- Field object: Contains id, value, and touched state
- Unique ID: Each field has a unique ID for stable key management
- Per-field validation: Validate each field independently
- Cross-field validation: Check duplicates or dependencies across fields
- Immutable updates: Use map/filter to update the array signal
#Examples
#Basic Field Array
1 email(s) added
1import { createSignal, createMemo } from '@barefootjs/dom'2import { Input } from '@/components/ui/input'3import { Button } from '@/components/ui/button'45type EmailField = {6 id: number7 value: string8 touched: boolean9}1011const [fields, setFields] = createSignal<EmailField[]>([12 { id: 1, value: '', touched: false }13])14const [nextId, setNextId] = createSignal(2)1516const validateEmail = (email: string): string => {17 if (email.trim() === '') return 'Email is required'18 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Invalid email format'19 return ''20}2122const handleAdd = () => {23 setFields([...fields(), { id: nextId(), value: '', touched: false }])24 setNextId(nextId() + 1)25}2627const handleRemove = (id: number) => {28 if (fields().length > 1) {29 setFields(fields().filter(f => f.id !== id))30 }31}3233const handleChange = (id: number, value: string) => {34 setFields(fields().map(f => f.id === id ? { ...f, value } : f))35}3637{fields().map((field, index) => (38 <div key={field.id}>39 <Input40 inputValue={field.value}41 onInput={(e) => handleChange(field.id, e.target.value)}42 />43 <Button onClick={() => handleRemove(field.id)}>Remove</Button>44 </div>45))}46<Button onClick={handleAdd}>+ Add Email</Button>#Duplicate Detection
1import { createSignal, createMemo } from '@barefootjs/dom'23const isDuplicate = (id: number, value: string): boolean => {4 if (value.trim() === '') return false5 return fields().some(f => f.id !== id && f.value.toLowerCase() === value.toLowerCase())6}78const getFieldError = (field: EmailField): string => {9 if (!field.touched) return ''10 const basicError = validateEmail(field.value)11 if (basicError) return basicError12 if (isDuplicate(field.id, field.value)) return 'Duplicate email'13 return ''14}1516const duplicateCount = createMemo(() => {17 const values = fields().map(f => f.value.toLowerCase().trim()).filter(v => v !== '')18 const uniqueValues = new Set(values)19 return values.length - uniqueValues.size20})2122{duplicateCount() > 0 && (23 <p class="text-amber-400">{duplicateCount()} duplicate(s) detected</p>24)}#Min/Max Field Constraints
1 / 5 emails
1import { createSignal, createMemo } from '@barefootjs/dom'23const MIN_FIELDS = 14const MAX_FIELDS = 556const canAdd = createMemo(() => fields().length < MAX_FIELDS)7const canRemove = createMemo(() => fields().length > MIN_FIELDS)89const handleAdd = () => {10 if (canAdd()) {11 setFields([...fields(), { id: nextId(), value: '', touched: false }])12 setNextId(nextId() + 1)13 }14}1516const handleRemove = (id: number) => {17 if (canRemove()) {18 setFields(fields().filter(f => f.id !== id))19 }20}2122<Button onClick={handleAdd} disabled={!canAdd()}>23 + Add Email24</Button>25<p>{fields().length} / {MAX_FIELDS} emails</p>#Key Points
Array State Management
- Store field array in a single signal:
createSignal<Field[]>([]) - Each field object contains: id, value, touched (and any other state)
- Use immutable operations:
map(),filter(), spread operator - Maintain a separate counter signal for generating unique IDs
Key Management
- Always use
key={field.id}for list items - Never use array index as key (causes issues on reorder/delete)
- Generate unique IDs with incrementing counter:
nextId() - Unique keys ensure proper DOM reconciliation
Per-Item Validation
- Create a validation function that takes the field value
- Check touched state before showing errors
- Update touched state on blur:
onBlur={() => handleBlur(field.id)} - Each field error is computed independently
Cross-Field Validation
- Access entire array in validation:
fields().some() - Use
createMemofor derived validations (e.g., duplicate count) - Exclude current field when checking duplicates:
f.id !== id - Show summary warnings for array-level issues
Add/Remove Operations
- Add: Spread existing array and append new field
- Remove: Filter out field by ID
- Enforce min/max constraints with
createMemofor canAdd/canRemove - Disable buttons when constraints are reached