2025-11-07 12:51:12 -06:00

37 KiB

Form Component Documentation

Overview

A highly flexible and configurable dynamic form component built with PrimeVue. This component generates forms based on field configuration objects and supports various input types including AutoComplete with custom values, validation, responsive layouts, and both controlled and uncontrolled form state management.

New Features (PrimeVue Migration)

  • AutoComplete component - Users can select from suggestions OR enter completely custom values
  • Enhanced Date/Time Pickers - Comprehensive date handling with multiple formats, time selection, constraints, and smart defaults
  • Better accessibility with ARIA support
  • More flexible styling with CSS custom properties
  • Enhanced mobile responsiveness with CSS Grid

Basic Usage

<template>
  <Form
    :fields="formFields"
    :form-data="formData"
    @submit="handleSubmit"
    @change="handleFieldChange"
  />
</template>

<script setup>
import { ref } from "vue";
import Form from "./components/common/Form.vue";

const formData = ref({});

const formFields = [
  {
    name: "firstName",
    label: "First Name",
    type: "text",
    required: true,
    cols: 6,
  },
  {
    name: "email",
    label: "Email",
    type: "text",
    format: "email",
    required: true,
    cols: 6,
  },
  // NEW: AutoComplete with custom values!
  {
    name: "country",
    label: "Country",
    type: "autocomplete",
    placeholder: "Type or select a country",
    options: [
      { label: "United States", value: "US" },
      { label: "Canada", value: "CA" },
      { label: "United Kingdom", value: "UK" },
    ],
    optionLabel: "label",
    optionValue: "value",
    forceSelection: false, // Allows custom entries!
    cols: 6,
    helpText: "You can select from the list or enter a custom country",
  },
  {
    name: "bio",
    label: "Biography",
    type: "textarea",
    rows: 4,
    cols: 12,
  },
];

const handleSubmit = (data) => {
  console.log("Form submitted:", data);
};

const handleFieldChange = (event) => {
  console.log("Field changed:", event.fieldName, event.value);
};
</script>

Props

fields (Array) - Required

  • Description: Array of field configuration objects that define the form structure
  • Type: Array<Object>
  • Required: true

formData (Object)

  • Description: External form data object for controlled form state
  • Type: Object
  • Default: null
  • Note: When provided, the form operates in controlled mode. When null, uses internal state.

onChange (Function)

  • Description: Global change handler function called when any field changes
  • Type: Function
  • Signature: (fieldName: string, value: any, formData: Object) => void

onSubmit (Function)

  • Description: Submit handler function called when form is submitted
  • Type: Function
  • Signature: (formData: Object) => Promise<void> | void

showSubmitButton (Boolean)

  • Description: Controls visibility of the submit button
  • Type: Boolean
  • Default: true

showCancelButton (Boolean)

  • Description: Controls visibility of the cancel button
  • Type: Boolean
  • Default: false

submitButtonText (String)

  • Description: Text displayed on the submit button
  • Type: String
  • Default: 'Submit'

cancelButtonText (String)

  • Description: Text displayed on the cancel button
  • Type: String
  • Default: 'Cancel'

validateOnChange (Boolean)

  • Description: Enables real-time validation as fields change
  • Type: Boolean
  • Default: true

Field Configuration

Each field object in the fields array supports the following properties:

Basic Properties

  • name (String, required) - Unique identifier for the field
  • label (String, required) - Display label for the field
  • type (String, required) - Field input type
  • required (Boolean, default: false) - Makes the field mandatory
  • disabled (Boolean, default: false) - Disables the field
  • readonly (Boolean, default: false) - Makes the field read-only
  • placeholder (String) - Placeholder text for input fields
  • helpText (String) - Help text displayed below the field
  • defaultValue (Any) - Initial value for the field

Layout Properties

  • cols (Number, default: 12) - Column width on extra small screens
  • sm (Number, default: 12) - Column width on small screens
  • md (Number, default: 6) - Column width on medium screens
  • lg (Number, default: 6) - Column width on large screens

Validation Properties

  • validate (Function) - Custom validation function
    • Signature: (value: any) => string | null
    • Returns: Error message string or null if valid

Field-Specific Properties

  • onChangeOverride (Function) - Field-specific change handler that overrides global onChange
    • Signature: (value: any, fieldName: string, formData: Object) => void

Field Types

Text Input (type: 'text')

Standard text input field with optional format validation.

