Vue Usage
Basic usage
Import Form.vue from the @form-builder alias and pass the schema as a prop:
<script setup lang="ts">
import Form from '@form-builder/components/Form.vue';
import type { FormSchema } from '@form-builder/types/form-builder';
defineProps<{ form: FormSchema }>();
</script>
<template>
<Form :schema="form" />
</template>
The component uses Inertia's useForm() internally and handles submission, error display, and loading state automatically.
Events
@success
Fired after a successful form submission (2xx response from Inertia):
<template>
<Form :schema="form" @success="handleSuccess" />
</template>
<script setup lang="ts">
function handleSuccess() {
// redirect, show toast, etc.
}
</script>
@error
Fired when submission returns validation errors:
<template>
<Form :schema="form" @error="handleError" />
</template>
<script setup lang="ts">
function handleError(errors: Record<string, string>) {
console.log(errors);
}
</script>
Reacting to field changes
Use @onFieldChange to run logic whenever any field value changes. This is useful for cross-field reactivity — for example, showing extra fields when a toggle is enabled, or fetching related data.
<template>
<Form
:schema="form"
@onFieldChange="handleFieldChange"
/>
</template>
<script setup lang="ts">
function handleFieldChange(
field: string,
value: unknown,
form: Record<string, unknown>
) {
if (field === 'country' && value === 'US') {
// do something
}
}
</script>
The callback receives:
field— the name of the field that changedvalue— the new valueform— the entire current form state as a plain object
Custom layouts with FormRenderer
Form.vue wraps everything in a <form> element. If you need a custom layout — for example, a two-column grid — use FormRenderer.vue instead. It renders as a fragment (no wrapper element) so it can be placed inside any grid or flex container.
<script setup lang="ts">
import FormRenderer from '@form-builder/components/FormRenderer.vue';
import type { FormSchema } from '@form-builder/types/form-builder';
defineProps<{ form: FormSchema }>();
</script>
<template>
<form @submit.prevent="submit">
<div class="grid grid-cols-2 gap-4">
<FormRenderer :fields="form.fields" />
</div>
<button type="submit">Save</button>
</form>
</template>
FormRenderer accepts:
fields— the array of field schemas from the form configform(optional) — the current form state object (for conditional field visibility)onFieldChange(optional) — same callback as onForm.vuecolumns(optional) —1(default) to4, see below
Two columns
Pass columns="2" to lay the fields out in a responsive two-column grid (3 and 4 work too, see Columns). Below the md breakpoint the form stays a single column.
<template>
<Form :schema="form" :columns="2" />
</template>
Fields that need the full width get it automatically: wysiwyg, textarea, repeater, keyvalue, hidden and the button types. Any other field can ask for the same with fullWidth in its schema:
<template>
<Form
:schema="form"
:columns="2"
:field-overrides="{ notes: { fullWidth: true } }"
/>
</template>
With the default columns="1" the renderer still emits no wrapper element at all, so existing markup and CSS are untouched.
Horizontal labels
layout="horizontal" puts the label to the left of its control instead of above it, from the sm breakpoint up. Fields without a label stay stacked.
<template>
<Form :schema="form" layout="horizontal" />
</template>
Reusing one schema for many rows
A dialog per table row ("reject booking", "put on option") does not need a schema per row. Render the form once in PHP and override the target and the values per use:
<template>
<Dialog v-model:open="open">
<Form
:schema="rejectForm"
:action="`/bookings/${booking.id}/reject`"
method="patch"
:defaults="{ reason: booking.reason ?? '' }"
@success="open = false"
/>
</Dialog>
</template>
actionandmethodreplace the schema's own for this form only.defaultsare merged over the schema defaults. When they change (another row opens the same dialog), the form loads them, resets the rest to the schema defaults and clears its errors. Values typed by the user stay as long as the defaults stay the same.fieldOverrideskeeps working for per-row props on a field, such as options.
Resetting after a successful submit
A form that adds lines ("add booking line", "add guest") should be empty again after each submit. Inertia keeps the page state after a POST, so the typed values would otherwise stay. Turn it on in PHP or per use:
$form->resetOnSuccess();
<Form :schema="lineForm" reset-on-success />
After a successful submit the form goes back to its defaults and clears its errors. A failed submit keeps what was
typed. The prop wins over the schema, so :reset-on-success="false" keeps the values for one use.
Independent of this option, file fields always forget the chosen file after a successful submit (and their remove checkbox unticks): the upload is done, and submitting again should not upload it twice.
Errors that belong to no field
An error whose key matches no field — conflicts, form, a failed HTTP request — has nowhere to go under a field, so
it is shown at the top of the form in a role="alert" block:
throw ValidationException::withMessages([
'conflicts' => 'This booking overlaps with another one.',
]);
Keys of a field's nested values (lines.0.amount for a repeater lines, title.nl for a translatable title) and of
remove flags count as that field's own. Replace the block with the form-errors slot:
<Form :schema="form">
<template #form-errors="{ errors }">
<Alert variant="destructive">
<AlertDescription v-for="(message, key) in errors" :key="key">{{ message }}</AlertDescription>
</Alert>
</template>
</Form>
The same formErrors (key to first message) is exposed on the component ref.
Translations
Every text the renderer shows itself (date picker buttons, placeholders, "No results", aria labels, …) comes from a
message catalogue. English is the default and Dutch is built in. Labels the schema carries, such as ->placeholder()
or the translated labels PHP sends for selects and numbers, still win.
The locale is the first of:
- the
localeprop on<Form> - the locale set with
configureFormMessages({ locale }) - the
localein the schema, which the PHP core fills withapp()->getLocale()(override with$form->locale('nl')) en
A regional locale such as nl-BE falls back to nl; a missing message falls back to English.
// app.ts
import { configureFormMessages } from '@form-builder';
configureFormMessages({
locale: 'nl',
messages: {
de: { pickDate: 'Datum wählen', today: 'Heute' },
nl: { pickDate: 'Kies een dag' },
},
});
Override single messages for one form with the messages prop:
<Form :schema="form" :messages="{ pickDate: 'Arrival day' }" />
The standalone components (TreeView, SortableList, Tabs) follow the global locale. The full list of keys is the
FormMessages type; englishMessages and dutchMessages are exported as starting points for a new language.
Submitting without Inertia
By default the form submits through Inertia's useForm(). Pass transport="http" to submit over fetch instead and stay on the page — useful in a modal, a side panel or an admin screen that keeps its own state.
<template>
<Form :schema="form" transport="http" @success="close" />
</template>
The built-in submitter:
- sends the
XSRF-TOKENcookie as anX-XSRF-TOKENheader, falling back to a<meta name="csrf-token">tag, so Laravel'sVerifyCsrfTokendoes not answer419 - sends
X-Requested-With: XMLHttpRequestandAccept: application/json, so validation failures come back as422JSON instead of a redirect - switches to
multipart/form-dataas soon as a field holds a file, and spoofs_methodforPUT,PATCHandDELETE(the Inertia transport does the same: aPUTwith a file goes out as aPOSTwith_method) - spreads nested arrays and objects into
filters[status][0]=openstyle parameters on aGET
Validation errors land on the matching field. Any other failure lands under the key form, which no field renders — read it yourself if you want to show it:
<template>
<p v-if="formRef?.form.errors.form">{{ formRef.form.errors.form }}</p>
<Form ref="formRef" :schema="form" transport="http" />
</template>
Your own transport
Pass a submitter to route the request through your own client, and an errorAdapter when your API does not answer in Laravel's { errors: { field: [...] } } shape.
<script setup lang="ts">
import type { HttpFormRequest } from '@form-builder';
async function submitter({ method, url, data }: HttpFormRequest) {
return api.request({ method, url, data });
}
function errorAdapter(error: any) {
return { name: error.response.data.detail };
}
</script>
<template>
<Form
:schema="form"
transport="http"
:submitter="submitter"
:error-adapter="errorAdapter"
/>
</template>
The form exposes its own API under the names
data,errors,processing,recentlySuccessful,reset,clearErrors,submit,get,post,put,patchanddelete. A field with one of those names throws on setup rather than silently overwriting the method.
