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 thefields
object. - These three dynamic fields has access to the
field
through thev-bind
property.
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
@success
and@error
handlers - Validate Before Submit: Use
isMainButtonDisabled
to 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