Introduction
Accordion
Badge
Button
Card
Checkbox
Counter
Dialog
Dropdown
Input
Select
Switch
Tabs
Toast
Tooltip
Controlled Input
Field Arrays
Submit
Validation

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 createMemo for 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 createMemo for canAdd/canRemove
  • Disable buttons when constraints are reached