Skip to content

Form host for the form controls inside it: owns the vee-validate form (useForm), validates every field on submit, and emits submit with the collected values only when the form is valid. schema takes any Standard Schema (Zod, Valibot, ArkType, Yup) as it is; a failed submit focuses the first invalid field on screen and emits invalid. Server errors (a 422): setErrors(errors) from the slot or useBbFormContext(id) puts each on its field.

import { BbForm } from 'bitboss-ui';

On this page

Props

NameDescription
disableSubmitLock Type: booleanDefaultfalse

Keeps the fields editable while a @submit promise is pending (by default every joined field is read-only until it settles; a @submit that returns nothing never locks). Submit buttons spin either way.

The lock is the right default: a value edited during the save is not the one that was sent. Turn it off for a background autosave the user should keep typing through. It lifts the read-only part only: the submit button that submitted still spins while the save is pending, and isSubmitting in the slot still follows it.

See example
fieldErrors Type: Record<string, unknown> | null

Server validation errors by field path (Inertia's form.errors, a Laravel 422 body), shown on their fields while they are in the object. They never block a submit; a key no field has is in unmatchedErrors. A one-off message: setErrors.

id Type: string

Lets anything outside the form read and drive it through useBbFormContext(id) (a dialog footer, a toolbar, a leave guard). It is also the <form>'s id, so <button form="id" type="submit"> outside it submits it.

The id is for useBbFormContext(id) only: it is not rendered on the <form> element.

See example
keepValuesOnUnmount Type: boolean | Ref<boolean, boolean>

Keep a field's value (and its place in @submit) when it unmounts — the v-if wizard step you left, a collapsed section. Off, an unmounted field drops out of the values.

A field that stops applying for good (not a step you left) still keeps its value: call removeField(path) when you hide it.

See example
model Type: Record<string, any>

The object the fields edit in place (reactive(), a ref, Pinia state, Inertia useForm()): bind it one way, :model, never v-model. A field without its own v-model reads and writes the value at its name path. Nothing is written on mount.

The form fills this object in place; it emits no update:modelValue. With Inertia, pass the useForm() object and declare every field in it: Inertia posts only the keys it was created with.

See example
schema Type: MaybeRef<S | undefined>

One Standard Schema for the form (Zod, Valibot, ArkType, Yup, Joi, Effect), passed as is. An issue lands on the field whose name is its path; @submit gets the typed output. Set it at mount (a computed may change it); per-field rules are then ignored.

Give each type its message too (z.string('Enter a name.')): an emptied text field holds null. Zod and Valibot drop keys the schema does not declare from @submit.

See example
validationMode Type: ValidationMode

When the fields inside validate as the user works: aggressive, balanced, lazy, submit-only or submit-live (see ValidationMode). A field's own validation-mode wins. Unset: the app's formControls.validationMode (default 'balanced').

The five modes, and when to pick each, are in the Validated forms guide.

Events

NameDescription
invalid Type: (payload: BbFormInvalidPayload): void

Fired when a submit fails validation (source: 'submit') or server errors land ('server': setErrors(), a new field-errors message), before the form's own focus; a handler that moves focus keeps it. unmatched: the paths no field claims.

See example
submit Type: (values: BbFormSubmitValues<S>, meta: BbFormSubmitMeta): void

Fired only when every field passes. Carries the values — with a schema, its parsed OUTPUT (transforms and coercions applied), typed from it.

Return the request promise and the form locks until it settles: fields read-only, the submit button that submitted spinning (a submit from code spins none: the button calling submit() spins itself). The second argument is { submitter }, null from code. A synchronous handler never locks. With a schema, annotate the handler with z.output<typeof schema>; with per-step schemas keep the steps array as const.

See example

Slots

The listed properties are the ones exposed to the slot scope.

NameDescription
default Type: { touched: boolean; dirty: boolean; valid: boolean; pending: boolean; initialValues?: Partial<GenericObject> | undefined; errors: Record<string, string[]>; fields: Record<string, BbFormFieldEntry>; isSubmitting: boolean; removeField: (path: string) => void; resetForm: (state?: Partial<FormState<GenericObject>> | undefined, opts?: Partial<ResetFormOpts> | undefined) => void; setErrors: (serverErrors: BbFormServerErrors, options?: BbFormSetErrorsOptions) => void; snapshot: (next?: Record<string, unknown> | undefined) => void; submitCount: number; unmatchedErrors: Record<string, string[]>; validate: (opts?: Partial<ValidationOptions> | undefined) => Promise<FormValidationResult<GenericObject, GenericObject>>; values: GenericObject; }

errors is string[] per path (errors.email?.[0]). unmatchedErrors holds server errors no field owns: render them, nothing else will. setErrors, snapshot and removeField are here too, and on useBbFormContext(id).

See example

Exposed methods

NameDescription
$el Type: HTMLFormElement | null

Renamed and removed

Names from v2 and from earlier 3.0 betas that no longer work, and what to write instead.

  • modelValuemodelfails silently