{
  name: 'username',
  label: 'Username',
  type: 'text',
  required: true,
  placeholder: 'Enter your username'
}

Additional Properties:

  • format (String) - Input format validation ('email' for email validation)

Number Input (type: 'number')

Numeric input field with optional min/max constraints.

{
  name: 'age',
  label: 'Age',
  type: 'number',
  min: 0,
  max: 120,
  step: 1
}

Additional Properties:

  • min (Number) - Minimum allowed value
  • max (Number) - Maximum allowed value
  • step (Number) - Step increment for the input

Textarea (type: 'textarea')

Multi-line text input for longer content.

{
  name: 'description',
  label: 'Description',
  type: 'textarea',
  rows: 4,
  placeholder: 'Enter description...'
}

Additional Properties:

  • rows (Number, default: 3) - Number of visible text lines

Select Dropdown (type: 'select')

Dropdown selection field with predefined options and built-in filtering.

{
  name: 'country',
  label: 'Country',
  type: 'select',
  required: true,
  options: [
    { label: 'United States', value: 'us' },
    { label: 'Canada', value: 'ca' },
    { label: 'Mexico', value: 'mx' }
  ],
  filter: true,  // Enable search filtering
  showClear: true  // Allow clearing selection
}

Additional Properties:

  • options (Array, required) - Array of option objects with label and value properties
  • optionLabel (String, default: 'label') - Property name for display text
  • optionValue (String, default: 'value') - Property name for value
  • filter (Boolean, default: true) - Enable filtering/search
  • showClear (Boolean, default: true) - Show clear button

AutoComplete (type: 'autocomplete') - NEW!

The game-changer! AutoComplete field that allows users to select from suggestions OR enter completely custom values not in the predefined list.

// Basic AutoComplete - allows custom entries
{
  name: 'skills',
  label: 'Skills',
  type: 'autocomplete',
  placeholder: 'Add your skills',
  options: [
    { label: 'JavaScript', value: 'js' },
    { label: 'Python', value: 'python' },
    { label: 'Vue.js', value: 'vue' }
  ],
  optionLabel: 'label',
  optionValue: 'value',
  forceSelection: false,  // KEY: Allows custom values!
  multiple: true,         // Multiple selection
  helpText: 'Select existing skills or add your own custom skills'
}

// Strict selection (only predefined options)
{
  name: 'department',
  label: 'Department',
  type: 'autocomplete',
  options: [
    { label: 'Engineering', value: 'eng' },
    { label: 'Marketing', value: 'mkt' },
    { label: 'Sales', value: 'sales' }
  ],
  forceSelection: true,   // Only allows predefined options
  dropdown: true,         // Show dropdown button
  required: true
}

// Dynamic/Remote data loading
{
  name: 'users',
  label: 'Select Users',
  type: 'autocomplete',
  options: [],  // Initially empty
  onSearch: async (query, callback) => {
    if (query.length > 2) {
      const response = await fetch(`/api/users?search=${query}`);
      const users = await response.json();
      callback(users);  // Update options dynamically
    }
  }
}

Additional Properties:

  • options (Array) - Array of suggestion objects
  • optionLabel (String, default: 'label') - Property for display text
  • optionValue (String, default: 'value') - Property for value
  • forceSelection (Boolean, default: false) - If true, only allows predefined options. If false, allows custom values!
  • multiple (Boolean, default: false) - Allow multiple selections
  • dropdown (Boolean, default: true) - Show dropdown button
  • onSearch (Function) - Custom search function for dynamic data loading
    • Signature: (query: string, callback: Function) => void

Checkbox (type: 'checkbox')

Boolean checkbox input.

{
  name: 'subscribe',
  label: 'Subscribe to newsletter',
  type: 'checkbox',
  defaultValue: false
}

Radio Group (type: 'radio')

Radio button group for single selection from multiple options.

{
  name: 'gender',
  label: 'Gender',
  type: 'radio',
  required: true,
  options: [
    { label: 'Male', value: 'male' },
    { label: 'Female', value: 'female' },
    { label: 'Other', value: 'other' }
  ]
}

Additional Properties:

  • options (Array, required) - Array of option objects with label and value properties

Date Input (type: 'date')

Enhanced date picker input field with comprehensive formatting and configuration options.

// Basic date input
{
  name: 'birthDate',
  label: 'Birth Date',
  type: 'date',
  required: true,
}

