Form Components 
FancyCRUD provides a modular component architecture that gives you complete control over form layout and customization. The form functionality is split into four distinct components, each serving a specific purpose, allowing you to build forms with any layout or structure you need.
Overview 
FancyCRUD forms are built from four main components:
- <f-form>: The main container that orchestrates all form components
- <f-form-header>: Displays the form title with dynamic mode-based text
- <f-form-body>: Renders all form fields with powerful slot customization
- <f-form-footer>: Contains form action buttons (submit/cancel)
Each component can be used independently or combined to create custom form layouts.
Import Components 
All form components can be imported from @fancy-crud/vue:
<script lang="ts" setup>
import { FForm, FFormHeader, FFormBody, FFormFooter } from '@fancy-crud/vue'
</script>Quick Start 
The simplest way to use FancyCRUD forms is with the <f-form> component:
<template>
  <f-form v-bind="form" />
</template>
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
  fields: {
    name: {
      type: FieldType.text,
      label: 'Name'
    },
    email: {
      type: FieldType.text,
      label: 'Email'
    }
  },
  settings: {
    url: 'users/',
    title: '{{ Create User | Edit User }}'
  }
})
</script>This automatically renders a complete form with header, fields, and footer buttons.
Custom Layouts 
For more control, you can use individual components to create custom layouts:
<template>
  <div class="custom-form-container">
    <f-form-header :title="form.settings.title" :mode="form.settings.mode" />
    
    <div class="two-column-layout">
      <f-form-body
        :form-id="form.id"
        :fields="form.fields"
        :settings="form.settings"
      />
    </div>
    
    <f-form-footer
      :buttons="form.buttons"
      :settings="form.settings"
      :is-form-valid="isFormValid"
      @main-click="handleSubmit"
      @aux-click="handleCancel"
    />
  </div>
</template>
<script lang="ts" setup>
import { useForm, FFormHeader, FFormBody, FFormFooter, FieldType } from '@fancy-crud/vue'
const form = useForm({
  fields: {
    name: { type: FieldType.text, label: 'Name' },
    email: { type: FieldType.text, label: 'Email' }
  },
  settings: {
    url: 'users/'
  }
})
const isFormValid = computed(() => {
  return Object.values(form.fields).every(field => !field.errors.length)
})
const handleSubmit = () => {
  form.submit()
}
const handleCancel = () => {
  form.reset()
}
</script>
<style scoped>
.custom-form-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}
.two-column-layout {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}
</style>Component Architecture 
Each component is designed to be:
- Modular: Use individually or together
- Flexible: Extensive slot support for customization
- Type-Safe: Full TypeScript support
- Framework-Agnostic: Works with any UI framework wrapper
TIP
Use <f-form> for quick standard forms, or individual components when you need custom layouts or advanced styling.
Form Header 
The f-form-header component focuses on the form header, allowing you to customize and present information related to the form. It provides the flexibility to dynamically display the form mode title.
Props 
| Name | Description | Type | Default | 
|---|---|---|---|
| title | Title value to display in the form header | string|undefined | |
| mode | Current form mode | FormMode | 
Slots 
| Name | Description | Scope | 
|---|---|---|
| default | Default slot to display form title | { formModeTitle: string } | 
Form Body 
The f-form-body component is designed to handle the rendering of form fields. It efficiently manages the display of fields, providing a clean structure for your form's body.
Props 
| Name | Description | Type | Default | 
|---|---|---|---|
| formId | The idgenerated inuseFormcomposable | symbol | |
| fields | Object with normalized fields. | ObjectWithNormalizedFields|Record<string, NormalizedField>|NormalizedFields<T> | |
| settings | Form settings | NormalizedSettings | 
Slots 
- You have three dynamic slots for each field in the form.
- Where the [fieldKey]is the key name in thefieldsobject.
- These three dynamic fields has access to the fieldthrough thev-bindproperty.
| Name | Description | Scope | 
|---|---|---|
| before-field-[fieldKey] | Adds elements before a field. | { field: NormalizedField } | 
| field-[fieldKey] | Overrides the auto-rendered field. | { field: NormalizedField } | 
| after-field-[fieldKey] | Adds elements after a field. | { field: NormalizedField } | 
Let's see an example where the [fieldKey] is email.
<template>
  <f-form v-bind="form">
    <template #before-field-email="{ field }">
      <p class="col-span-12">Your email is: {{ field.modelValue }}</p>
    </template>
    <template #field-email>
      <input
        v-model="form.fields.email.modelValue"
        placeholder="Type your email"
        class="col-span-12"
      />
    </template>
    <template #after-field-email>
      <p class="col-span-12">
        <input type="checkbox">
        <span class="pl-1">Do you want to receive ads?</span>
      </p>
    </template>
  </f-form>
