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 id generated in useForm composable | 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 main and aux buttons | { main: NormalizedButton, aux: NormalizedButton } | |
| settings | Normalized settings object | NormalizedSettings | |
| isFormValid | If the value is false the user won't be able to click the main button | boolean | false |
Events
| Name | Description | Type |
|---|---|---|
| @main-click | Event emitted when the user click on the main button | () => void |
| @aux-click | Event emitted when the user click on the aux button | () => 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