@umpire/solid
@umpire/solid gives Solid apps two small APIs.
useUmpire()when a component owns the valuesfromSolidStore()when one shared store or context should back the form
If you want to use reactiveUmp() directly, see Signals — Solid.
Install
Section titled “Install”yarn add @umpire/core @umpire/solid solid-jsuseUmpire()
Section titled “useUmpire()”Use useUmpire() when your component already owns the values and just wants derived availability plus fouls.
import type { Accessor } from 'solid-js'import type { AvailabilityMap, FieldDef, Foul, InputValues, Umpire } from '@umpire/core'import { useUmpire } from '@umpire/solid'
function useUmpire< F extends Record<string, FieldDef>, C extends Record<string, unknown>,>( ump: Umpire<F, C>, values: Accessor<InputValues>, conditions?: Accessor<C>,): { check: Accessor<AvailabilityMap<F>> fouls: Accessor<Foul<F>[]>}Example
Section titled “Example”import { createStore } from 'solid-js/store'import { enabledWhen, requires, umpire } from '@umpire/core'import { useUmpire } from '@umpire/solid'
const signupUmp = umpire({ fields: { email: { required: true, isEmpty: (v: unknown) => !v }, password: { required: true, isEmpty: (v: unknown) => !v }, confirmPassword: { required: true, isEmpty: (v: unknown) => !v }, companyName: { isEmpty: (v: unknown) => !v }, companySize: { isEmpty: (v: unknown) => !v }, }, rules: [ requires('confirmPassword', 'password'), enabledWhen('companyName', (_v, conditions) => conditions.plan === 'business', { reason: 'business plan required', }), enabledWhen('companySize', (_v, conditions) => conditions.plan === 'business', { reason: 'business plan required', }), requires('companySize', 'companyName'), ],})
type SignupConditions = { plan: 'personal' | 'business' }
function SignupForm(props: { plan: Accessor<SignupConditions['plan']> }) { const [values, setValues] = createStore(signupUmp.init())
const { check, fouls } = useUmpire( signupUmp, () => values, () => ({ plan: props.plan() }), )
function applyFoul<K extends keyof typeof values>( field: K, value: (typeof values)[K], ) { setValues(field, value) }
function applyResets() { for (const foul of fouls()) { applyFoul(foul.field, foul.suggestedValue) } }
return ( <form> <input value={String(values.password ?? '')} onInput={(e) => setValues('password', e.currentTarget.value)} />
<input value={String(values.confirmPassword ?? '')} disabled={!check().confirmPassword.enabled} onInput={(e) => setValues('confirmPassword', e.currentTarget.value)} />
{!check().companyName.enabled && <p>{check().companyName.reason}</p>}
{fouls().length > 0 && ( <button type="button" onClick={applyResets}>Apply resets</button> )} </form> )}Behavior
Section titled “Behavior”valuesandconditionsare accessors.check()andfouls()are accessors.- Previous values are tracked internally.
Like @umpire/react, the hook stays focused on derivation. You decide when to act on fouls.
fromSolidStore()
Section titled “fromSolidStore()”Use fromSolidStore() when the form lives in shared Solid state and child components should read from the same Umpire instance.
import type { Accessor } from 'solid-js'import type { FieldDef, FieldValues, FieldsOf, Umpire } from '@umpire/core'import { fromSolidStore } from '@umpire/solid'
function fromSolidStore< F extends Record<string, FieldDef>, C extends Record<string, unknown> = Record<string, unknown>,>( ump: Umpire<F, C>, options: { values: FieldValues<F> set<K extends keyof F & string>(name: K, value: FieldValues<F>[K]): void conditions?: Partial<{ [K in keyof C & string]: Accessor<C[K]> }> },): SolidStoreUmpire<F>Example
Section titled “Example”import { createContext, useContext } from 'solid-js'import { createStore } from 'solid-js/store'import { enabledWhen, type FieldsOf, umpire } from '@umpire/core'import { fromSolidStore, type SolidStoreUmpire } from '@umpire/solid'
const eventUmp = umpire({ fields: { allDay: { default: false }, startTime: { default: '' }, endTime: { default: '' }, }, rules: [ enabledWhen('startTime', (values) => !values.allDay, { reason: 'all-day events do not use times' }), enabledWhen('endTime', (values) => !values.allDay, { reason: 'all-day events do not use times' }), ],})
type EventForm = SolidStoreUmpire<FieldsOf<typeof eventUmp>>type EventValues = ReturnType<typeof eventUmp.init>
const EventFormContext = createContext<{ values: EventValues set: EventForm['set'] form: EventForm}>()
function EventFormProvider(props: { children: JSX.Element }) { const [values, setValues] = createStore(eventUmp.init({ startTime: '09:00', endTime: '10:00', }))
const setEventValue: EventForm['set'] = (name, value) => { setValues(name, value) }
const form = fromSolidStore(eventUmp, { values, set: setEventValue, })
return ( <EventFormContext.Provider value={{ values, set: setEventValue, form }}> {props.children} </EventFormContext.Provider> )}
function EndTimeField() { const ctx = useContext(EventFormContext)! const status = () => ctx.form.field('endTime')
return ( <input value={ctx.values.endTime} disabled={!status().enabled} onInput={(e) => ctx.set('endTime', e.currentTarget.value)} /> )}The returned object gives you field(), foul(), set(), update(), values, fouls, and dispose().
When to pick which
Section titled “When to pick which”- Choose
useUmpire()when one component owns the values and just needs derived availability. - Choose
fromSolidStore()when several children should read from one shared form state. - Choose
@umpire/signals/solidwhen you want to work withreactiveUmp()directly.
- If you create a long-lived shared instance outside component scope, call
dispose()when you’re done.