Skip to content

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:

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:

vue
<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:

vue
<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

NameDescriptionTypeDefault
titleTitle value to display in the form headerstring | undefined
modeCurrent form modeFormMode

Slots

NameDescriptionScope
defaultDefault 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

NameDescriptionTypeDefault
formIdThe id generated in useForm composablesymbol
fieldsObject with normalized fields.ObjectWithNormalizedFields | Record<string, NormalizedField> | NormalizedFields<T>
settingsForm settingsNormalizedSettings

Slots

  • You have three dynamic slots for each field in the form.
  • Where the [fieldKey] is the key name in the fields object.
  • These three dynamic fields has access to the field through the v-bind property.
NameDescriptionScope
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.

vue
<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>

Create new record

Your email is:

Do you want to receive ads?

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

NameDescriptionTypeDefault
buttonsObject with normalized main and aux buttons{ main: NormalizedButton, aux: NormalizedButton }
settingsNormalized settings objectNormalizedSettings
isFormValidIf the value is false the user won't be able to click the main buttonbooleanfalse

Events

NameDescriptionType
@main-clickEvent emitted when the user click on the main button() => void
@aux-clickEvent emitted when the user click on the aux button() => void

Slots

NameDescriptionScope
defaultDefault 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

NameDescriptionTypeRequiredDefault
idForm ID as symbol valuesymbolYes-
fieldsNormalized fields to render in the formNormalizedFieldsYes-
settingsNormalized settings to manage form behaviorNormalizedSettingsYes-
buttonsNormalized buttons to display in the form footerNormalizedButtonsYes-

Props Shortcut

Instead of passing each prop individually, use v-bind="form" to pass all props at once:

vue
<f-form v-bind="form" />

Basic Usage

vue
<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:

NameDescriptionPayload TypeWhen Emitted
@successEmitted when form submission succeeds(response: any) => voidAfter successful API call
@errorEmitted when form submission fails(error?: any) => voidAfter failed API call

Handling Events

vue
<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 NameDescriptionScope PropertiesFrom Component
form-headerCustomize 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-footerCustomize the entire footer section{ mainButton, auxButton, ... }<f-form-footer>

Customizing the Header

vue
<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

vue
<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>
vue
<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:

vue
<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

  1. Use v-bind="form": Simplifies prop passing and keeps code clean
  2. Leverage Slots: Customize only what you need; let FancyCRUD handle the rest
  3. Handle Events: Always implement @success and @error handlers
  4. Validate Before Submit: Use isMainButtonDisabled to prevent invalid submissions
  5. 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