Skip to content

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

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

js
{
  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:

vue
<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 TypeUse CaseExample Data
textGeneral text inputNames, titles, emails
passwordSecure password inputUser passwords, API keys
colorColor picker#FF5733, rgb(255, 87, 51)
datepickerDate selection2024-01-15
radioSingle selection from optionsGender, status
checkboxBoolean or multiple selectionTerms acceptance, features
selectDropdown selectionCountry, category
textareaMulti-line textComments, descriptions
file / imageFile uploadsDocuments, images

Using Field Types

You can specify field types using the FieldType enum (recommended) or as strings when using custom field types:

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

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

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

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

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

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

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

vue
<script lang="ts" setup>
const form = useForm({
  fields: {
    brandColor: {
      type: FieldType.color,
      label: 'Brand Color',
      modelValue: '#3B82F6',
    },
  }
})
</script>

Date Picker

Calendar-based date selection.

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

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

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

Create new record

R
G
B
A

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:

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

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

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

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

AttributeTypeDescriptionApplicable Fields
placeholderstringHint text when field is emptytext, textarea, select
requiredbooleanMakes field requiredall
disabledbooleanDisables the fieldall
readonlybooleanMakes field read-onlyall
minlengthnumberMinimum character lengthtext, textarea
maxlengthnumberMaximum character lengthtext, textarea
minnumber/stringMinimum value/datenumber, datepicker
maxnumber/stringMaximum value/datenumber, datepicker
stepnumberValue increment stepnumber
patternstringRegex validation patterntext
autocompletestringBrowser autocomplete hinttext, password
rowsnumberNumber of visible rowstextarea
colsnumberNumber of visible columnstextarea
acceptstringAcceptable file typesfile, image
multiplebooleanAllow multiple selectionsselect, file

Example: Complete Field with Native Attributes

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

PropertyTypeDefaultRequired
modelValueanynullNo

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.

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

Create new record

ModelKey

PropertyTypeDefaultRequired
modelKeystringSame as field keyNo

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

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

Create new record


Request payload

{ "firstName": null }

Errors

PropertyTypeDefaultRequired
errorsstring[][]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

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

Create new record


Rules

PropertyTypeDefaultRequired
rules(value: any) => string | true | unknownundefinedNo

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

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

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

PropertyTypeDefaultRequired
optionsany[]undefinedNo
optionLabelstringundefinedNo
optionValuestringundefinedNo

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

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

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

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

Create new record


URL

PropertyTypeDefaultRequired
urlstringundefinedNo

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

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

json
[
  { "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:

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

Create new record


DebounceTime

PropertyTypeDefaultRequired
debounceTimenumber0No

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

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

PropertyTypeDefaultRequired
createOnlybooleanfalseNo

Display the field only when the form is in create mode. Useful for fields that should only appear when creating new records.

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

PropertyTypeDefaultRequired
updateOnlybooleanfalseNo

Display the field only when the form is in update mode. Useful for fields that should only appear when editing existing records.

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

PropertyTypeDefaultRequired
hiddenbooleanfalseNo

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.

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

PropertyTypeDefaultRequired
excludebooleanfalseNo

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.

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

PropertyTypeDefaultRequired
multiplebooleanfalseNo

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.

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

PropertyTypeDefaultRequired
wrapperobject{}No

Pass additional attributes to the field's wrapper component. This is particularly useful for applying custom styling classes or wrapper-specific props.

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

PropertyTypeDefaultRequired
recordValue(record: any) => unknownundefinedNo

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

ts
// Backend returns this structure:
{
  id: 1,
  name: 'John Doe',
  employee: {
    id: 42,
    name: 'Samira Gonzales',
    department: 'Engineering'
  }
}
vue
<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

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

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

PropertyTypeDefaultRequired
interceptOptions(options: any[]) => unknown[]undefinedNo

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

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

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

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

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

PropertyTypeDefaultRequired
parseModelValue(value: any) => unknownundefinedNo

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

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

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

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

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

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

EventWhen TriggeredUse Case
onFocusField gains focusShow hints, load related data
onBlurField loses focusValidate, auto-format, save draft
onChangeValue changesTrack changes, dependent fields
onClickField is clickedCustom interactions
onInputUser typesReal-time validation, character count
onKeydownKey is pressedKeyboard shortcuts, input restrictions
onKeyupKey is releasedSearch suggestions, auto-complete

Basic Event Handling

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

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

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

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

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

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

vue
<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 value
  • errors - Array of error messages
  • options - List of available options (for select/radio/checkbox)
  • hidden - Visibility state
  • disabled - Disabled state
  • Any custom properties you add

Using Computed Properties

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

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

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

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

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

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

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

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