</template>
<script lang="ts" setup>
import { FieldType, useForm } from '@fancy-crud/vue';
const form = useForm({
  fields: {
    email: {
      type: FieldType.text,
      label: 'Email'
    },
  },
})
</script>
<style scoped>
.col-span-12 {
  grid-column: span 12 / span 12;
}
.pl-1 {
  padding-left: 0.5rem;
}
input {
  border: 1px solid #4f4f4f;
  border-radius: 5px;
  padding: 1rem;
}
</style>Form Footer 
Handling the form's footer, f-form-footer, this component includes buttons and functionality for main and auxiliary actions. It enables easy customization of buttons and responsiveness to user interactions.
Props 
| Name | Description | Type | Default | 
|---|---|---|---|
| buttons | Object with normalized mainandauxbuttons | { main: NormalizedButton, aux: NormalizedButton } | |
| settings | Normalized settings object | NormalizedSettings | |
| isFormValid | If the value is falsethe user won't be able to click themainbutton | boolean | false | 
Events 
| Name | Description | Type | 
|---|---|---|
| @main-click | Event emitted when the user click on the mainbutton | () => void | 
| @aux-click | Event emitted when the user click on the auxbutton | () => void | 
Slots 
| Name | Description | Scope | 
|---|---|---|
| default | Default slot to render buttons | { mainButton: NormalizedButton; auxButton: NormalizedButton; getLabel: string; onMainClick: Function; onAxuClick: Function; isMainButtonDisabled: boolean } | 
Form Container (<f-form>) 
The <f-form> component is the main form container that orchestrates all form functionality. It automatically integrates the header, body, and footer components, providing a complete form solution with minimal setup.
When to Use 
Use <f-form> when you want:
- Quick setup: Default form layout with header, fields, and buttons
- Standard forms: Common create/edit forms with typical layouts
- Minimal configuration: Let FancyCRUD handle the structure
For custom layouts or advanced styling, use individual components (<f-form-header>, <f-form-body>, <f-form-footer>).
Props 
| Name | Description | Type | Required | Default | 
|---|---|---|---|---|
| id | Form ID as symbol value | symbol | Yes | - | 
| fields | Normalized fields to render in the form | NormalizedFields | Yes | - | 
| settings | Normalized settings to manage form behavior | NormalizedSettings | Yes | - | 
| buttons | Normalized buttons to display in the form footer | NormalizedButtons | Yes | - | 
Props Shortcut
Instead of passing each prop individually, use v-bind="form" to pass all props at once:
<f-form v-bind="form" />Basic Usage 
<template>
  <div class="card">
    <!-- Short syntax (recommended) -->
    <f-form v-bind="form" />
    
    <!-- Expanded syntax -->
    <!-- <f-form
      :id="form.id"
      :fields="form.fields"
      :settings="form.settings"
      :buttons="form.buttons"
    /> -->
  </div>
</template>
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
  fields: {
    name: {
      type: FieldType.text,
      label: 'Name',
      required: true
    },
    email: {
      type: FieldType.text,
      label: 'Email',
      required: true
    },
    bio: {
      type: FieldType.textarea,
      label: 'Bio'
    }
  },
  settings: {
    url: 'users/',
    title: '{{ Create User | Edit User }}'
  }
})
</script>Events 
The <f-form> component emits events for form lifecycle hooks:
| Name | Description | Payload Type | When Emitted | 
|---|---|---|---|
| @success | Emitted when form submission succeeds | (response: any) => void | After successful API call | 
| @error | Emitted when form submission fails | (error?: any) => void | After failed API call | 
Handling Events 
<template>
  <f-form 
    v-bind="form"
    @success="onSuccess"
    @error="onError"
  />
