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 ​