Outside a form

Not every required choice is an input. A plan picker, a map marker, a signature pad and a file dropzone all have a valid and an invalid state, and none of them are form controls.

ref={validate()} works on any element. Give it a data-name so its errors have a key, and call submit() yourself instead of relying on a form:

function PlanPicker() {
  const [plan, setPlan] = createSignal<string | null>(null);
  const { validate, submit, errors, isSubmitting } = useForm();

  const planChosen = () => !plan() && 'Choose a plan to continue';

  return (
    <>
      <div ref={validate(() => [planChosen])} data-name='plan'>
        <For each={plans}>
          {name => <button onClick={() => setPlan(name)}>{name}</button>}
        </For>
      </div>
      <span>{errors.plan}</span>

      <button disabled={isSubmitting()} onClick={() => submit(checkout)}>
        Continue
      </button>
    </>
  );
}

submit()

submit(callback, payload?) runs the same sequence formSubmit does: check every registered field, focus and scroll to the first failure, and call the callback only if everything passed. The optional payload is handed to the callback, which is how formSubmit passes the form element through.

await submit(async plan => api.checkout(plan), plan());

Errors do not self-clear on a non-input

The automatic clearing is bound to the element's input event. A div never fires one, so an error on a non-input element stays visible until the next submit(), validateField() or form reset, even after the underlying choice has changed.

Call validateField when the value changes if you want the message to disappear immediately:

function choose(name: string) {
  setPlan(name);
  validateField('plan');
}

Mixing both

Registered elements all live in the same form, whether they are inputs or not. A <form ref={formSubmit(onSubmit)}> containing a div ref={validate()} validates both on submit, and a single errors store covers them.

Unmounted elements

A field removed from the document is dropped during the next submit rather than blocking it. Conditional sections can appear and disappear without leaving a stale error that nothing can clear.