</template>
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
const router = useRouter()
const toast = useToast()
const form = useForm({
  fields: {
    name: { type: FieldType.text, label: 'Name' }
  },
  settings: {
    url: 'users/'
  }
})
const onSuccess = (response: any) => {
  toast.success('User saved successfully!')
  router.push(`/users/${response.data.id}`)
}
const onError = (error: any) => {
  console.error('Form submission failed:', error)
  toast.error('Failed to save user')
}
</script>Slots 
The <f-form> component provides access to all slots from its child components:
| Slot Name | Description | Scope Properties | From Component | 
|---|---|---|---|
| form-header | Customize the entire header section | { formModeTitle: string } | <f-form-header> | 
| before-field-[fieldKey] | Add content before a specific field | { field: NormalizedField } | <f-form-body> | 
| field-[fieldKey] | Override the default field rendering | { field: NormalizedField } | <f-form-body> | 
| after-field-[fieldKey] | Add content after a specific field | { field: NormalizedField } | <f-form-body> | 
| form-footer | Customize the entire footer section | { mainButton, auxButton, ... } | <f-form-footer> | 
Customizing the Header 
<template>
  <f-form v-bind="form">
    <template #form-header="{ formModeTitle }">
      <div class="custom-header">
        <h1 class="text-3xl font-bold">{{ formModeTitle }}</h1>
        <p class="text-gray-500">Fill out the form below</p>
      </div>
    </template>
  </f-form>
</template>Customizing Fields 
<template>
  <f-form v-bind="form">
    <!-- Add content before a field -->
    <template #before-field-email="{ field }">
      <div class="info-box">
        <span>ℹ️ We'll never share your email</span>
      </div>
    </template>
    
    <!-- Completely replace a field -->
    <template #field-username="{ field }">
      <div class="custom-field">
        <label>{{ field.label }}</label>
        <input
          v-model="field.modelValue"
          :placeholder="field.placeholder"
          class="custom-input"
        />
        <button @click="checkAvailability">Check Availability</button>
      </div>
    </template>
    
    <!-- Add content after a field -->
    <template #after-field-password>
      <div class="password-strength">
        <span>Password strength: Strong</span>
      </div>
    </template>
  </f-form>
</template>
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
  fields: {
    username: { type: FieldType.text, label: 'Username' },
    email: { type: FieldType.text, label: 'Email' },
    password: { type: FieldType.password, label: 'Password' }
  },
  settings: {
    url: 'users/'
  }
})
const checkAvailability = () => {
  console.log('Checking username availability...')
}
</script>Customizing the Footer 
<template>
  <f-form v-bind="form">
    <template #form-footer="{ mainButton, auxButton, onMainClick, onAuxClick }">
      <div class="custom-footer">
        <button @click="onAuxClick" class="secondary-btn">
          {{ auxButton.label }}
        </button>
        
        <div class="actions-right">
          <button @click="saveAsDraft" class="draft-btn">
            Save as Draft
          </button>
          <button
            @click="onMainClick"
            :disabled="mainButton.disabled"
            class="primary-btn"
          >
            {{ mainButton.label }}
          </button>
        </div>
      </div>
    </template>
  </f-form>
