Field Arrays
Dynamic list of inputs. The array is a raw signal; the per-item rule is the same Zod schema you'd hand to createForm.
See interactive examples below.
1import { createSignal } from '@barefootjs/client'2import { z } from 'zod'34// Same per-item schema you'd nest inside createForm5const emailSchema = z6 .string()7 .min(1, 'Email is required')8 .email('Invalid email format')910const validateEmail = (v: string) => {11 const r = emailSchema.safeParse(v)12 return r.success ? '' : r.error.issues[0]?.message ?? ''13}1415type Item = { value: string; touched: boolean }1617const [items, setItems] = createSignal<Item[]>([{ value: '', touched: false }])1819const itemError = (item: Item) =>20 item.touched ? validateEmail(item.value) : ''2122const update = (i: number, value: string) =>23 setItems(items().map((it, idx) => idx === i ? { ...it, value } : it))2425const blur = (i: number) =>26 setItems(items().map((it, idx) => idx === i ? { ...it, touched: true } : it))2728const add = () =>29 setItems([...items(), { value: '', touched: false }])3031const remove = (i: number) => {32 if (items().length > 1) setItems(items().filter((_, idx) => idx !== i))33}3435{items().map((item, i) => (36 <div key={i}>37 <input38 value={item.value}39 onInput={(e) => update(i, e.target.value)}40 onBlur={() => blur(i)}41 />42 <p>{itemError(item)}</p>43 <button onClick={() => remove(i)}>X</button>44 </div>45))}46<button onClick={add}>+ Add Email</button>#Overview
createForm targets fixed-shape records: it validates the array on submit but routes errors to dot-paths (emails.0, emails.1, …), not the top-level field, so per-item live feedback isn't reachable through field('emails').error(). Instead, store the array in a createSignal of { value, touched } objects and reuse the same per-item Zod schema you'd otherwise nest in createForm.
#Examples
#Basic Field Array
1 email(s) added
1import { createSignal } from '@barefootjs/client'2import { z } from 'zod'34// Same per-item schema you'd nest inside createForm5const emailSchema = z6 .string()7 .min(1, 'Email is required')8 .email('Invalid email format')910const validateEmail = (v: string) => {11 const r = emailSchema.safeParse(v)12 return r.success ? '' : r.error.issues[0]?.message ?? ''13}1415type Item = { value: string; touched: boolean }1617const [items, setItems] = createSignal<Item[]>([{ value: '', touched: false }])1819const itemError = (item: Item) =>20 item.touched ? validateEmail(item.value) : ''2122const update = (i: number, value: string) =>23 setItems(items().map((it, idx) => idx === i ? { ...it, value } : it))2425const blur = (i: number) =>26 setItems(items().map((it, idx) => idx === i ? { ...it, touched: true } : it))2728const add = () =>29 setItems([...items(), { value: '', touched: false }])3031const remove = (i: number) => {32 if (items().length > 1) setItems(items().filter((_, idx) => idx !== i))33}3435{items().map((item, i) => (36 <div key={i}>37 <input38 value={item.value}39 onInput={(e) => update(i, e.target.value)}40 onBlur={() => blur(i)}41 />42 <p>{itemError(item)}</p>43 <button onClick={() => remove(i)}>X</button>44 </div>45))}46<button onClick={add}>+ Add Email</button>#Duplicate Detection
1// Reuse the per-item schema, then layer a cross-item rule on top.2const itemError = (item: Item, i: number) => {3 if (!item.touched) return ''4 const basic = validateEmail(item.value)5 if (basic) return basic6 const lower = item.value.toLowerCase()7 const isDup = items().some((o, idx) => idx !== i && o.value.toLowerCase() === lower)8 return isDup ? 'Duplicate email' : ''9}1011const duplicateCount = createMemo(() => {12 const values = items().map(it => it.value.toLowerCase().trim()).filter(v => v !== '')13 return values.length - new Set(values).size14})#Min / Max Field Constraints
1 / 5 emails
1const MIN_FIELDS = 12const MAX_FIELDS = 534const canAdd = createMemo(() => items().length < MAX_FIELDS)5const canRemove = createMemo(() => items().length > MIN_FIELDS)67<button onClick={add} disabled={!canAdd()}>+ Add Email</button>8<p>{items().length} / {MAX_FIELDS} emails</p>