Guide

Renderers and composables

Render schemas with application-owned components or consume the form engine composables directly.

The Vue package is headless. Form creates the form engine, provides it to descendants, and returns only its slot content. Renderer factories, field iterators, fieldsets, collections, wizards, uploads, errors, and submission helpers likewise return consumer slot nodes or null—never package-owned markup.

Register application renderers

Create one renderer registry for the components your application owns.

resources/js/components/form/index.ts
import {
  createFormRenderer,
  type FormFieldRenderer,
} from '@inertify/form-vue'
import CheckboxField from './CheckboxField.vue'
import TextField from './TextField.vue'
import UnsupportedField from './UnsupportedField.vue'

const renderers = {
  Text: TextField,
  Textarea: {
    component: TextField,
    props: { multiline: true },
  },
  Checkbox: CheckboxField,
  Submit: null,
} satisfies Record<string, FormFieldRenderer>

export const { FormField, FormFields } = createFormRenderer({
  name: 'AppForm',
  renderers,
  fallback: UnsupportedField,
})

Registry keys are the exact serialized component discriminators, not PHP class names. The TextInput, OtpInput, and Hidden PHP fields serialize as Text, Otp, and Hidden. A component receives the complete field slot payload, { component, props } adds preset props, and null marks a type as intentionally renderless. Unknown types render null unless you configure fallback or provide an unsupported slot. Each factory call snapshots an isolated registry, including during SSR.

Use the generated components inside Form:

resources/js/Pages/Profiles/Edit.vue
<script setup lang="ts">
import { Form, type FormResource } from '@inertify/form-vue'
import { FormFields } from '@/components/form'

defineProps<{ form: FormResource }>()
</script>

<template>
  <Form :form="form" v-slot="{ form: formApi, processing }">
    <FormFields :form="formApi" />
    <button type="submit" :disabled="processing">Save</button>
  </Form>
</template>

FormFields follows schema order. Use FormField for explicit placement:

<div class="grid gap-6 md:grid-cols-2">
  <FormField name="name" />
  <FormField name="email" />
</div>
<FormField name="bio" />

A registered collection renderer owns its repeater or block descendants, so the top-level traversal does not emit those controls twice.

If a renderer declares only part of the slot payload, use defineOptions({ inheritAttrs: false }) so controller objects and callbacks do not fall through to its root DOM element.

Select fieldset sections

Use FormFieldsets when a page should iterate selected server-declared groups while owning each section or card. Selectors match fieldset IDs exactly, so assign stable, unique IDs in PHP:

use Inertify\Form\Fields\Fieldset;
use Inertify\Form\Fields\TextInput;

Fieldset::make('Identity')
    ->id('identity')
    ->fields([
        TextInput::make('name')->required(),
        TextInput::make('email')->email()->required(),
    ]);

Both selectors accept a string or readonly string array. only is applied first and except removes from that result; neither selector changes schema order or visibility.

<script setup lang="ts">
import { FormFieldsets } from '@inertify/form-vue'
import { FormFields } from '@/components/form'

const cardFieldsets = ['identity', 'preferences', 'internal'] as const
</script>

<template>
  <FormFieldsets
    :only="cardFieldsets"
    except="internal"
    v-slot="{ fieldset }"
  >
    <section class="card">
      <h2>{{ fieldset.legend }}</h2>
      <p v-if="fieldset.description">{{ fieldset.description }}</p>
      <FormFields :fieldset="fieldset" />
    </section>
  </FormFieldsets>
</template>

An ID present in both selectors is excluded. Hidden fieldsets and fields remain filtered unless include-hidden is set independently. For a fixed page layout, write each application-owned card directly and render its group with <FormFields fieldset="identity" /> instead.

Render with slots

Calling createFormRenderer() without options creates slot-only FormField and FormFields components. Field replacement slots resolve in this order:

  1. field-{path}
  2. type-{component}, normalized to kebab case
  3. default
  4. Deprecated {name}-field compatibility fallback

path is the fully qualified data path, including collection indexes. Use before-{path}-field and after-{path}-field for insertion. Fieldset components expose matching fieldset slots.

<script setup lang="ts">
import { createFormRenderer } from '@inertify/form-vue'

const { FormFields } = createFormRenderer()
</script>

<template>
  <FormFields>
    <template
      #type-text="{
        field,
        name,
        value,
        error,
        disabled,
        readonly,
        setValue,
        blur,
        registerElement,
      }"
    >
      <label :for="`field-${name}`">{{ field.label }}</label>
      <input
        :id="`field-${name}`"
        :ref="registerElement"
        :name="name"
        :value="value"
        :disabled="disabled"
        :readonly="readonly"
        @input="setValue(($event.target as HTMLInputElement).value)"
        @blur="blur"
      >
      <p v-if="error" role="alert">{{ error }}</p>
    </template>
  </FormFields>
</template>

The slot also receives controller, errors, visible, touched, dirty, required, and validation helpers. Registering the application-owned input element enables scrollToFirstError without requiring package markup.

Work composables-first

Import the engine and focused APIs from the package root or the public composable entry point:

import {
  useForm,
  useFormContext,
  useFormField,
  useFormFields,
  useFormValidation,
  useFormVisibility,
  useFormSubmission,
  useFormWizard,
  useFormCollection,
  useFormCollections,
  useFormCombobox,
  useFormUploads,
} from '@inertify/form-vue'

Specialized field controllers include useTextInput(), useSlug(), useChoices(), useDate(), useFile(), useComposer(), useRichText(), useOtp(), and useLink(). They adapt the shared field controller without choosing an editor or component library.

Only ., ./components, ./composables, and ./package.json are public package exports. Avoid importing package internals.

Copyright © 2026 Inertify · Released under the MIT License