// Date with custom format
{
  name: 'eventDate',
  label: 'Event Date',
  type: 'date',
  format: 'YYYY-MM-DD',  // or 'mm/dd/yyyy', 'dd/mm/yyyy', etc.
  required: true,
  placeholder: 'Select event date'
}

// Date with time picker
{
  name: 'appointmentDateTime',
  label: 'Appointment Date & Time',
  type: 'date',
  showTime: true,
  hourFormat: '12',  // '12' or '24'
  required: true,
  defaultToNow: true,  // Set to current date/time by default
}

// Time-only picker
{
  name: 'preferredTime',
  label: 'Preferred Time',
  type: 'date',
  timeOnly: true,
  hourFormat: '12',
  stepMinute: 15,  // 15-minute intervals
  defaultValue: 'now'
}

// Advanced date configuration
{
  name: 'projectDeadline',
  label: 'Project Deadline',
  type: 'date',
  format: 'dd/mm/yyyy',
  minDate: 'today',  // Can't select past dates
  maxDate: '2025-12-31',  // Maximum date
  defaultToToday: true,
  showButtonBar: true,
  yearNavigator: true,
  monthNavigator: true,
  yearRange: '2024:2030',
  helpText: 'Select a deadline for the project completion'
}

// Inline date picker (always visible)
{
  name: 'calendarDate',
  label: 'Calendar',
  type: 'date',
  inline: true,
  view: 'date',  // 'date', 'month', 'year'
  showWeek: true,
  defaultValue: 'today'
}

Additional Properties:

  • format (String) - Date format: 'YYYY-MM-DD', 'mm/dd/yyyy', 'dd/mm/yyyy', 'dd-mm-yyyy', 'mm-dd-yyyy'
  • dateFormat (String) - PrimeVue-specific format string (overrides format)
  • showTime (Boolean, default: false) - Include time picker
  • timeOnly (Boolean, default: false) - Show only time picker (no date)
  • hourFormat (String, default: '24') - Hour format: '12' or '24'
  • stepHour (Number, default: 1) - Hour step increment
  • stepMinute (Number, default: 1) - Minute step increment
  • showSeconds (Boolean, default: false) - Show seconds in time picker
  • stepSecond (Number, default: 1) - Second step increment
  • minDate (String|Date) - Minimum selectable date
  • maxDate (String|Date) - Maximum selectable date
  • defaultToToday (Boolean, default: false) - Set default to today's date
  • defaultToNow (Boolean, default: false) - Set default to current date/time
  • showButtonBar (Boolean, default: true) - Show today/clear buttons
  • todayButtonLabel (String, default: 'Today') - Today button text
  • clearButtonLabel (String, default: 'Clear') - Clear button text
  • showWeek (Boolean, default: false) - Show week numbers
  • manualInput (Boolean, default: true) - Allow manual date entry
  • yearNavigator (Boolean, default: false) - Show year dropdown
  • monthNavigator (Boolean, default: false) - Show month dropdown
  • yearRange (String, default: '1900:2100') - Available year range
  • inline (Boolean, default: false) - Display picker inline (always visible)
  • view (String, default: 'date') - Default view: 'date', 'month', 'year'
  • touchUI (Boolean, default: false) - Optimize for touch devices
  • onDateChange (Function) - Custom date change handler
    • Signature: (dateValue: Date) => any

Default Value Options:

// String values
defaultValue: "today"; // Set to today's date
defaultValue: "now"; // Set to current date/time
defaultValue: "2024-12-25"; // Specific date string

// Boolean flags
defaultToToday: true; // Set to today (date only)
defaultToNow: true; // Set to current date/time

// Date object
defaultValue: new Date(); // Specific Date object

DateTime Input (type: 'datetime') - LEGACY

⚠️ DEPRECATED: Use type: 'date' with showTime: true instead.

Legacy date and time picker input field. This is maintained for backward compatibility.

// LEGACY - Use date with showTime instead
{
  name: 'appointmentTime',
  label: 'Appointment Time',
  type: 'datetime',
  required: true
}

// RECOMMENDED - Use this instead
{
  name: 'appointmentTime',
  label: 'Appointment Time',
  type: 'date',
  showTime: true,
  required: true
}

Additional Properties:

  • min (String) - Minimum allowed datetime
  • max (String) - Maximum allowed datetime
  • hourFormat (String, default: '24') - Hour format: '12' or '24'

File Input (type: 'file')

File upload input field.

