Skip to content

@umpire/solid

@umpire/solid gives Solid apps two small APIs.

  • useUmpire() when a component owns the values
  • fromSolidStore() when one shared store or context should back the form

If you want to use reactiveUmp() directly, see Signals — Solid.

Terminal window
yarn add @umpire/core @umpire/solid solid-js

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>[]>
}
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>
)
}
  • values and conditions are accessors.
  • check() and fouls() are accessors.
  • Previous values are tracked internally.

Like @umpire/react, the hook stays focused on derivation. You decide when to act on fouls.

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>
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().

  • 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/solid when you want to work with reactiveUmp() directly.
  • If you create a long-lived shared instance outside component scope, call dispose() when you’re done.