Fields
Fields are the building blocks of forms in FancyCRUD. When you define fields, FancyCRUD automatically:
✅ Creates the HTML structure
✅ Manages form data and state
✅ Handles validation
✅ Sends HTTP requests
✅ Processes responses
✅ Displays notifications
✅ Shows field-specific errors
✅ Supports reactive updates
How Fields Work
Fields are defined as plain objects and passed to the useForm
composable. FancyCRUD normalizes these raw field definitions, adding all necessary properties and methods to make them fully functional.
As long as you're using one of the existing wrappers (Vuetify, Element Plus, Quasar, Oruga UI), you can use all props and slots from the underlying UI framework.
Basic Example
<template>
<div class="card">
<!-- Component to render the form -->
<f-form v-bind="form" />
</div>
</template>
<script lang="ts" setup>
import { FieldType, useForm } from '@fancy-crud/vue'
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First name',
placeholder: 'John',
},
lastName: {
type: FieldType.text,
label: 'Last name',
placeholder: 'Doe',
},
},
settings: {
url: 'endpoint/'
},
})
</script>
In this example, we only defined three attributes (type
, label
, and placeholder
) for each field. However, the useForm
composable automatically normalizes these fields and adds all necessary properties.
Field Normalization
After normalization, each field will have a complete structure like this:
{
fields: {
firstName: {
type: "text",
label: "First name",
id: `field-firstName-control`,
modelKey: "firstName",
name: "firstName",
errors: [],
wasFocused: false,
modelValue: null,
class: '',
wrapper: {
class: '',
},
ref: null,
}
}
}
Accessing Normalized Fields
You can override any of these default values by providing your own values. All field properties are accessible and reactive:
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First name',
modelValue: 'John Doe'
}
}
})
console.log(form.fields.firstName.modelValue) // John Doe
console.log(form.fields.firstName.label) // First name
console.log(form.fields.firstName.id) // field-firstName-control
console.log(form.fields.firstName.errors) // []
</script>
Field Types
FancyCRUD provides a comprehensive set of field types to cover most form use cases. Each field type is optimized for specific data input scenarios.
Available Field Types
Field Type | Use Case | Example Data |
---|---|---|
text | General text input | Names, titles, emails |
password | Secure password input | User passwords, API keys |
color | Color picker | #FF5733, rgb(255, 87, 51) |
datepicker | Date selection | 2024-01-15 |
radio | Single selection from options | Gender, status |
checkbox | Boolean or multiple selection | Terms acceptance, features |
select | Dropdown selection | Country, category |
textarea | Multi-line text | Comments, descriptions |
file / image | File uploads | Documents, images |
Using Field Types
You can specify field types using the FieldType
enum (recommended) or as strings when using custom field types:
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
// Text input
firstName: {
type: FieldType.text,
label: 'First name',
placeholder: 'John',
},
// Password input
password: {
type: FieldType.password,
label: 'Password',
placeholder: 'Enter your password',
},
// Email input (use text type with validation)
email: {
type: FieldType.text,
label: 'Email',
placeholder: 'user@example.com',
rules: (value) => {
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format'
return true
},
},
// Color picker
favoriteColor: {
type: FieldType.color,
label: 'Favorite color',
modelValue: '#FF5733',
},
// Date picker
birthDate: {
type: FieldType.datepicker,
label: 'Birth date',
},
// Radio buttons
gender: {
type: FieldType.radio,
label: 'Gender',
options: ['Male', 'Female', 'Other']
},
// Checkbox
acceptTerms: {
type: FieldType.checkbox,
label: 'I accept the terms and conditions',
modelValue: false,
},
// Select dropdown
country: {
type: FieldType.select,
label: 'Country',
options: ['USA', 'Canada', 'Mexico', 'UK']
},
// Textarea
comments: {
type: FieldType.textarea,
label: 'Comments',
placeholder: 'Enter your comments here...',
rows: 4,
},
// File upload
avatar: {
type: FieldType.file,
label: 'Profile Picture',
accept: 'image/*',
},
}
})
</script>
Detailed Field Type Examples
Text Field
The most common field type for general text input.
<script lang="ts" setup>
const form = useForm({
fields: {
username: {
type: FieldType.text,
label: 'Username',
placeholder: 'johndoe',
minlength: 3,
maxlength: 20,
},
}
})
</script>
Password Field
Securely masks user input. Perfect for passwords and sensitive data.
<script lang="ts" setup>
const form = useForm({
fields: {
password: {
type: FieldType.password,
label: 'Password',
minlength: 8,
rules: (value) => (value && value.length >= 8) || 'Password must be at least 8 characters'
},
confirmPassword: {
type: FieldType.password,
label: 'Confirm Password',
rules: (value) => value === form.fields.password.modelValue || 'Passwords must match'
},
}
})
</script>
Select Field
Dropdown selection from a list of options. See the Options section for advanced usage with objects.
<script lang="ts" setup>
const form = useForm({
fields: {
priority: {
type: FieldType.select,
label: 'Priority',
options: ['Low', 'Medium', 'High', 'Critical'],
modelValue: 'Medium',
},
}
})
</script>
Radio Field
Single selection from a visible list of options.
<script lang="ts" setup>
const form = useForm({
fields: {
subscription: {
type: FieldType.radio,
label: 'Subscription Plan',
options: ['Free', 'Pro', 'Enterprise'],
modelValue: 'Free',
},
}
})
</script>
Checkbox Field
Boolean value or multiple selection.
<script lang="ts" setup>
const form = useForm({
fields: {
// Single checkbox (boolean)
newsletter: {
type: FieldType.checkbox,
label: 'Subscribe to newsletter',
modelValue: false,
},
// Multiple checkboxes (array)
interests: {
type: FieldType.checkbox,
label: 'Interests',
options: ['Technology', 'Sports', 'Music', 'Travel'],
multiple: true,
modelValue: [],
},
}
})
</script>
Textarea Field
Multi-line text input for longer content.
<script lang="ts" setup>
const form = useForm({
fields: {
description: {
type: FieldType.textarea,
label: 'Description',
placeholder: 'Enter a detailed description...',
rows: 5,
maxlength: 500,
},
}
})
</script>
Color Picker
Visual color selection tool.
<script lang="ts" setup>
const form = useForm({
fields: {
brandColor: {
type: FieldType.color,
label: 'Brand Color',
modelValue: '#3B82F6',
},
}
})
</script>
Date Picker
Calendar-based date selection.
<script lang="ts" setup>
const form = useForm({
fields: {
startDate: {
type: FieldType.datepicker,
label: 'Start Date',
min: new Date().toISOString().split('T')[0], // Today
},
endDate: {
type: FieldType.datepicker,
label: 'End Date',
},
}
})
</script>
File Upload
File and image upload handling.
<script lang="ts" setup>
const form = useForm({
fields: {
// Single file upload
document: {
type: FieldType.file,
label: 'Upload Document',
accept: '.pdf,.doc,.docx',
},
// Image upload
profilePicture: {
type: FieldType.image,
label: 'Profile Picture',
accept: 'image/*',
preview: true, // Show image preview
},
// Multiple files
attachments: {
type: FieldType.file,
label: 'Attachments',
multiple: true,
},
}
})
</script>
Custom Field Types
You can also use custom field types by providing a string value and registering a custom component:
<script lang="ts" setup>
const form = useForm({
fields: {
rating: {
type: 'rating', // Custom type
label: 'Rate this product',
max: 5,
},
tags: {
type: 'tags-input', // Custom type
label: 'Tags',
modelValue: [],
},
}
})
</script>
See the Custom Components section for more information on creating custom field types.
Native Attributes
Fields in FancyCRUD support all native HTML input attributes as well as framework-specific attributes from your UI wrapper (Vuetify, Element Plus, Quasar, or Oruga UI). When FancyCRUD renders a field, it automatically passes all attributes to the underlying input component.
Common HTML Attributes
You can use standard HTML input attributes on any field:
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
username: {
type: FieldType.text,
label: 'Username',
placeholder: 'Enter your username', // HTML attribute
minlength: 3, // HTML attribute
maxlength: 20, // HTML attribute
required: true, // HTML attribute
autocomplete: 'username', // HTML attribute
pattern: '[a-zA-Z0-9]+', // HTML attribute
},
email: {
type: FieldType.text,
label: 'Email',
placeholder: 'user@example.com',
required: true,
autocomplete: 'email',
},
age: {
type: FieldType.text,
label: 'Age',
min: 18, // HTML attribute
max: 120, // HTML attribute
step: 1, // HTML attribute
},
description: {
type: FieldType.textarea,
label: 'Description',
rows: 5, // HTML attribute
cols: 50, // HTML attribute
maxlength: 500, // HTML attribute
placeholder: 'Enter description...',
},
}
})
</script>
Framework-Specific Attributes
Each UI wrapper supports its own framework-specific attributes. FancyCRUD passes these directly to the component.
Vuetify Attributes
<script lang="ts" setup>
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First name',
placeholder: 'John Doe',
// Vuetify-specific attributes
variant: 'outlined',
density: 'compact',
clearable: true,
persistentHint: true,
hint: 'Enter your first name',
},
}
})
</script>
Element Plus Attributes
<script lang="ts" setup>
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First name',
placeholder: 'John Doe',
// Element Plus-specific attributes
size: 'large',
clearable: true,
prefixIcon: 'User',
showWordLimit: true,
},
}
})
</script>
Quasar Attributes
<script lang="ts" setup>
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First name',
placeholder: 'John Doe',
// Quasar-specific attributes
filled: true,
rounded: true,
clearable: true,
bottomSlots: true,
},
}
})
</script>
Commonly Used Native Attributes
Attribute | Type | Description | Applicable Fields |
---|---|---|---|
placeholder | string | Hint text when field is empty | text, textarea, select |
required | boolean | Makes field required | all |
disabled | boolean | Disables the field | all |
readonly | boolean | Makes field read-only | all |
minlength | number | Minimum character length | text, textarea |
maxlength | number | Maximum character length | text, textarea |
min | number/string | Minimum value/date | number, datepicker |
max | number/string | Maximum value/date | number, datepicker |
step | number | Value increment step | number |
pattern | string | Regex validation pattern | text |
autocomplete | string | Browser autocomplete hint | text, password |
rows | number | Number of visible rows | textarea |
cols | number | Number of visible columns | textarea |
accept | string | Acceptable file types | file, image |
multiple | boolean | Allow multiple selections | select, file |
Example: Complete Field with Native Attributes
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
email: {
type: FieldType.text,
label: 'Email Address',
// HTML attributes
placeholder: 'user@example.com',
required: true,
maxlength: 100,
autocomplete: 'email',
pattern: '[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$',
// FancyCRUD attributes
rules: (value) => {
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format'
return true
},
// Wrapper-specific (Vuetify example)
variant: 'outlined',
hint: 'We will never share your email',
persistentHint: true,
},
},
settings: {
url: 'users/',
},
})
</script>
TIP
You can mix HTML attributes, FancyCRUD-specific attributes, and UI framework attributes in the same field definition. FancyCRUD intelligently passes each attribute to the appropriate destination.
INFO
If an attribute is not recognized by the HTML input or your UI framework, it will be safely ignored. This allows for forward compatibility and experimentation.
Reserved Attributes
FancyCRUD reserves certain attribute names for special functionality. These attributes control how fields behave, validate, and interact with your data.
ModelValue
Property | Type | Default | Required |
---|---|---|---|
modelValue | any | null | No |
The modelValue
attribute stores the current value of the field. It's reactive and updates automatically as the user types or selects values. You can also set an initial value.
<template>
<f-form v-bind="form"></f-form>
{{ form.fields.firstName.modelValue }}
</template>
<script setup lang='ts'>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First name',
modelValue: 'Juan Camaney'
},
}
})
</script>
modelValue: Juan Camaney
ModelKey
Property | Type | Default | Required |
---|---|---|---|
modelKey | string | Same as field key | No |
The modelKey
attribute specifies the property name that will be used in the HTTP request payload. By default, it uses the field key, but you can override it to match your API's expected format.
Example: Matching API Field Names
<script setup lang='ts'>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
// Field key is "firstName" but API expects "first_name"
firstName: {
type: FieldType.text,
label: 'First name',
modelKey: 'first_name',
},
// Field key is "emailAddress" but API expects "email"
emailAddress: {
type: FieldType.text,
label: 'Email',
modelKey: 'email',
},
},
settings: {
url: 'users/',
},
})
// When form is submitted, the payload will be:
// { first_name: "John", email: "john@example.com" }
// Instead of: { firstName: "John", emailAddress: "john@example.com" }
</script>
TIP
Use modelKey
when your frontend field names don't match your API's expected property names. This is common when working with APIs that use snake_case while your Vue code uses camelCase.
Request payload
{
"firstName": null
}
Errors
Property | Type | Default | Required |
---|---|---|---|
errors | string[] | [] | No |
The errors
attribute contains validation error messages for the field. Errors can come from:
- Frontend validation rules
- Backend API validation responses
- Programmatic assignment
Error messages are automatically displayed by the field component.
Example: Programmatic Error Setting
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
username: {
type: FieldType.text,
label: 'Username',
errors: ['This username is already taken'],
}
}
})
// You can also set errors programmatically
function checkUsername() {
form.fields.username.errors = ['Username is already in use']
}
// Or clear errors
function clearErrors() {
form.fields.username.errors = []
}
</script>
INFO
Errors are automatically cleared when the user modifies the field value. FancyCRUD also automatically populates errors from backend validation responses.
Rules
Property | Type | Default | Required |
---|---|---|---|
rules | (value: any) => string | true | unknown | undefined | No |
The rules
attribute defines validation logic for the field. Rules are functions that return either true
(valid) or an error message string (invalid).
Simple Validation Example
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
email: {
type: FieldType.text,
label: 'Email',
rules: (value) => {
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format'
return true
},
},
age: {
type: FieldType.text,
label: 'Age',
rules: (value) => {
if (!value) return 'Age is required'
const age = parseInt(value)
if (age < 18) return 'Must be 18 or older'
return true
},
},
}
})
</script>
Using Validation Libraries
FancyCRUD supports popular validation libraries like Zod, Valibot, Yup, and Vuelidate. See the Validation Rules section for setup details.
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { z } from 'zod'
const form = useForm({
fields: {
email: {
type: FieldType.text,
label: 'Email',
rules: value => ({ value, rule: z.string().email() }),
},
password: {
type: FieldType.password,
label: 'Password',
rules: value => ({ value, rule: z.string().min(8) }),
},
}
})
</script>
TIP
For more complex validation scenarios and library-specific examples, see the complete Rules documentation.
Options
Property | Type | Default | Required |
---|---|---|---|
options | any[] | undefined | No |
optionLabel | string | undefined | No |
optionValue | string | undefined | No |
The options
attribute provides a list of choices for select, radio, and checkbox fields. Options can be simple arrays of strings or arrays of objects.
Simple Array Options
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
favoriteColor: {
type: FieldType.select,
label: 'Favorite color',
options: ['Red', 'Blue', 'Green', 'Purple'],
},
skills: {
type: FieldType.checkbox,
label: 'Skills',
options: ['Vue', 'React', 'Angular', 'Svelte'],
multiple: true,
},
}
})
</script>
Object Array Options
When working with objects, use optionLabel
to specify which property to display and optionValue
to specify which property to use as the value.
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const employees = [
{ id: 1, name: 'Marco Anderson', department: 'Engineering' },
{ id: 2, name: 'Linda Chen', department: 'Design' },
{ id: 3, name: 'Alex Johnson', department: 'Sales' },
{ id: 4, name: 'Emily Rodriguez', department: 'Marketing' },
]
const form = useForm({
fields: {
assignedTo: {
type: FieldType.select,
label: 'Assign to',
options: employees,
optionLabel: 'name', // Display "Marco Anderson"
optionValue: 'id', // Submit value: 1
},
}
})
// When user selects "Marco Anderson"
// form.fields.assignedTo.modelValue will be: 1
</script>
Dynamic Options from Reactive Data
<script lang="ts" setup>
import { ref } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'
const categories = ref([
{ id: 1, name: 'Electronics' },
{ id: 2, name: 'Clothing' },
{ id: 3, name: 'Books' },
])
const form = useForm({
fields: {
category: {
type: FieldType.select,
label: 'Category',
options: categories.value, // Reactive options
optionLabel: 'name',
optionValue: 'id',
},
}
})
// Update options dynamically
function addCategory() {
categories.value.push({ id: 4, name: 'Sports' })
form.fields.category.options = categories.value
}
</script>
WARNING
When working with object arrays, always specify optionLabel
to avoid displaying [object Object]
in the UI.
TIP
If you omit optionValue
when working with objects, the entire object will be stored in modelValue
instead of just a single property.
Fetching Options from API
For loading options from a backend API, use the url
attribute instead. See the URL section below.
URL
Property | Type | Default | Required |
---|---|---|---|
url | string | undefined | No |
The url
attribute allows fields to fetch their options from a backend API. When specified, FancyCRUD automatically sends a GET request to the URL and populates the field's options
with the response data.
Basic API Usage
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
assignedTo: {
type: FieldType.select,
label: 'Assign to Employee',
url: 'employees/', // Fetches from /api/employees/
optionLabel: 'name', // Display employee name
optionValue: 'id', // Submit employee ID
},
country: {
type: FieldType.select,
label: 'Country',
url: 'countries/',
optionLabel: 'name',
optionValue: 'code',
},
}
})
</script>
Expected API Response Format
The API should return an array of objects:
[
{ "id": 1, "name": "Marco Anderson", "department": "Engineering" },
{ "id": 2, "name": "Linda Chen", "department": "Design" },
{ "id": 3, "name": "Alex Johnson", "department": "Sales" }
]
Loading State
FancyCRUD automatically handles the loading state while fetching options. The field will be disabled during the request and re-enabled once data is loaded.
Error Handling
If the API request fails, FancyCRUD will:
- Log the error to the console
- Keep the field enabled
- Leave the
options
array empty
Intercepting and Transforming Options
You can intercept and transform the API response before it's assigned to options
:
<script lang="ts" setup>
const form = useForm({
fields: {
employee: {
type: FieldType.select,
label: 'Employee',
url: 'employees/',
optionLabel: 'name',
optionValue: 'id',
// Transform the response data
interceptOptions: (options) => {
// Filter, sort, or modify options
return options.filter(emp => emp.active === true)
},
},
}
})
</script>
See interceptOptions in Methods for more details.
TIP
The url
attribute is relative to your configured base HTTP client. Make sure you've set up your HTTP configuration in fancy-crud.config.ts
. See Configuration.
DebounceTime
Property | Type | Default | Required |
---|---|---|---|
debounceTime | number | 0 | No |
The debounceTime
property specifies a delay (in milliseconds) before updating the modelValue
after user input. This is useful for reducing the frequency of validation checks or API calls.
Example: Debounced Username Check
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
import { watch } from 'vue'
const form = useForm({
fields: {
username: {
type: FieldType.text,
label: 'Username',
debounceTime: 500, // Wait 500ms after user stops typing
},
}
})
// This will only fire 500ms after the user stops typing
watch(() => form.fields.username.modelValue, async (value) => {
if (value) {
// Check username availability
const response = await fetch(`/api/check-username/${value}`)
const data = await response.json()
if (!data.available) {
form.fields.username.errors = ['Username is already taken']
}
}
})
</script>
TIP
Use debounceTime
for fields that trigger expensive operations like API calls, database queries, or complex validations. A value between 300-500ms provides a good user experience.
CreateOnly
Property | Type | Default | Required |
---|---|---|---|
createOnly | boolean | false | No |
Display the field only when the form is in create mode. Useful for fields that should only appear when creating new records.
<script lang="ts" setup>
import { useForm, FieldType, FORM_MODE } from '@fancy-crud/vue'
const form = useForm({
fields: {
password: {
type: FieldType.password,
label: 'Password',
createOnly: true, // Only shown when creating a new user
},
confirmPassword: {
type: FieldType.password,
label: 'Confirm Password',
createOnly: true,
},
// This field appears in both modes
email: {
type: FieldType.text,
label: 'Email',
},
},
settings: {
url: 'users/',
mode: FORM_MODE.create,
},
})
</script>
UpdateOnly
Property | Type | Default | Required |
---|---|---|---|
updateOnly | boolean | false | No |
Display the field only when the form is in update mode. Useful for fields that should only appear when editing existing records.
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
id: {
type: FieldType.text,
label: 'ID',
updateOnly: true, // Only shown when updating
readonly: true,
},
lastModified: {
type: FieldType.datepicker,
label: 'Last Modified',
updateOnly: true,
readonly: true,
},
// This field appears in both modes
name: {
type: FieldType.text,
label: 'Name',
},
}
})
</script>
Hidden
Property | Type | Default | Required |
---|---|---|---|
hidden | boolean | false | No |
Hide the field from the form UI regardless of the form mode. The field will still be included in the request payload if it has a modelValue
.
<script lang="ts" setup>
const form = useForm({
fields: {
userId: {
type: FieldType.text,
label: 'User ID',
hidden: true, // Never displayed in the form
modelValue: currentUser.id, // But still sent in the request
},
name: {
type: FieldType.text,
label: 'Name',
},
}
})
</script>
TIP
Use hidden
for fields that need to be submitted with the form but shouldn't be visible to users, such as IDs, timestamps, or system-generated values.
Exclude
Property | Type | Default | Required |
---|---|---|---|
exclude | boolean | false | No |
Exclude the field from the HTTP request payload. The field will still be displayed in the form, but its value won't be sent to the server.
<script lang="ts" setup>
const form = useForm({
fields: {
confirmPassword: {
type: FieldType.password,
label: 'Confirm Password',
exclude: true, // Not sent to the API
rules: (value) => {
return value === form.fields.password.modelValue || 'Passwords must match'
},
},
password: {
type: FieldType.password,
label: 'Password',
// This WILL be sent to the API
},
}
})
</script>
TIP
Use exclude
for client-side-only fields like password confirmation, search filters, or UI state that shouldn't be persisted.
Multiple
Property | Type | Default | Required |
---|---|---|---|
multiple | boolean | false | No |
Enable multiple value selection. When true
, the modelValue
is initialized as an empty array []
instead of null
. Commonly used with select
, checkbox
, and file
fields.
<script lang="ts" setup>
const form = useForm({
fields: {
// Multiple select
skills: {
type: FieldType.select,
label: 'Skills',
options: ['Vue', 'React', 'Angular', 'Svelte'],
multiple: true,
// modelValue will be: []
},
// Multiple checkboxes
interests: {
type: FieldType.checkbox,
label: 'Interests',
options: ['Technology', 'Sports', 'Music', 'Travel'],
multiple: true,
// modelValue will be: []
},
// Multiple file uploads
attachments: {
type: FieldType.file,
label: 'Upload Files',
multiple: true,
// modelValue will be: []
},
}
})
</script>
Wrapper
Property | Type | Default | Required |
---|---|---|---|
wrapper | object | {} | No |
Pass additional attributes to the field's wrapper component. This is particularly useful for applying custom styling classes or wrapper-specific props.
<script lang="ts" setup>
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First name',
// Apply classes to the field wrapper
wrapper: {
class: 'col-span-6 md:col-span-4',
},
},
lastName: {
type: FieldType.text,
label: 'Last name',
wrapper: {
class: 'col-span-6 md:col-span-4',
},
},
description: {
type: FieldType.textarea,
label: 'Description',
wrapper: {
class: 'col-span-12',
style: 'margin-top: 1rem',
},
},
}
})
</script>
TIP
The wrapper
attribute's behavior depends on your UI framework wrapper. Check your wrapper's documentation for supported properties.
Methods
Field methods are functions that allow you to transform, intercept, or compute field values at different stages of the form lifecycle.
RecordValue
Property | Type | Default | Required |
---|---|---|---|
recordValue | (record: any) => unknown | undefined | No |
The recordValue
function extracts and transforms values from the source record object before assigning them to the field's modelValue
. This is essential for handling nested objects, computed values, or data transformations during form initialization.
Use Cases:
- Extracting values from nested objects
- Computing derived values
- Transforming data structures
- Accessing related object properties
Example: Extracting Nested Values
// Backend returns this structure:
{
id: 1,
name: 'John Doe',
employee: {
id: 42,
name: 'Samira Gonzales',
department: 'Engineering'
}
}
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
interface Employee {
id: number
name: string
department: string
}
const form = useForm({
fields: {
employeeId: {
type: FieldType.text,
label: 'Employee ID',
recordValue: (record: { employee: Employee }) => record.employee.id,
// When form loads, modelValue will be: 42
},
employeeName: {
type: FieldType.text,
label: 'Employee Name',
recordValue: (record: { employee: Employee }) => record.employee.name,
// When form loads, modelValue will be: "Samira Gonzales"
},
}
})
</script>
Example: Computed Values
<script lang="ts" setup>
const form = useForm({
fields: {
fullName: {
type: FieldType.text,
label: 'Full Name',
recordValue: (record) => `${record.firstName} ${record.lastName}`,
// Combines first and last name
},
displayPrice: {
type: FieldType.text,
label: 'Price',
recordValue: (record) => `$${(record.price / 100).toFixed(2)}`,
// Converts cents to dollars
},
}
})
</script>
Example: Array Mapping
<script lang="ts" setup>
const form = useForm({
fields: {
tagIds: {
type: FieldType.select,
label: 'Tags',
multiple: true,
recordValue: (record) => record.tags.map((tag: any) => tag.id),
// Extracts IDs from array of tag objects
// Input: [{ id: 1, name: 'vue' }, { id: 2, name: 'typescript' }]
// Output: [1, 2]
},
}
})
</script>
TIP
recordValue
is called when the form loads data (update mode) or when you manually set form data. It does NOT affect the data being submitted - use parseModelValue
for that.
InterceptOptions
Property | Type | Default | Required |
---|---|---|---|
interceptOptions | (options: any[]) => unknown[] | undefined | No |
The interceptOptions
function intercepts and transforms options before they are assigned to the field's options
attribute. This is useful when fetching options from an API (using the url
attribute) and you need to filter, sort, or transform the data.
Use Cases:
- Filtering options based on criteria
- Sorting options
- Transforming option structures
- Adding computed properties to options
Example: Filtering Active Items
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
employee: {
type: FieldType.select,
label: 'Assign to',
url: 'employees/', // Fetches all employees from API
optionLabel: 'name',
optionValue: 'id',
interceptOptions: (options) => {
// Only show active employees
return options.filter(emp => emp.status === 'active')
},
},
}
})
</script>
Example: Sorting Options
<script lang="ts" setup>
const form = useForm({
fields: {
category: {
type: FieldType.select,
label: 'Category',
url: 'categories/',
optionLabel: 'name',
optionValue: 'id',
interceptOptions: (options) => {
// Sort categories alphabetically
return options.sort((a, b) => a.name.localeCompare(b.name))
},
},
}
})
</script>
Example: Adding Computed Properties
<script lang="ts" setup>
const form = useForm({
fields: {
product: {
type: FieldType.select,
label: 'Product',
url: 'products/',
optionLabel: 'displayName',
optionValue: 'id',
interceptOptions: (options) => {
// Add a computed display name
return options.map(product => ({
...product,
displayName: `${product.name} - $${product.price}`
}))
},
},
}
})
</script>
Example: Complex Transformations
<script lang="ts" setup>
const form = useForm({
fields: {
manager: {
type: FieldType.select,
label: 'Manager',
url: 'users/',
optionLabel: 'fullName',
optionValue: 'id',
interceptOptions: (options) => {
return options
.filter(user => user.role === 'manager')
.filter(user => user.active)
.sort((a, b) => a.lastName.localeCompare(b.lastName))
.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName} (${user.department})`
}))
},
},
}
})
</script>
TIP
interceptOptions
is particularly useful when working with the url
attribute, as it allows you to process API responses without modifying your backend.
ParseModelValue
Property | Type | Default | Required |
---|---|---|---|
parseModelValue | (value: any) => unknown | undefined | No |
The parseModelValue
function transforms the field's modelValue
before including it in the HTTP request payload. This is essential for data type conversions, formatting, or structural transformations.
Use Cases:
- Converting data types (string to number, etc.)
- Formatting dates or currency
- Transforming data structures
- Normalizing values
Example: Type Conversion
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
age: {
type: FieldType.text,
label: 'Age',
modelValue: '25', // String from input
parseModelValue: Number,
// Sent to API as: 25 (number)
},
price: {
type: FieldType.text,
label: 'Price',
modelValue: '99.99',
parseModelValue: parseFloat,
// Sent to API as: 99.99 (number)
},
}
})
</script>
Example: Date Formatting
<script lang="ts" setup>
const form = useForm({
fields: {
birthDate: {
type: FieldType.datepicker,
label: 'Birth Date',
parseModelValue: (value) => {
// Convert Date object to ISO string
return value instanceof Date ? value.toISOString() : value
},
},
scheduledAt: {
type: FieldType.datepicker,
label: 'Scheduled Date',
parseModelValue: (value) => {
// Format as YYYY-MM-DD
return value instanceof Date ? value.toISOString().split('T')[0] : value
},
},
}
})
</script>
Example: Currency Conversion
<script lang="ts" setup>
const form = useForm({
fields: {
price: {
type: FieldType.text,
label: 'Price ($)',
modelValue: '99.99',
parseModelValue: (value) => {
// Convert dollars to cents for API
return Math.round(parseFloat(value) * 100)
},
// Input: "99.99"
// Sent to API: 9999 (cents)
},
}
})
</script>
Example: Array Transformation
<script lang="ts" setup>
const form = useForm({
fields: {
tags: {
type: FieldType.select,
label: 'Tags',
multiple: true,
modelValue: [1, 2, 3], // Array of IDs
parseModelValue: (value) => {
// Convert array to comma-separated string
return Array.isArray(value) ? value.join(',') : value
},
// Input: [1, 2, 3]
// Sent to API: "1,2,3"
},
}
})
</script>
Example: Nested Object Transformation
<script lang="ts" setup>
const form = useForm({
fields: {
employeeId: {
type: FieldType.select,
label: 'Employee',
options: employees,
optionLabel: 'name',
// Note: no optionValue, so entire object is stored
parseModelValue: (value) => {
// Extract just the ID from the employee object
return typeof value === 'object' ? value.id : value
},
// modelValue: { id: 42, name: 'John Doe', department: 'IT' }
// Sent to API: 42
},
}
})
</script>
TIP
Use parseModelValue
when you need to transform how data is sent to the API, while keeping the field's display logic unchanged. For transforming data when loading (opposite direction), use recordValue
.
Events
Fields support all native HTML input events as well as custom events from your UI wrapper. Events are defined using the on[EventName]
pattern (e.g., onFocus
, onBlur
, onClick
).
Common Field Events
Event | When Triggered | Use Case |
---|---|---|
onFocus | Field gains focus | Show hints, load related data |
onBlur | Field loses focus | Validate, auto-format, save draft |
onChange | Value changes | Track changes, dependent fields |
onClick | Field is clicked | Custom interactions |
onInput | User types | Real-time validation, character count |
onKeydown | Key is pressed | Keyboard shortcuts, input restrictions |
onKeyup | Key is released | Search suggestions, auto-complete |
Basic Event Handling
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
email: {
type: FieldType.text,
label: 'Email',
onFocus: (event) => {
console.log('Email field focused')
},
onBlur: (event) => {
// Validate email when user leaves the field
console.log('Email field blurred')
},
onChange: (event) => {
console.log('Email value changed:', event.target.value)
},
},
}
})
</script>
Advanced Event Examples
Example: Auto-formatting on Blur
<script lang="ts" setup>
const form = useForm({
fields: {
phoneNumber: {
type: FieldType.text,
label: 'Phone Number',
placeholder: '(555) 123-4567',
onBlur: () => {
const value = form.fields.phoneNumber.modelValue
if (value) {
// Auto-format phone number
const formatted = value.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3')
form.fields.phoneNumber.modelValue = formatted
}
},
},
}
})
</script>
Example: Dependent Fields
<script lang="ts" setup>
const form = useForm({
fields: {
country: {
type: FieldType.select,
label: 'Country',
options: ['USA', 'Canada', 'Mexico'],
onChange: async () => {
const country = form.fields.country.modelValue
// Load states/provinces based on selected country
const response = await fetch(`/api/states/${country}`)
const states = await response.json()
form.fields.state.options = states
form.fields.state.modelValue = null // Reset state selection
},
},
state: {
type: FieldType.select,
label: 'State/Province',
options: [],
},
}
})
</script>
Example: Character Counter
<script lang="ts" setup>
import { ref } from 'vue'
const charCount = ref(0)
const maxLength = 280
const form = useForm({
fields: {
description: {
type: FieldType.textarea,
label: 'Description',
maxlength: maxLength,
onInput: (event) => {
charCount.value = event.target.value.length
},
},
}
})
</script>
<template>
<f-form v-bind="form" />
<p>{{ charCount }} / {{ maxLength }} characters</p>
</template>
Example: Real-time Search
<script lang="ts" setup>
import { ref } from 'vue'
import { watchDebounced } from '@vueuse/core'
const searchResults = ref([])
const form = useForm({
fields: {
search: {
type: FieldType.text,
label: 'Search',
placeholder: 'Type to search...',
debounceTime: 300,
},
}
})
// Watch for changes and perform search
watchDebounced(
() => form.fields.search.modelValue,
async (query) => {
if (query && query.length >= 3) {
const response = await fetch(`/api/search?q=${query}`)
searchResults.value = await response.json()
} else {
searchResults.value = []
}
},
{ debounce: 300 }
)
</script>
Example: Keyboard Shortcuts
<script lang="ts" setup>
const form = useForm({
fields: {
notes: {
type: FieldType.textarea,
label: 'Notes',
onKeydown: (event) => {
// Save on Ctrl+S or Cmd+S
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault()
console.log('Saving draft...')
// Trigger save
}
// Clear on Ctrl+K or Cmd+K
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault()
form.fields.notes.modelValue = ''
}
},
},
}
})
</script>
Example: Focus Management
<script lang="ts" setup>
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First Name',
onKeydown: (event) => {
// Move to next field on Enter
if (event.key === 'Enter') {
event.preventDefault()
document.querySelector('input[name="lastName"]')?.focus()
}
},
},
lastName: {
type: FieldType.text,
label: 'Last Name',
},
}
})
</script>
TIP
Events receive the native browser event object as their first parameter. You can access the event target, prevent default behavior, and stop propagation as needed.
WARNING
When using onChange
with debounceTime
, be aware that the event fires after the debounce delay, not immediately upon user input. Use onInput
for immediate feedback.
Reactivity
All field properties returned by useForm
are fully reactive. This means you can use them with Vue's reactive APIs like watch
, computed
, watchEffect
, and more. Changes to field values, errors, or any other properties automatically trigger reactivity.
Reactive Field Properties
All field properties are reactive, including:
modelValue
- The current field valueerrors
- Array of error messagesoptions
- List of available options (for select/radio/checkbox)hidden
- Visibility statedisabled
- Disabled state- Any custom properties you add
Using Computed Properties
<script lang="ts" setup>
import { computed } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First Name',
},
lastName: {
type: FieldType.text,
label: 'Last Name',
},
}
})
// Compute full name from fields
const fullName = computed(() => {
const first = form.fields.firstName.modelValue || ''
const last = form.fields.lastName.modelValue || ''
return `${first} ${last}`.trim()
})
// Check if form has any errors
const hasErrors = computed(() => {
return Object.values(form.fields).some(field => field.errors.length > 0)
})
// Count filled fields
const filledFieldsCount = computed(() => {
return Object.values(form.fields).filter(field => field.modelValue).length
})
</script>
<template>
<f-form v-bind="form" />
<p>Full Name: {{ fullName }}</p>
<p>Form has errors: {{ hasErrors }}</p>
<p>Filled fields: {{ filledFieldsCount }}</p>
</template>
Watching Field Changes
<script lang="ts" setup>
import { watch } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
email: {
type: FieldType.text,
label: 'Email',
},
password: {
type: FieldType.password,
label: 'Password',
},
}
})
// Watch a single field value
watch(() => form.fields.email.modelValue, (newValue, oldValue) => {
console.log('Email changed from', oldValue, 'to', newValue)
})
// Watch multiple fields
watch(
[
() => form.fields.email.modelValue,
() => form.fields.password.modelValue,
],
([email, password]) => {
console.log('Form values:', { email, password })
// You could enable/disable a submit button based on these values
}
)
// Watch errors
watch(() => form.fields.email.errors, (errors) => {
if (errors.length > 0) {
console.log('Email has errors:', errors)
}
}, { deep: true })
</script>
Conditional Field Display
<script lang="ts" setup>
import { computed } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
accountType: {
type: FieldType.radio,
label: 'Account Type',
options: ['Personal', 'Business'],
},
companyName: {
type: FieldType.text,
label: 'Company Name',
},
taxId: {
type: FieldType.text,
label: 'Tax ID',
},
}
})
// Show/hide fields based on account type
watch(() => form.fields.accountType.modelValue, (type) => {
const isBusiness = type === 'Business'
// Toggle field visibility
form.fields.companyName.hidden = !isBusiness
form.fields.taxId.hidden = !isBusiness
})
</script>
Dependent Field Updates
<script lang="ts" setup>
import { watch } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
quantity: {
type: FieldType.text,
label: 'Quantity',
modelValue: 1,
},
pricePerUnit: {
type: FieldType.text,
label: 'Price per Unit',
modelValue: 10,
},
total: {
type: FieldType.text,
label: 'Total Price',
readonly: true,
},
}
})
// Auto-calculate total
watch(
[
() => form.fields.quantity.modelValue,
() => form.fields.pricePerUnit.modelValue,
],
([qty, price]) => {
const quantity = parseFloat(qty) || 0
const pricePerUnit = parseFloat(price) || 0
form.fields.total.modelValue = (quantity * pricePerUnit).toFixed(2)
},
{ immediate: true }
)
</script>
Real-time Validation
<script lang="ts" setup>
import { watch } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
username: {
type: FieldType.text,
label: 'Username',
debounceTime: 500,
},
}
})
// Check username availability
watch(() => form.fields.username.modelValue, async (username) => {
if (!username || username.length < 3) {
form.fields.username.errors = []
return
}
try {
const response = await fetch(`/api/check-username/${username}`)
const data = await response.json()
if (!data.available) {
form.fields.username.errors = ['Username is already taken']
} else {
form.fields.username.errors = []
}
} catch (error) {
console.error('Error checking username:', error)
}
})
</script>
Using watchEffect
<script lang="ts" setup>
import { watchEffect } from 'vue'
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
firstName: {
type: FieldType.text,
label: 'First Name',
},
lastName: {
type: FieldType.text,
label: 'Last Name',
},
displayName: {
type: FieldType.text,
label: 'Display Name',
},
}
})
// Auto-sync display name
watchEffect(() => {
const first = form.fields.firstName.modelValue
const last = form.fields.lastName.modelValue
if (first || last) {
form.fields.displayName.modelValue = `${first || ''} ${last || ''}`.trim()
}
})
</script>
Template Refs with Reactivity
<template>
<f-form v-bind="form">
<template #field-email="{ field }">
<div>
<input
v-model="field.modelValue"
:class="{ 'error': field.errors.length > 0 }"
@blur="validateEmail(field)"
/>
<span v-if="field.errors.length" class="error-message">
{{ field.errors[0] }}
</span>
</div>
</template>
</f-form>
</template>
<script lang="ts" setup>
import { useForm, FieldType } from '@fancy-crud/vue'
const form = useForm({
fields: {
email: {
type: FieldType.text,
label: 'Email',
},
}
})
function validateEmail(field: any) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!field.modelValue) {
field.errors = ['Email is required']
} else if (!emailRegex.test(field.modelValue)) {
field.errors = ['Invalid email format']
} else {
field.errors = []
}
}
</script>
Performance Considerations
TIP
When watching deeply nested objects or arrays, use the deep: true
option carefully as it can impact performance. Consider watching specific properties instead of entire objects when possible.
<script lang="ts" setup>
// Good: Watch specific property
watch(() => form.fields.email.modelValue, (value) => {
console.log(value)
})
// Less efficient: Watch entire field object
watch(() => form.fields.email, (field) => {
console.log(field)
}, { deep: true })
</script>
INFO
All FancyCRUD fields use Vue 3's reactivity system under the hood, which means they benefit from all of Vue's reactivity optimizations and performance improvements.