{
  name: 'resume',
  label: 'Resume',
  type: 'file',
  accept: '.pdf,.doc,.docx',
  multiple: false
}

Additional Properties:

  • accept (String) - File types to accept (MIME types or file extensions)
  • multiple (Boolean, default: false) - Allow multiple file selection

Events

update:formData

  • Description: Emitted when form data changes (controlled mode only)
  • Payload: Updated form data object
  • Usage: @update:formData="handleFormDataUpdate"

submit

  • Description: Emitted when form is successfully submitted
  • Payload: Form data object
  • Usage: @submit="handleSubmit"

cancel

  • Description: Emitted when cancel button is clicked
  • Usage: @cancel="handleCancel"

change

  • Description: Emitted when any field value changes
  • Payload: Object with fieldName, value, and formData properties
  • Usage: @change="handleFieldChange"

Exposed Methods

The component exposes several methods that can be accessed via template refs:

<template>
  <Form ref="formRef" :fields="fields" />
</template>

<script setup>
import { ref } from "vue";

const formRef = ref(null);

// Access exposed methods
const validateForm = () => formRef.value.validateForm();
const resetForm = () => formRef.value.resetForm();
</script>

validateForm()

  • Description: Validates the entire form and returns validation status
  • Returns: Boolean - true if valid, false if invalid
  • Side Effect: Updates form error state

getCurrentFormData()

  • Description: Gets the current form data object
  • Returns: Object - Current form data

resetForm()

  • Description: Resets form to initial state and clears all errors
  • Returns: void

setFieldError(fieldName, error)

  • Description: Sets an error message for a specific field
  • Parameters:
    • fieldName (String) - The field name
    • error (String) - Error message to display

clearFieldError(fieldName)

  • Description: Clears the error for a specific field
  • Parameters:
    • fieldName (String) - The field name to clear

Usage Examples

Basic Contact Form

<script setup>
import { ref } from "vue";

const formData = ref({});

const contactFields = [
  {
    name: "firstName",
    label: "First Name",
    type: "text",
    required: true,
    cols: 12,
    md: 6,
  },
  {
    name: "lastName",
    label: "Last Name",
    type: "text",
    required: true,
    cols: 12,
    md: 6,
  },
  {
    name: "email",
    label: "Email",
    type: "text",
    format: "email",
    required: true,
    cols: 12,
    md: 6,
  },
  {
    name: "preferredContactDate",
    label: "Preferred Contact Date",
    type: "date",
    format: "mm/dd/yyyy",
    minDate: "today",
    defaultToToday: true,
    cols: 12,
    md: 6,
    helpText: "When would you like us to contact you?",
  },
  {
    name: "message",
    label: "Message",
    type: "textarea",
    rows: 5,
    required: true,
    cols: 12,
  },
];

const handleSubmit = async (data) => {
  try {
    await sendContactForm(data);
    alert("Form submitted successfully!");
  } catch (error) {
    console.error("Submission failed:", error);
  }
};
</script>

<template>
  <Form
    :fields="contactFields"
    v-model:form-data="formData"
    @submit="handleSubmit"
    submit-button-text="Send Message"
  />
</template>

AutoComplete Examples

<script setup>
import { ref } from "vue";

const formData = ref({});

// Sample data
const countries = [
  { label: "United States", value: "US" },
  { label: "Canada", value: "CA" },
  { label: "United Kingdom", value: "UK" },
  { label: "Germany", value: "DE" },
  { label: "France", value: "FR" },
];

const skills = [
  { label: "JavaScript", value: "javascript" },
  { label: "Python", value: "python" },
  { label: "Vue.js", value: "vue" },
  { label: "React", value: "react" },
  { label: "Node.js", value: "node" },
];

