Appearance
@vibe-labs/design-vue-forms
SPA-friendly form management for the Vibe Design System. No visual components beyond a renderless <VibeForm> wrapper — just composables, validation, and field bindings that wire into @vibe-labs/design-vue-inputs (or any v-model component).
Installation
ts
import { VibeForm, useVibeForm, required, email } from "@vibe-labs/design-vue-forms";Peer dependency: vue ^3.5.18. No CSS package required — this is pure logic.
Quick Start
vue
<script setup lang="ts">
import { VibeForm, required, email } from "@vibe-labs/design-vue-forms";
import { VibeInput } from "@vibe-labs/design-vue-inputs";
const initialValues = { email: "", password: "" };
function validate(values: typeof initialValues) {
const errors: Record<string, string> = {};
if (!values.email) errors.email = "Required";
if (values.password.length < 8) errors.password = "Must be at least 8 characters";
return errors;
}
async function onSubmit(values: typeof initialValues, { setErrors, setFormError }) {
try {
await api.login(values);
router.push("/dashboard");
} catch (e) {
if (e.fieldErrors) setErrors(e.fieldErrors);
else setFormError("Something went wrong");
}
}
</script>
<template>
<VibeForm v-slot="form" :initial-values="initialValues" :validate="validate" @submit="onSubmit">
<VibeInput v-bind="form.field('email')" label="Email" type="email" />
<VibeInput v-bind="form.field('password')" label="Password" type="password" />
<button :disabled="form.submitting.value">Sign in</button>
</VibeForm>
</template>VibeForm renders a <form novalidate> element, calls useVibeForm internally, and exposes the full form API via its scoped slot. The @submit event only fires when validation passes.
Components
<VibeForm>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
initial-values | T | — | Starting values. Defines the form shape and TypeScript type. |
validate | (values: T) => FormErrors<T> | — | Form-level validation. Return errors object — empty = valid. Can be async. |
validate-on | "change" | "blur" | "submit" | "submit" | When to re-run form-level validation after first submit. |
no-form-error | boolean | false | Disable the built-in form error display. |
id | string | — | HTML id for the form element. |
autocomplete | string | — | HTML autocomplete for the form element. |
Events
| Event | Payload | Description |
|---|---|---|
submit | (values: T, helpers: FormSubmitHelpers<T>) | Fires when all validation passes. |
reset | — | Fires after form.reset() is called. |
Slots
| Slot | Scope | Description |
|---|---|---|
default | FormReturn<T> | The full form API. |
form-error | { error: string | null } | Override the default form error display. |
Scoped Slot API (FormReturn<T>)
Field Binding
vue
<!-- The easy way — spreads modelValue, onUpdate:modelValue, name, error, onBlur -->
<VibeInput v-bind="form.field('email')" label="Email" />
<!-- The explicit way — full control -->
<VibeInput v-model="form.values.email" :error="form.errors.email" name="email" @blur="form.touchField('email')" />form.field() is fully typed — autocomplete suggests keys from initialValues.
Reactive State
| Property | Type | Description |
|---|---|---|
values | T | Reactive form values. Mutate directly or via field(). |
errors | Partial<Record<keyof T, string>> | Field-level errors. |
formError | Ref<string | null> | Non-field error (e.g. network failure). |
touched | Record<keyof T, boolean> | Which fields have been blurred. |
dirty | ComputedRef<boolean> | True if any value differs from initial. |
submitting | Ref<boolean> | True while onSubmit is executing. |
submitted | Ref<boolean> | True after first submit attempt. |
submitCount | Ref<number> | Total submit attempts. |
valid | ComputedRef<boolean> | True when no field errors and no form error. |
Methods
| Method | Description |
|---|---|
field(name) | Returns bindable props for a field. |
handleSubmit(e?) | Trigger submission programmatically. |
setErrors(errors) | Set multiple field errors (e.g. from server response). |
setFieldError(name, msg) | Set a single field error. |
clearFieldError(name) | Clear a single field error. |
clearErrors() | Clear all errors. |
setFormError(msg) | Set the form-level error. |
reset(nextValues?) | Reset to initial values (or merge partial overrides). |
touchField(name) | Mark a field as touched. |
isFieldDirty(name) | Check if a specific field has changed. |
Validation
Form-Level (Cross-Field)
ts
function validate(values) {
const errors = {};
if (!values.email) errors.email = "Required";
if (values.password !== values.confirmPassword) {
errors.confirmPassword = "Passwords don't match";
}
return errors;
}Using Rule Builders
ts
import { createFormValidator, required, email, minLength, matches } from "@vibe-labs/design-vue-forms";
const validate = createFormValidator({
email: [required(), email()],
password: [required(), minLength(8)],
confirmPassword: [required(), matches(() => form.values.password, "Passwords don't match")],
});Available Rules
| Rule | Description |
|---|---|
required(msg?) | Non-empty after trim. |
minLength(n, msg?) | Minimum character count. |
maxLength(n, msg?) | Maximum character count. |
pattern(regex, msg?) | Must match regex. |
email(msg?) | Basic email format. |
url(msg?) | Valid URL (via new URL()). |
min(n, msg?) | Numeric value ≥ n. |
max(n, msg?) | Numeric value ≤ n. |
matches(getFn, msg?) | Must equal another value. Good for confirm fields. |
custom(fn, trigger?) | Arbitrary validation function. |
Per-Field Rules (on the input)
Rules can also live on the input directly — these work independently of VibeForm:
vue
<VibeInput v-bind="form.field('email')" label="Email" :rules="[required(), email()]" validate-on="blur" />Server Error Handling
ts
async function onSubmit(values, { setErrors, setFieldError, setFormError }) {
const res = await fetch("/api/register", { method: "POST", body: JSON.stringify(values) });
if (!res.ok) {
const body = await res.json();
if (body.errors) {
setErrors(body.errors); // { email: "Already taken", ... }
return;
}
setFormError(body.message ?? "Something went wrong");
}
}Utilities
toFormData(values)
Converts a values object to FormData for multipart submissions. Handles strings, numbers, booleans, File, Blob, and arrays.
ts
import { toFormData } from "@vibe-labs/design-vue-forms";
async function onSubmit(values) {
await fetch("/api/upload", { method: "POST", body: toFormData(values) });
}dirtyValues(values, initial)
Returns only the fields that changed — useful for PATCH-style APIs:
ts
import { dirtyValues } from "@vibe-labs/design-vue-forms";
async function onSubmit(values) {
const changed = dirtyValues(values, initialValues);
await fetch("/api/profile", { method: "PATCH", body: JSON.stringify(changed) });
}Composables
useVibeForm(config)
The core composable — use directly when you need form logic without the <VibeForm> component wrapper:
vue
<script setup lang="ts">
import { useVibeForm } from "@vibe-labs/design-vue-forms";
const form = useVibeForm({
initialValues: { search: "" },
onSubmit: async (values) => {
await performSearch(values.search);
},
});
</script>
<template>
<form @submit.prevent="form.handleSubmit">
<VibeInput v-bind="form.field('search')" placeholder="Search..." type="search" />
</form>
</template>useFormField(options)
Integration hook for input components to auto-register with a parent VibeForm. Fields auto-register and unregister on unmount.
ts
// Inside a custom input component's setup
import { useFormField } from "@vibe-labs/design-vue-forms";
const formCtx = useFormField({
name: props.name,
validate: () => runValidation("submit"),
clearValidation,
});Form-Error Slot
Override the built-in error display:
vue
<VibeForm v-slot="form" :initial-values="initialValues" @submit="onSubmit">
<!-- fields -->
<template #form-error="{ error }">
<MyCustomAlert v-if="error" :message="error" variant="danger" />
</template>
</VibeForm>Or disable entirely:
vue
<VibeForm v-slot="form" :initial-values="initialValues" no-form-error @submit="onSubmit">
<div v-if="form.formError.value">{{ form.formError.value }}</div>
</VibeForm>Package Exports
ts
// Component
export { VibeForm } from "@vibe-labs/design-vue-forms";
// Composables
export { useVibeForm, useFormField } from "@vibe-labs/design-vue-forms";
// Rules
export {
required,
minLength,
maxLength,
pattern,
email,
url,
min,
max,
matches,
custom,
createFormValidator,
} from "@vibe-labs/design-vue-forms";
// Utilities
export { toFormData, dirtyValues } from "@vibe-labs/design-vue-forms";
// Injection key
export { VibeFormKey } from "@vibe-labs/design-vue-forms";
// Types
export type {
FormConfig,
FormReturn,
FormErrors,
FormSubmitHelpers,
FieldBindings,
RegisteredField,
FormContext,
ValidationRule,
} from "@vibe-labs/design-vue-forms";Dependencies
No runtime dependencies — pure Vue composables. Designed to integrate with @vibe-labs/design-vue-inputs but works with any v-model component.
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.