1513 lines
37 KiB
Markdown
1513 lines
37 KiB
Markdown
````markdown
|
|
# 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
|
|
|
|
```vue
|
|
<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.
|
|
|
|
```javascript
|
|
{
|
|
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.
|
|
|
|
```javascript
|
|
{
|
|
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.
|
|
|
|
```javascript
|
|
{
|
|
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.
|
|
|
|
```javascript
|
|
{
|
|
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.
|
|
|
|
```javascript
|
|
// 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.
|
|
|
|
```javascript
|
|
{
|
|
name: 'subscribe',
|
|
label: 'Subscribe to newsletter',
|
|
type: 'checkbox',
|
|
defaultValue: false
|
|
}
|
|
```
|
|
|
|
### Radio Group (`type: 'radio'`)
|
|
|
|
Radio button group for single selection from multiple options.
|
|
|
|
```javascript
|
|
{
|
|
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.
|
|
|
|
```javascript
|
|
// 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:**
|
|
|
|
```javascript
|
|
// 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.
|
|
|
|
```javascript
|
|
// 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.
|
|
|
|
```javascript
|
|
{
|
|
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:
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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>
|
|
```
|
|
|
|
```vue
|
|
|
|
```
|
|
|
|
### User Registration Form
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```javascript
|
|
{
|
|
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:
|
|
|
|
```javascript
|
|
{
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// OLD (Vuetify):
|
|
@update:model-value="handleChange"
|
|
|
|
// NEW (PrimeVue):
|
|
@update:model-value="handleChange" // Same!
|
|
// But also enhanced with better event data
|
|
```
|
|
|
|
#### Styling Changes
|
|
|
|
```css
|
|
/* 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
|
|
````
|