const autoCompleteFields = [
  // Basic AutoComplete - allows custom countries
  {
    name: "country",
    label: "Country",
    type: "autocomplete",
    placeholder: "Type or select a country",
    options: countries,
    optionLabel: "label",
    optionValue: "value",
    forceSelection: false, // Users can enter "Custom Country"
    required: true,
    cols: 6,
    helpText: "Select from list or enter any country name",
  },

  // Multiple selection with custom values
  {
    name: "skills",
    label: "Technical Skills",
    type: "autocomplete",
    placeholder: "Add your skills",
    options: skills,
    optionLabel: "label",
    optionValue: "value",
    multiple: true,
    forceSelection: false, // Users can add custom skills like "Machine Learning"
    cols: 6,
    helpText: "Select existing skills or add custom ones",
  },

  // Strict selection - only predefined options
  {
    name: "department",
    label: "Department",
    type: "autocomplete",
    options: [
      { label: "Engineering", value: "eng" },
      { label: "Marketing", value: "mkt" },
      { label: "Sales", value: "sales" },
      { label: "HR", value: "hr" },
    ],
    forceSelection: true, // Only allows company departments
    dropdown: true,
    required: true,
    cols: 6,
  },

  // Dynamic search with API calls
  {
    name: "users",
    label: "Assign to User",
    type: "autocomplete",
    placeholder: "Search users...",
    options: [],
    optionLabel: "name",
    optionValue: "id",
    onSearch: async (query, callback) => {
      if (query.length > 2) {
        try {
          const response = await fetch(`/api/users?search=${query}`);
          const users = await response.json();
          callback(users);
        } catch (error) {
          console.error("Search failed:", error);
          callback([]);
        }
      } else {
        callback([]);
      }
    },
    cols: 6,
    helpText: "Type at least 3 characters to search",
  },
];
</script>

<template>
  <Form
    :fields="autoCompleteFields"
    v-model:form-data="formData"
    @submit="handleSubmit"
    submit-button-text="Save"
  />

  <!-- Show current values -->
  <div v-if="Object.keys(formData).length" class="mt-4">
    <h3>Current Values:</h3>
    <pre>{{ JSON.stringify(formData, null, 2) }}</pre>
  </div>
</template>

Enhanced Date Picker Examples

<script setup>
import { ref } from "vue";

const formData = ref({});

const dateFields = [
  // Basic date picker
  {
    name: "birthDate",
    label: "Birth Date",
    type: "date",
    format: "mm/dd/yyyy",
    required: true,
    cols: 6,
  },

  // Date with time - appointment scheduling
  {
    name: "appointmentDateTime",
    label: "Appointment Date & Time",
    type: "date",
    showTime: true,
    hourFormat: "12",
    stepMinute: 15,
    defaultToNow: true,
    minDate: "today",
    cols: 6,
    helpText: "Select appointment date and time",
  },

  // Project deadline with constraints
  {
    name: "projectDeadline",
    label: "Project Deadline",
    type: "date",
    format: "YYYY-MM-DD",
    minDate: "today",
    maxDate: "2025-12-31",
    defaultToToday: true,
    yearNavigator: true,
    monthNavigator: true,
    required: true,
    cols: 6,
    helpText: "Deadline cannot be in the past",
  },

  // Time-only picker for preferences
  {
    name: "preferredTime",
    label: "Preferred Contact Time",
    type: "date",
    timeOnly: true,
    hourFormat: "12",
    stepMinute: 30,
    defaultValue: "09:00",
    cols: 6,
    helpText: "Best time to contact you",
  },

  // Event date with week numbers
  {
    name: "eventDate",
    label: "Event Date",
    type: "date",
    format: "dd/mm/yyyy",
    showWeek: true,
    yearNavigator: true,
    monthNavigator: true,
    yearRange: "2024:2026",
    cols: 6,
  },

  // Inline calendar for scheduling
  {
    name: "availableDate",
    label: "Available Dates",
    type: "date",
    inline: true,
    multiple: true, // Note: This would require custom implementation
    showButtonBar: true,
    cols: 12,
    helpText: "Click dates when you are available",
  },
];

const handleSubmit = (data) => {
  console.log("Date form submitted:", data);

  // Example of working with date values
  if (data.appointmentDateTime) {
    console.log(
      "Appointment scheduled for:",
      data.appointmentDateTime.toLocaleString(),
    );
  }

  if (data.projectDeadline) {
    const deadline = new Date(data.projectDeadline);
    const daysUntilDeadline = Math.ceil(
      (deadline - new Date()) / (1000 * 60 * 60 * 24),
    );
    console.log(`Days until deadline: ${daysUntilDeadline}`);
  }
};
</script>

<template>
  <Form
    :fields="dateFields"
    v-model:form-data="formData"
    @submit="handleSubmit"
    submit-button-text="Schedule"
  />

  <!-- Display current values -->
  <div v-if="Object.keys(formData).length" class="mt-4">
    <h3>Selected Dates:</h3>
    <div v-for="(value, key) in formData" :key="key" class="mb-2">
      <strong>{{ key }}:</strong>
      <span v-if="value instanceof Date">
        {{ value.toLocaleString() }}
      </span>
      <span v-else>{{ value }}</span>
    </div>
  </div>