</template>
<script lang="ts" setup>
const saveAsDraft = () => {
  console.log('Saving as draft...')
}
</script>
<style scoped>
.custom-footer {
  display: flex;
  justify-content: space-between;
  padding: 1rem;
  border-top: 1px solid #e5e5e5;
}
.actions-right {
  display: flex;
  gap: 0.5rem;
}
</style>Complete Example 
Here's a comprehensive example showing multiple customizations:
<template>
  <div class="page-container">
    <f-form
      v-bind="form"
      @success="handleSuccess"
      @error="handleError"
    >
      <!-- Custom Header -->
      <template #form-header="{ formModeTitle }">
        <div class="custom-header">
          <h1>{{ formModeTitle }}</h1>
          <p class="subtitle">Please provide accurate information</p>
        </div>
      </template>
      
      <!-- Add help text before email field -->
      <template #before-field-email>
        <div class="help-text">
          💡 Use your work email for business accounts
        </div>
      </template>
      
      <!-- Custom phone field with formatting -->
      <template #field-phone="{ field }">
        <div class="custom-field">
          <label>{{ field.label }}</label>
          <input
            v-model="field.modelValue"
            @input="formatPhone"
            placeholder="(555) 123-4567"
            class="phone-input"
          />
        </div>
      </template>
      
      <!-- Add terms acceptance after last field -->
      <template #after-field-bio>
        <div class="terms">
          <label>
            <input type="checkbox" v-model="acceptTerms" />
            <span>I accept the terms and conditions</span>
          </label>
        </div>
      </template>
      
      <!-- Custom Footer with additional actions -->
      <template #form-footer="{ mainButton, auxButton, onMainClick, onAuxClick, isMainButtonDisabled }">
        <div class="footer-actions">
          <button @click="onAuxClick" class="btn-cancel">
            {{ auxButton.label }}
          </button>
          
          <div class="right-actions">
            <button @click="resetForm" class="btn-reset">
              Reset Form
            </button>
            <button
              @click="onMainClick"
              :disabled="isMainButtonDisabled || !acceptTerms"
              class="btn-submit"
            >
              {{ mainButton.label }}
            </button>
          </div>
        </div>
      </template>
    </f-form>
  </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useForm, FieldType, FORM_MODE } from '@fancy-crud/vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
const router = useRouter()
const toast = useToast()
const acceptTerms = ref(false)
const form = useForm({
  fields: {
    name: {
      type: FieldType.text,
      label: 'Full Name',
      required: true
    },
    email: {
      type: FieldType.text,
      label: 'Email',
      required: true
    },
    phone: {
      type: FieldType.text,
      label: 'Phone Number'
    },
    bio: {
      type: FieldType.textarea,
      label: 'Bio'
    }
  },
  settings: {
    url: 'users/',
    mode: FORM_MODE.create,
    title: '{{ Create Account | Update Profile }}'
  }
})
const formatPhone = (event: Event) => {
  const input = event.target as HTMLInputElement
  const value = input.value.replace(/\D/g, '')
  const formatted = value.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3')
  form.fields.phone.modelValue = formatted
}
const resetForm = () => {
  form.reset()
  acceptTerms.value = false
}
const handleSuccess = (response: any) => {
  toast.success('Account created successfully!')
  router.push('/dashboard')
}
const handleError = (error: any) => {
  toast.error('Failed to create account. Please try again.')
}
</script>
<style scoped>
.page-container {
  max-width: 600px;
  margin: 2rem auto;
  padding: 2rem;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.custom-header {
  text-align: center;
  margin-bottom: 2rem;
}
.custom-header h1 {
  font-size: 2rem;
  font-weight: bold;
  margin-bottom: 0.5rem;
}
.subtitle {
  color: #666;
}
.help-text {
  padding: 0.75rem;
  background: #e3f2fd;
  border-left: 4px solid #2196f3;
  margin-bottom: 1rem;
}
.custom-field {
  margin-bottom: 1rem;
}
.custom-field label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}
.phone-input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}
.terms {
  padding: 1rem;
  background: #f5f5f5;
  border-radius: 4px;
  margin-top: 1rem;
}
.footer-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 1.5rem;
  border-top: 1px solid #e5e5e5;
}
.right-actions {
  display: flex;
  gap: 0.5rem;
}
.btn-cancel,
.btn-reset {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  background: white;
  border-radius: 4px;
  cursor: pointer;
}
.btn-submit {
  padding: 0.5rem 1.5rem;
  background: #2196f3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.btn-submit:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>Best Practices 
Component Usage Tips
- Use v-bind="form": Simplifies prop passing and keeps code clean
- Leverage Slots: Customize only what you need; let FancyCRUD handle the rest
- Handle Events: Always implement @successand@errorhandlers
- Validate Before Submit: Use isMainButtonDisabledto prevent invalid submissions
- Keep It Simple: Use <f-form>for standard layouts; use individual components only when needed
Common Pitfalls
- Don't override all slots: You'll lose FancyCRUD's built-in functionality
- Remember field keys: Slot names must match your field keys exactly
- Handle errors gracefully: Always provide error feedback to users
Next Steps 
- Learn about Form Header Component for header customization
- Explore Form Body Component for field layout control
- Check out Form Footer Component for button customization