bitboss-ui/validated ships the standard form controls with
vee-validate field binding already
attached. A validated BbTextInput inside a BbForm needs nothing but a label
and its rules.
BbForm documents the host component itself. This page
is the setup around it.
A separate entrypoint
Import from bitboss-ui/validated only where you want validation. The core
bitboss-ui entrypoint never pulls vee-validate into an app that does not
validate anything. That is why the two exist.
npm i vee-validate # required peer for /validated
npm i @vee-validate/rules @vee-validate/i18n # recommended: standard rules and messages
The controls are the same components. BbTextInput from bitboss-ui/validated
is BbTextInput plus field binding. Every prop you already know still works, and
three more are added.
Define the rules once
Declare your rules with defineRule in a side-effect module, import that module
from the entry point, and then reference them as plain strings on every field.
Over a whole app this is the setup that costs least.
// app/validators.ts
import { configure, defineRule } from 'vee-validate';
import { localize, setLocale } from '@vee-validate/i18n';
import en from '@vee-validate/i18n/dist/locale/en.json';
import { required, email, min_value, max_value, numeric, min, max } from '@vee-validate/rules';
defineRule('required', required);
defineRule('email', email);
defineRule('min_value', min_value);
defineRule('max_value', max_value);
defineRule('numeric', numeric);
defineRule('min', min);
defineRule('max', max);
// Project rules use the same (value, params, ctx) signature.
defineRule('password', (v: unknown) =>
typeof v === 'string' && v.length > 0 && v.length < 8
? 'Password must be at least 8 characters'
: true
);
configure({ generateMessage: localize({ en }) });
setLocale('en');
// main.ts — imported once, for its side effects
import './app/validators';
Build the form
Wrap the fields in <BbForm> and give each one a label (or a legend) and its
rules. The field name is derived from the label, so there is no vid to write.
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { BbButton } from 'bitboss-ui';
import { BbForm, BbTextInput, BbNumberInput, BbCheckbox } from 'bitboss-ui/validated';
const form = reactive({ email: null, quantity: null, terms: false });
// `@submit` hands you the collected values, so the ref has to be able to hold
// them — a bare `ref(null)` infers `Ref<null>` and the assignment will not compile.
const submitted = ref<Record<string, unknown> | null>(null);
</script>
<template>
<BbForm v-slot="{ isSubmitting }" @submit="(v) => (submitted = v)">
<BbTextInput v-model="form.email" label="Email" rules="required|email" />
<BbNumberInput v-model="form.quantity" label="Quantity" rules="required|min_value:1" />
<BbCheckbox v-model="form.terms" label="Accept terms" rules="required" />
<BbButton type="submit" :loading="isSubmitting">Submit</BbButton>
</BbForm>
</template>
@submit fires only when every field passes, with the collected values. The
slot also exposes isSubmitting, submitCount, values, validate and
resetForm: enough for any chrome that lives inside the form.
Prefer rule strings, but a one-off inline function still works:
:rules="(v) => !!v || 'Required'".
Chrome outside the form
A dialog footer, a page toolbar, a leave guard: anything that needs the form's
state but does not live inside it. Give the form an explicit id and read it
with useBbFormContext(id), the same id-context pattern as useBbTableContext
and useBbDropdownContext.
There is no template-ref handle. useBbFormContext is the only outside surface,
and that is deliberate. A ref would let a caller reach past the form's own
contract into whatever vee-validate happens to expose this version.
The three added props
| Prop | Type | Notes |
|---|---|---|
rules | RuleExpression<modelValue> | A rule string ("required|email"), a function, or an object |
validateOn | timing array | When to re-validate. Per-control defaults below |
vid | string | Escape hatch — an explicit field name |
vid is an escape hatch. The field name comes from the label: "Preferred
time" becomes preferredtime. Set it only when that name would not be unique, as
in v-for'd rows or array v-models sharing one label.
<BbTextInput
v-for="(_, i) in form.attendees"
:key="i"
v-model="form.attendees[i]"
label="Attendee"
:vid="`attendee_${i}`"
/>
BbDropzone has no label, so it always needs one.
External errors compose. Whatever you pass to the base errors prop is
merged with the vee-validate messages, so a server error and a rule message
show together rather than replacing each other. That is what makes the same field
work for client rules and an Inertia or Laravel round trip.
When a field re-validates
| Default | Controls |
|---|---|
['blur'] | BbTextInput, BbNumberInput, BbTextarea, BbColorInput, BbDropzone |
['inactive'] | BbSelect, BbTag, BbSlider, BbRating, BbDatePickerInput, BbTimePickerInput, BbCheckboxGroup, BbSwitchGroup, BbRadioGroup |
['update:modelValue', 'change', 'blur'] | BbCheckbox, BbSwitch |
The grouping follows how each control is used, not how it is built. A text field is judged when you leave it, a picker when it goes inactive. A single toggle is judged the moment it flips: there is nothing else to wait for.
The required asterisk
Turn on the plugin option and required fields carry a red *:
// vite.config.ts
bitbossUi({ requiredAsterisk: true });
It shows on any /validated control that is required, or whose rules
contain a required rule, rendered into the control's #label or #legend
slot.
Rules alone are not enough, and deliberately so. Every other rule is conditional
on a value being present. max:5 passes on an empty field, which is what makes
"optional, but at most 5 if filled" expressible. Raising the asterisk for any
rule would mark optional fields as mandatory and contradict the engine.
Both rule shapes are read: the string form ('required|email', arguments
allowed, so 'required:true' counts) and the object form ({ required: true },
where { required: false } correctly shows nothing). required_if and its
relatives do not count: conditionally required is not required.
Core, non-validated components ignore the option entirely. The marker itself is
the exported BbAsterisk, which you can drop into
your own label content when you build a bespoke field.
The controls that ship validated: BbTextInput, BbNumberInput, BbTextarea,
BbSelect, BbTag, BbCheckbox, BbCheckboxGroup, BbSwitch,
BbSwitchGroup, BbRadioGroup, BbRating, BbSlider, BbDatePickerInput,
BbTimePickerInput and BbColorInput. There is also BbForm, and the
useValidatedField composable for wrapping controls of your own.
Start with one BbForm, one shared validators module and rule strings on the
fields. Add useBbFormContext only when controls live outside the form, and use
useValidatedField only for a control that the validated entrypoint does not
already export. For Inertia submissions, continue with
Forms that return promises.