</template>

Real-World Date Picker Scenarios

<script setup>
import { ref } from "vue";

const bookingData = ref({});

// Hotel booking form with date constraints
const hotelBookingFields = [
  {
    name: "checkIn",
    label: "Check-in Date",
    type: "date",
    format: "mm/dd/yyyy",
    minDate: "today",
    defaultToToday: true,
    required: true,
    cols: 6,
    onDateChange: (date) => {
      // Auto-update checkout date to be at least 1 day later
      if (bookingData.value.checkOut && bookingData.value.checkOut <= date) {
        const nextDay = new Date(date);
        nextDay.setDate(nextDay.getDate() + 1);
        bookingData.value.checkOut = nextDay;
      }
      return date;
    },
  },
  {
    name: "checkOut",
    label: "Check-out Date",
    type: "date",
    format: "mm/dd/yyyy",
    minDate: "today",
    required: true,
    cols: 6,
    validate: (value) => {
      if (
        value &&
        bookingData.value.checkIn &&
        value <= bookingData.value.checkIn
      ) {
        return "Check-out date must be after check-in date";
      }
      return null;
    },
  },
];

// Meeting scheduler with business hours
const meetingFields = [
  {
    name: "meetingDate",
    label: "Meeting Date",
    type: "date",
    format: "dd/mm/yyyy",
    minDate: "today",
    // Exclude weekends
    validate: (value) => {
      if (value) {
        const day = value.getDay();
        if (day === 0 || day === 6) {
          return "Please select a weekday";
        }
      }
      return null;
    },
    cols: 6,
  },
  {
    name: "meetingTime",
    label: "Meeting Time",
    type: "date",
    timeOnly: true,
    hourFormat: "12",
    stepMinute: 30,
    // Business hours only: 9 AM to 5 PM
    validate: (value) => {
      if (value) {
        const hours = value.getHours();
        if (hours < 9 || hours >= 17) {
          return "Please select a time between 9:00 AM and 5:00 PM";
        }
      }
      return null;
    },
    cols: 6,
  },
];

// Birth date with age calculation
const personalInfoFields = [
  {
    name: "birthDate",
    label: "Date of Birth",
    type: "date",
    format: "mm/dd/yyyy",
    maxDate: "today",
    yearNavigator: true,
    yearRange: "1920:2010",
    required: true,
    onDateChange: (date) => {
      // Calculate and display age
      if (date) {
        const today = new Date();
        const age = today.getFullYear() - date.getFullYear();
        const monthDiff = today.getMonth() - date.getMonth();

        if (
          monthDiff < 0 ||
          (monthDiff === 0 && today.getDate() < date.getDate())
        ) {
          age--;
        }

        console.log(`Age: ${age} years`);
      }
      return date;
    },
    cols: 12,
  },
];
</script>

<template>
  <div>
    <h3>Hotel Booking</h3>
    <Form :fields="hotelBookingFields" v-model:form-data="bookingData" />

    <h3 class="mt-6">Meeting Scheduler</h3>
    <Form :fields="meetingFields" />

    <h3 class="mt-6">Personal Information</h3>
    <Form :fields="personalInfoFields" />
  </div>
</template>

User Registration Form

<script setup>
import { ref } from "vue";

const registrationData = ref({});

const registrationFields = [
  {
    name: "username",
    label: "Username",
    type: "text",
    required: true,
    validate: (value) => {
      if (value && value.length < 3) {
        return "Username must be at least 3 characters";
      }
      return null;
    },
  },
  {
    name: "email",
    label: "Email",
    type: "text",
    format: "email",
    required: true,
  },
  {
    name: "age",
    label: "Age",
    type: "number",
    min: 18,
    max: 100,
    required: true,
  },
  // UPDATED: Using AutoComplete instead of Select
  {
    name: "country",
    label: "Country",
    type: "autocomplete", // Changed from 'select' to 'autocomplete'
    placeholder: "Type or select your country",
    required: true,
    options: [
      { label: "United States", value: "us" },
      { label: "Canada", value: "ca" },
      { label: "United Kingdom", value: "uk" },
      { label: "Australia", value: "au" },
      { label: "Germany", value: "de" },
    ],
    optionLabel: "label",
    optionValue: "value",
    forceSelection: false, // Allows users to enter unlisted countries
    helpText: "Select from common countries or type your own",
  },
  {
    name: "terms",
    label: "I agree to the terms and conditions",
    type: "checkbox",
    required: true,
    validate: (value) => {
      if (!value) {
        return "You must agree to the terms and conditions";
      }
      return null;
    },
  },
];
</script>

