Skip to content

Slot System ​

AutoVorm supports a powerful slot API that gives you complete control over field rendering while maintaining all the benefits of automatic validation and state management.

Slot Types ​

Field Slots ​

Replace the entire field rendering:

vue
<AutoVorm>
  <template #email="{ field, state }">
    <div class="custom-email">
      <label>{{ field.label }}</label>
      <input v-model="vorm.formData.email" type="email" />
      <span v-if="state.error">{{ state.error }}</span>
    </div>
  </template>
</AutoVorm>

Wrapper Slots ​

Wrap the default input while keeping automatic rendering:

  • wrapper:fieldName — specific field wrapper
  • wrapper:[fieldA,fieldB] — multi-field wrapper
  • wrapper — global fallback
vue
<AutoVorm>
  <template #wrapper:email="{ field, state, content }">
    <div class="my-wrapper">
      <label>{{ field.label }}</label>
      <component :is="content" />
      <span v-if="state.error">{{ state.error }}</span>
    </div>
  </template>
</AutoVorm>

Before/After Slots ​

Add content before or after fields:

vue
<AutoVorm>
  <template #before-password>
    <div class="hint">Password must be at least 8 characters</div>
  </template>
  <template #after-password>
    <div class="strength">Strength: Strong</div>
  </template>
</AutoVorm>

Slot Props ​

Field Slot Props ​

ts
{
  field: ResolvedVormFieldSchema,  // Schema with resolved strings
  state: FieldState,               // Validation state
  path: string,                    // Full path (e.g., "contacts[0].email")
  indexes: number[]                // Repeater indexes
}

Wrapper Slot Props ​

Wrapper slots receive all field props plus bindings for third-party components:

ts
{
  // Field slot props
  field: ResolvedVormFieldSchema,
  state: FieldState,
  path: string,
  indexes: number[],
  content: () => VNode,            // Renders default input

  // Bindings for custom/third-party components
  modelValue: any,                 // Current value
  'onUpdate:modelValue': (v) => void,  // Update handler
  items: Option[],                 // Options (Vuetify naming)
  options: Option[],               // Options (generic naming)
  error: string | undefined,       // Error message
  errorMessages: string[]          // Errors as array (Vuetify)
}

FieldState Interface ​

ts
interface FieldState {
  error: string | null;
  valid: boolean;
  invalid: boolean;
  validationMode: 'onInput' | 'onBlur' | 'onSubmit';
  touched: boolean;
  dirty: boolean;
  initialValue: any;
  classes: string;
}

ResolvedVormFieldSchema ​

All reactive strings are pre-resolved to plain strings:

ts
interface ResolvedVormFieldSchema {
  name: string;
  type: string;
  label: string;           // Already resolved!
  placeholder: string;     // Already resolved!
  helpText: string;        // Already resolved!
  disabled: boolean;       // Already resolved!
  required: boolean;
  showError: boolean;
  classes: { /* ... */ };
  // ... other properties
}

Slot Resolution Priority ​

AutoVorm resolves slots in this order:

  1. wrapper:fieldName — exact wrapper match
  2. wrapper:[fieldA,fieldB] — multi-field wrapper match
  3. Inherited wrapper via inheritWrapper: true
  4. wrapper — global fallback
  5. Direct field slot (#fieldName)
  6. Default rendering

Third-Party Integration ​

Vuetify ​

vue
<AutoVorm>
  <template #wrapper:country="slotProps">
    <v-select
      v-bind="slotProps"
      :label="slotProps.field.label"
      variant="outlined"
    />
  </template>
</AutoVorm>

PrimeVue ​

vue
<AutoVorm>
  <template #wrapper:city="{ modelValue, options, 'onUpdate:modelValue': update, error }">
    <Dropdown
      :model-value="modelValue"
      :options="options"
      option-label="label"
      option-value="value"
      @update:model-value="update"
      :class="{ 'p-invalid': error }"
    />
    <small v-if="error" class="p-error">{{ error }}</small>
  </template>
</AutoVorm>

Headless UI ​

vue
<AutoVorm>
  <template #wrapper:status="{ modelValue, options, 'onUpdate:modelValue': update, field }">
    <Listbox :model-value="modelValue" @update:model-value="update">
      <ListboxLabel>{{ field.label }}</ListboxLabel>
      <ListboxButton>{{ modelValue || 'Select...' }}</ListboxButton>
      <ListboxOptions>
        <ListboxOption v-for="opt in options" :key="opt.value" :value="opt.value">
          {{ opt.label }}
        </ListboxOption>
      </ListboxOptions>
    </Listbox>
  </template>
</AutoVorm>

Wrapper Inheritance ​

Enable inheritWrapper: true on a field to let nested fields use parent wrappers:

ts
const schema: VormSchema = [
  {
    name: 'contacts',
    type: 'repeater',
    fields: [
      { name: 'email', type: 'email', inheritWrapper: true }
    ]
  }
];

Now wrapper:email will also apply to contacts[0].email, contacts[1].email, etc.

Multi-Field Wrappers ​

Apply the same wrapper to multiple fields:

vue
<AutoVorm>
  <template #wrapper:[email,phone,address]="{ field, state, content }">
    <div class="contact-field">
      <label>{{ field.label }}</label>
      <component :is="content" />
      <span v-if="state.error">{{ state.error }}</span>
    </div>
  </template>
</AutoVorm>

Visibility Control ​

AutoVorm determines visible fields based on:

  • only — explicit field inclusion
  • excludeRepeaters — skip repeaters and children
  • showIf — conditional visibility in schema

Complete Example ​

vue
<script setup lang="ts">
import { useVorm, type VormSchema } from 'vorm-vue';
import { VormProvider, AutoVorm } from 'vorm-vue';

const schema: VormSchema = [
  { name: 'name', type: 'text', label: 'Name' },
  { name: 'email', type: 'email', label: 'Email' },
  {
    name: 'country',
    type: 'select',
    label: 'Country',
    options: [
      { label: 'Germany', value: 'DE' },
      { label: 'France', value: 'FR' },
    ]
  },
];

const vorm = useVorm(schema);
</script>

<template>
  <VormProvider :vorm="vorm">
    <AutoVorm>
      <!-- Custom wrapper for all fields -->
      <template #wrapper="{ field, state, content }">
        <div class="field-group" :class="{ 'has-error': state.error }">
          <label class="label">{{ field.label }}</label>
          <component :is="content" class="input" />
          <p v-if="state.error" class="error">{{ state.error }}</p>
          <p v-if="field.helpText" class="help">{{ field.helpText }}</p>
        </div>
      </template>

      <!-- Custom select rendering -->
      <template #wrapper:country="{ field, modelValue, options, 'onUpdate:modelValue': update }">
        <div class="field-group">
          <label>{{ field.label }}</label>
          <select :value="modelValue" @change="update($event.target.value)">
            <option value="">Select...</option>
            <option v-for="opt in options" :key="opt.value" :value="opt.value">
              {{ opt.label }}
            </option>
          </select>
        </div>
      </template>
    </AutoVorm>
  </VormProvider>
</template>


The slot system gives you full control over layout, structure, and styling — without losing automatic validation or schema-driven logic.

MIT Licensed