<template>
  <Form
    :fields="registrationFields"
    v-model:form-data="registrationData"
    @submit="handleRegistration"
    submit-button-text="Register"
    show-cancel-button
    @cancel="handleCancel"
  />
</template>

Survey Form with Custom Validation

<script setup>
import { ref } from "vue";

const surveyData = ref({});

const surveyFields = [
  {
    name: "satisfaction",
    label: "How satisfied are you with our service?",
    type: "radio",
    required: true,
    options: [
      { label: "Very Satisfied", value: "5" },
      { label: "Satisfied", value: "4" },
      { label: "Neutral", value: "3" },
      { label: "Dissatisfied", value: "2" },
      { label: "Very Dissatisfied", value: "1" },
    ],
    cols: 12,
  },
  {
    name: "feedback",
    label: "Additional Feedback",
    type: "textarea",
    rows: 4,
    placeholder: "Please share your thoughts...",
    cols: 12,
    onChangeOverride: (value, fieldName, formData) => {
      // Custom logic for this field only
      console.log(`Feedback length: ${value?.length || 0} characters`);
    },
  },
  {
    name: "recommend",
    label: "Would you recommend us to others?",
    type: "checkbox",
    cols: 12,
  },
];

const handleSurveySubmit = (data) => {
  console.log("Survey submitted:", data);
};

const handleFieldChange = (event) => {
  console.log("Global change handler:", event);
};
</script>

<template>
  <Form
    :fields="surveyFields"
    v-model:form-data="surveyData"
    @submit="handleSurveySubmit"
    @change="handleFieldChange"
    submit-button-text="Submit Survey"
  />
</template>

File Upload Form

<script setup>
import { ref } from "vue";

const uploadData = ref({});

const uploadFields = [
  {
    name: "title",
    label: "Document Title",
    type: "text",
    required: true,
    cols: 12,
  },
  {
    name: "category",
    label: "Category",
    type: "select",
    required: true,
    options: [
      { label: "Reports", value: "reports" },
      { label: "Presentations", value: "presentations" },
      { label: "Documents", value: "documents" },
    ],
    cols: 12,
    md: 6,
  },
  {
    name: "uploadDate",
    label: "Upload Date",
    type: "date",
    required: true,
    cols: 12,
    md: 6,
  },
  {
    name: "files",
    label: "Select Files",
    type: "file",
    accept: ".pdf,.doc,.docx,.ppt,.pptx",
    multiple: true,
    required: true,
    cols: 12,
  },
  {
    name: "description",
    label: "Description",
    type: "textarea",
    rows: 3,
    helpText: "Optional description of the uploaded files",
    cols: 12,
  },
];
</script>

<template>
  <Form
    :fields="uploadFields"
    v-model:form-data="uploadData"
    @submit="handleFileUpload"
    submit-button-text="Upload Files"
  />
</template>

Form State Management

Controlled Mode (External Form Data)

When you provide a formData prop, the component operates in controlled mode:

<script setup>
import { ref } from "vue";

// External form state
const formData = ref({
  name: "John Doe",
  email: "john@example.com",
});
</script>

<template>
  <Form :fields="fields" v-model:form-data="formData" @submit="handleSubmit" />
</template>

Uncontrolled Mode (Internal Form Data)

When no formData is provided, the component manages its own internal state:

<template>
  <Form :fields="fields" @submit="handleSubmit" />
</template>

<script setup>
const handleSubmit = (data) => {
  // Data is passed to the submit handler
  console.log("Form data:", data);
};
</script>

Validation

Built-in Validation

  • Required fields - Validates that required fields are not empty
  • Email format - Validates email format when format: 'email' is used
  • Number ranges - Validates min/max values for number fields

Custom Validation

Each field can have a custom validation function:

{
  name: 'password',
  label: 'Password',
  type: 'text',
  required: true,
  validate: (value) => {
    if (value && value.length < 8) {
      return 'Password must be at least 8 characters long'
    }
    if (value && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
      return 'Password must contain at least one lowercase letter, one uppercase letter, and one number'
    }
    return null // Valid
  }
}

Validation Timing

  • On change - When validateOnChange is true (default)
  • On submit - Always validates on form submission
  • Manual - Using the exposed validateForm() method

Responsive Layout

The component uses Vuetify's grid system for responsive layouts:

{
  name: 'field',
  label: 'Field',
  type: 'text',
  cols: 12,    // Full width on extra small screens
  sm: 12,      // Full width on small screens
  md: 6,       // Half width on medium screens
  lg: 4        // One-third width on large screens
}

Styling

The component uses Vuetify's design system with:

  • Outlined variants for consistent appearance
  • Comfortable density for optimal spacing
  • Error state styling for validation feedback
  • Required field indicators with red asterisks
  • Responsive design that adapts to screen size

Best Practices

  1. Use meaningful field names that reflect the data structure
  2. Provide clear labels and help text for better user experience
  3. Implement proper validation for data integrity
  4. Consider responsive layout for different screen sizes
  5. Handle form submission errors gracefully
  6. Use controlled mode when form data needs to be managed externally
  7. Leverage custom validation for business-specific rules
  8. Test with various field combinations to ensure proper behavior
  9. Use appropriate field types for better user experience
  10. Provide meaningful default values when appropriate

Accessibility

The component includes:

  • Proper form semantics with native HTML form elements
  • Label associations for screen readers
  • Error message announcements for validation feedback
  • Keyboard navigation support throughout the form
  • Focus management for better usability
  • Required field indicators for clarity

Browser Support

Compatible with all modern browsers that support:

  • Vue 3 Composition API
  • Vuetify 3 components
  • ES6+ features
  • CSS Grid and Flexbox

Migration from Vuetify

The component has been completely migrated from Vuetify to PrimeVue. Here's what you need to know:

What's New

  • AutoComplete component - The star feature! Users can select from suggestions OR enter completely custom values
  • Enhanced Date/Time Pickers - Multiple format support (YYYY-MM-DD, mm/dd/yyyy, etc.), smart defaults (today, now), time-only mode, inline calendars, business hour constraints, and comprehensive validation
  • Better file uploads - Drag & drop, better validation, file preview
  • Improved accessibility - Full ARIA support, better keyboard navigation
  • Flexible styling - CSS custom properties, easier theming
  • Mobile-first responsive - Better grid system, improved mobile UX

🔄 Migration Changes

Layout System

// OLD (Vuetify):
{ cols: 12, sm: 12, md: 6, lg: 6 }

// NEW (CSS Grid): Same properties, better responsive behavior
{ cols: 12, md: 6, lg: 4 }

Component Mapping

Vuetify PrimeVue Notes
v-text-field InputText Same functionality
v-textarea Textarea Added autoResize
v-select Select Added filtering
v-checkbox Checkbox Improved styling
v-radio-group RadioButton Better layout
v-file-input FileUpload Enhanced features
v-btn Button Same functionality
New! AutoComplete 🚀 Custom values support
New! DatePicker Calendar popup

Event Changes

// OLD (Vuetify):
@update:model-value="handleChange"

// NEW (PrimeVue):
@update:model-value="handleChange"  // Same!
// But also enhanced with better event data

Styling Changes

/* OLD: Vuetify classes */
.v-form { ... }
.v-text-field { ... }

/* NEW: Custom CSS with variables */
.dynamic-form { ... }
.field-wrapper { ... }

/* Theming with CSS custom properties */
:root {
  --text-color: #374151;
  --red-500: #ef4444;
}

🚀 Upgrade Benefits

  1. AutoComplete with Custom Values - Users are no longer limited to predefined options
  2. Better Performance - PrimeVue components are more lightweight
  3. Enhanced Accessibility - Full WCAG compliance out of the box
  4. Improved Developer Experience - Better TypeScript support, clearer APIs
  5. Modern Styling - CSS Grid, custom properties, better mobile support
  6. Rich Components - Better date pickers, file uploads, and form controls

📝 Quick Migration Checklist

  • Update imports from Vuetify to PrimeVue components
  • Replace v-select with AutoComplete where custom values are needed
  • Update CSS classes from v-* to custom classes
  • Test responsive layout (should work better!)
  • Enjoy the new AutoComplete functionality! 🎉

Dependencies

  • Vue 3 with Composition API
  • PrimeVue 4.4+ components (InputText, InputNumber, Textarea, Select, AutoComplete, Checkbox, RadioButton, DatePicker, FileUpload, Button, Message)
  • Modern JavaScript features (ES6+)
  • CSS Grid and Flexbox support