HiForm Builder
HiForm Builder Component
Section titled “HiForm Builder Component”A powerful and flexible dynamic form component built with Vue 3, TypeScript, and Zod validation. Create complex forms with 9 field types, conditional logic, responsive layouts, and comprehensive validation.
✨ Key Features
Section titled “✨ Key Features”🔧 9 Field Types
Input, Select, Checkbox, Radio, Switch, Slider, Textarea, Combobox, DatePicker with full customization
📐 Flexible Layouts
1-3 column responsive grids with top/left label positioning and customizable spacing
✅ Zod Validation
Full integration with Zod schema validation for robust type-safe form validation
🎯 Conditional Fields
Show/hide fields based on other field values with flexible operators and logic
🔷 TypeScript First
Complete type safety, IntelliSense support, and auto-completion for all configurations
🎨 Customizable UI
Configurable buttons, loading states, animations, and styling with theme support
🚀 Basic Usage
Section titled “🚀 Basic Usage”<template> <HiForm :fields="formFields" :schema="validationSchema" @submit="handleSubmit" /></template>
<script setup lang="ts">import { z } from 'zod'import HiForm from '@/components/HiForm/HiForm.vue'import type { FormFieldConfig } from '@/components/HiForm/types'
const formFields: FormFieldConfig[] = [ { type: 'input', name: 'name', label: 'Full Name', placeholder: 'Enter your name', required: true, }, { type: 'select', name: 'country', label: 'Country', options: [ { label: 'United States', value: 'us' }, { label: 'Canada', value: 'ca' }, ], }]
const validationSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), country: z.string().min(1, 'Please select a country'),})
const handleSubmit = (data: z.infer<typeof validationSchema>) => { console.log('Form submitted:', data)}</script>
<template> <HiForm :fields="advancedFields" :schema="advancedSchema" :layout="layoutConfig" :buttons="buttonConfig" @submit="handleSubmit" @change="handleChange" /></template>
<script setup lang="ts">import { z } from 'zod'import type { FormLayoutConfig, FormButtonConfig } from '@/components/HiForm/types'
// Advanced validation schemaconst advancedSchema = z.object({ email: z.string().email('Please enter a valid email address'), hasJob: z.boolean(), companyName: z.string().optional()}).refine((data) => { // If has job, company name is required if (data.hasJob && (!data.companyName || data.companyName.trim() === '')) { return false } return true}, { message: 'Company name is required when employed', path: ['companyName']})
const layoutConfig: FormLayoutConfig = { columns: 2, labelPosition: 'top', gap: '1.5rem'}
const buttonConfig: FormButtonConfig = { submitText: 'Create Account', loadingText: 'Creating...', resetText: 'Clear Form', showResetButton: true}
const advancedFields: FormFieldConfig[] = [ { type: 'input', name: 'email', label: 'Email Address', inputType: 'email', required: true }, { type: 'checkbox', name: 'hasJob', text: 'Currently employed' }, { type: 'input', name: 'companyName', label: 'Company Name', placeholder: 'Enter company name', condition: { field: 'hasJob', value: true, operator: 'eq' } }]
const handleSubmit = (data: z.infer<typeof advancedSchema>) => { console.log('Form submitted:', data)}
const handleChange = (data: any) => { console.log('Form data changed:', data)}</script>
🎯 Field Types Reference
Section titled “🎯 Field Types Reference”HiForm supports 9 different field types, each with specific configuration options:
Input Field
Section titled “Input Field”For text, email, password, number inputs:
{ type: 'input', name: 'email', label: 'Email Address', inputType: 'email', // 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' placeholder: 'Enter your email', maxLength: 100, minLength: 5, required: true}
Textarea Field
Section titled “Textarea Field”For multi-line text input:
{ type: 'textarea', name: 'bio', label: 'Biography', placeholder: 'Tell us about yourself...', rows: 4, maxLength: 500, required: true}
Select Field
Section titled “Select Field”For dropdown selections:
{ type: 'select', name: 'category', label: 'Category', placeholder: 'Choose a category', options: [ { label: 'Technology', value: 'tech' }, { label: 'Business', value: 'business' }, { label: 'Design', value: 'design' } ], multiple: false // Enable multi-select}
Combobox Field
Section titled “Combobox Field”For dropdown selection (similar to select but with different styling):
{ type: 'combobox', name: 'skills', label: 'Technical Skills', placeholder: 'Select skills', options: [ { label: 'Vue.js', value: 'vue' }, { label: 'React', value: 'react' }, { label: 'TypeScript', value: 'typescript' }, { label: 'Node.js', value: 'nodejs' }, { label: 'Python', value: 'python' } ]}
Radio Field
Section titled “Radio Field”For single choice selection:
{ type: 'radio', name: 'gender', label: 'Gender', required: true, options: [ { label: 'Male', value: 'male' }, { label: 'Female', value: 'female' }, { label: 'Other', value: 'other' } ]}
Checkbox Field
Section titled “Checkbox Field”For boolean values:
{ type: 'checkbox', name: 'newsletter', text: 'Subscribe to newsletter and updates'}
Switch Field
Section titled “Switch Field”For toggle values:
{ type: 'switch', name: 'notifications', label: 'Push Notifications', text: 'Enable push notifications on your device'}
Slider Field
Section titled “Slider Field”For numeric range selection:
{ type: 'slider', name: 'experience', label: 'Years of Experience', min: 0, max: 30, step: 1, orientation: 'horizontal'}
DatePicker Field
Section titled “DatePicker Field”For date selection:
{ type: 'datepicker', name: 'birthDate', label: 'Birth Date', placeholder: 'Select your birth date', mode: 'single', // 'single' | 'range' format: 'yyyy-MM-dd'}
🔀 Conditional Logic
Section titled “🔀 Conditional Logic”Create dynamic forms that adapt based on user input:
// Show field only when another field has specific value{ type: 'input', name: 'companyName', label: 'Company Name', condition: { field: 'hasJob', value: true, operator: 'eq' // Equal to }}
// Hide field when condition is met{ type: 'input', name: 'reason', label: 'Reason for leaving', condition: { field: 'currentlyEmployed', value: false, operator: 'eq' }}
// Available operators for conditional logicconst operators = { 'eq': 'Equal to', 'ne': 'Not equal to', 'gt': 'Greater than', 'lt': 'Less than', 'in': 'Value in array', 'notIn': 'Value not in array'}
// Examples of different operatorsconst conditionalFields = [ { name: 'seniorRole', condition: { field: 'experience', value: 5, operator: 'gt' // Show when experience > 5 } }, { name: 'internshipProgram', condition: { field: 'experience', value: 2, operator: 'lt' // Show when experience < 2 } }, { name: 'remoteWork', condition: { field: 'location', value: ['remote', 'hybrid'], operator: 'in' // Show when location is remote or hybrid } }]
📐 Layout Configuration
Section titled “📐 Layout Configuration”Customize form layout for optimal user experience:
<template> <!-- Single column (mobile-friendly) --> <HiForm :fields="fields" :layout="{ columns: 1 }" />
<!-- Two columns (tablet/desktop) --> <HiForm :fields="fields" :layout="twoColumnLayout" />
<!-- Three columns (wide screens) --> <HiForm :fields="fields" :layout="threeColumnLayout" /></template>
<script setup lang="ts">const twoColumnLayout = { columns: 2, labelPosition: 'top', gap: '1.5rem'}
const threeColumnLayout = { columns: 3, labelPosition: 'left', gap: '2rem'}</script>
<template> <!-- Labels on top (default) --> <HiForm :fields="fields" :layout="{ columns: 2, labelPosition: 'top' }" />
<!-- Labels on left side --> <HiForm :fields="fields" :layout="{ columns: 2, labelPosition: 'left' }" /></template>
<script setup lang="ts">// Layout affects responsive behaviorconst responsiveLayout = { columns: 2, // Automatically becomes 1 column on mobile labelPosition: 'top', // Ensures mobile readability gap: '1.5rem' // Consistent spacing}</script>
✅ Validation with Zod
Section titled “✅ Validation with Zod”Robust form validation using Zod schemas:
import { z } from 'zod'
// Simple validation schemaconst basicSchema = z.object({ email: z.string().email('Invalid email address'), age: z.number().min(18, 'Must be at least 18'), name: z.string().min(2, 'Name too short').max(50, 'Name too long')})
// Complex validation with transformsconst advancedSchema = z.object({ // Transform string to number age: z.preprocess( (val) => Number(val), z.number().min(18).max(99) ),
// Enum validation role: z.enum(['admin', 'user', 'moderator'], { message: 'Please select a valid role' }),
// Optional fields with defaults bio: z.string().max(500).optional().default(''),
// Custom validation username: z.string().refine( (val) => !val.includes(' '), 'Username cannot contain spaces' )})
// Schema with conditional validationconst conditionalSchema = z.object({ hasJob: z.boolean(), companyName: z.string().optional(), experience: z.number().optional()}).refine((data) => { // If has job, company name is required if (data.hasJob && !data.companyName) { return false } return true}, { message: 'Company name is required when employed', path: ['companyName'] // Show error on specific field})
// Dynamic schema based on form stateconst createDynamicSchema = (formData: any) => { const baseSchema = z.object({ name: z.string().min(2), email: z.string().email() })
// Add conditional fields to schema if (formData.hasJob) { return baseSchema.extend({ companyName: z.string().min(1, 'Company name required'), jobTitle: z.string().min(1, 'Job title required') }) }
return baseSchema}
🏗️ Component Architecture & Development Practices
Section titled “🏗️ Component Architecture & Development Practices”Internal Architecture Analysis
Section titled “Internal Architecture Analysis”The HiForm component follows a composable-first architecture that promotes reusability and maintainability:
The component is built using a composition-based architecture:
src/components/HiForm/├── HiForm.vue # Main component entry point├── composables/│ └── useFormValidation.ts # Form state & validation logic├── components/│ └── FormField.vue # Individual field renderer└── types/ └── index.ts # TypeScript type definitions
Key Design Principles:
- Composable Logic: Form validation and state isolated in
useFormValidation
- Field Abstraction: Universal field renderer supporting 9 field types
- Conditional Rendering: Dynamic field visibility based on form state
- Type Safety: Full TypeScript support with generic form data types
// useFormValidation.ts - Core form state managementexport function useFormValidation(options: UseFormValidationOptions) { const { fields, schema, validateOnChange = true } = options
// Reactive form data state const formData = reactive<FormData>({})
// Validation errors tracking const errors = ref<ValidationError[]>([])
// Touched fields for UX optimization const touched = reactive<Record<string, boolean>>({})
// Form submission state const isSubmitting = ref(false)
// Initialize form data with smart defaults const initializeFormData = () => { fields.forEach((field) => { if (!(field.name in formData)) { let defaultValue: any = ''
switch (field.type) { case 'checkbox': case 'switch': defaultValue = false break case 'select': defaultValue = field.multiple ? [] : '' break case 'slider': defaultValue = field.min || 0 break case 'radio': defaultValue = field.options?.[0]?.value || '' break default: defaultValue = '' }
formData[field.name] = defaultValue } }) }
// Zod validation integration const validateField = (fieldName: string, value: any): ValidationError | null => { if (!schema) return null
try { // Validate single field using Zod const fieldSchema = schema.shape?.[fieldName] if (fieldSchema) { fieldSchema.parse(value) } return null } catch (error) { if (error instanceof z.ZodError) { return { field: fieldName, message: error.errors[0]?.message || 'Validation error' } } return null } }
return { formData, errors, touched, isSubmitting, validateField, // ... other form methods }}
Advanced Form Patterns
Section titled “Advanced Form Patterns”Create forms dynamically based on data schemas or API responses:
// utils/formGenerator.tsimport { z } from 'zod'import type { FormFieldConfig } from '@/components/HiForm/types'
// Generate form fields from Zod schemaexport function generateFieldsFromSchema(schema: z.ZodSchema): FormFieldConfig[] { const fields: FormFieldConfig[] = []
if (schema instanceof z.ZodObject) { const shape = schema.shape
Object.entries(shape).forEach(([fieldName, fieldSchema]) => { const field: FormFieldConfig = { name: fieldName, label: capitalizeLabel(fieldName), type: inferFieldType(fieldSchema), required: !fieldSchema.isOptional(), ...generateFieldProps(fieldSchema) }
fields.push(field) }) }
return fields}
// Smart field type inferencefunction inferFieldType(schema: z.ZodTypeAny): FormFieldConfig['type'] { if (schema instanceof z.ZodString) { // Check for email pattern if (schema._def.checks?.some(check => check.kind === 'email')) { return 'input' // with inputType: 'email' } return 'input' }
if (schema instanceof z.ZodNumber) { return 'input' // with inputType: 'number' }
if (schema instanceof z.ZodBoolean) { return 'switch' }
if (schema instanceof z.ZodEnum) { return 'select' }
return 'input'}
// Usage in componentconst UserSchema = z.object({ name: z.string().min(2), email: z.string().email(), age: z.number().min(18).max(99), role: z.enum(['user', 'admin']), active: z.boolean(), bio: z.string().max(500).optional()})
const dynamicFields = computed(() => generateFieldsFromSchema(UserSchema))
Implement complex multi-step forms with validation at each step:
<template> <div class="space-y-6"> <!-- Step Progress Indicator --> <div class="flex items-center space-x-4"> <div v-for="(step, index) in formSteps" :key="step.id" class="flex items-center" > <div :class="[ 'w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold', index < currentStep ? 'bg-green-500 text-white' : index === currentStep ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500' ]" > {{ index + 1 }} </div> <div v-if="index < formSteps.length - 1" class="w-12 h-0.5 bg-gray-200 mx-2" /> </div> </div>
<!-- Current Step Form --> <BaseCard> <BaseCardHeader> <h2 class="text-xl font-semibold">{{ currentStepConfig.title }}</h2> <p class="text-sm text-muted-foreground">{{ currentStepConfig.description }}</p> </BaseCardHeader> <BaseCardContent> <HiForm ref="currentFormRef" :fields="currentStepConfig.fields" :schema="currentStepConfig.schema" :layout="{ columns: 2 }" :buttons="{ submitText: isLastStep ? 'Complete Registration' : 'Next Step', showResetButton: false }" @submit="handleStepSubmit" />
<!-- Step Navigation --> <div class="flex justify-between mt-6"> <Button v-if="currentStep > 0" @click="goToPreviousStep" variant="outline" > Previous </Button> <div v-else></div>
<Button v-if="!isLastStep" @click="goToNextStep" :disabled="!isCurrentStepValid" > Next </Button> </div> </BaseCardContent> </BaseCard> </div></template>
<script setup lang="ts">import { z } from 'zod'import HiForm from '@/components/HiForm/HiForm.vue'
// Multi-step form configurationconst formSteps = [ { id: 'personal', title: 'Personal Information', description: 'Basic details about yourself', fields: [ { type: 'input', name: 'firstName', label: 'First Name', required: true }, { type: 'input', name: 'lastName', label: 'Last Name', required: true }, { type: 'input', name: 'email', label: 'Email', inputType: 'email', required: true }, { type: 'input', name: 'phone', label: 'Phone Number', inputType: 'tel' } ], schema: z.object({ firstName: z.string().min(2), lastName: z.string().min(2), email: z.string().email(), phone: z.string().optional() }) }, { id: 'professional', title: 'Professional Details', description: 'Your work experience and skills', fields: [ { type: 'input', name: 'jobTitle', label: 'Job Title', required: true }, { type: 'input', name: 'company', label: 'Company Name', required: true }, { type: 'slider', name: 'experience', label: 'Years of Experience', min: 0, max: 50 }, { type: 'combobox', name: 'skills', label: 'Skills', options: skillOptions } ], schema: z.object({ jobTitle: z.string().min(2), company: z.string().min(2), experience: z.number().min(0), skills: z.string() }) }, { id: 'preferences', title: 'Preferences', description: 'Customize your experience', fields: [ { type: 'switch', name: 'newsletter', label: 'Newsletter Subscription' }, { type: 'select', name: 'theme', label: 'Preferred Theme', options: themeOptions }, { type: 'textarea', name: 'notes', label: 'Additional Notes', rows: 4 } ], schema: z.object({ newsletter: z.boolean(), theme: z.string(), notes: z.string().optional() }) }]
const currentStep = ref(0)const stepData = ref<Record<string, any>>({})
const currentStepConfig = computed(() => formSteps[currentStep.value])const isLastStep = computed(() => currentStep.value === formSteps.length - 1)const isCurrentStepValid = computed(() => { // Check if current step data is valid const stepSchema = currentStepConfig.value.schema try { stepSchema.parse(stepData.value[currentStepConfig.value.id] || {}) return true } catch { return false }})
const handleStepSubmit = (data: any) => { // Store step data stepData.value[currentStepConfig.value.id] = data
if (isLastStep.value) { // Final submission const completeFormData = Object.values(stepData.value).reduce((acc, step) => ({ ...acc, ...step }), {}) handleFinalSubmit(completeFormData) } else { goToNextStep() }}
const goToNextStep = () => { if (currentStep.value < formSteps.length - 1) { currentStep.value++ }}
const goToPreviousStep = () => { if (currentStep.value > 0) { currentStep.value-- }}
const handleFinalSubmit = async (formData: any) => { try { await submitRegistration(formData) toast.success('Registration completed successfully!') router.push('/dashboard') } catch (error) { toast.error('Registration failed. Please try again.') }}</script>
Persist form data across sessions and page reloads:
// composables/useFormPersistence.tsimport { watch, onMounted, onUnmounted } from 'vue'
export function useFormPersistence( formData: Record<string, any>, formId: string, options: { storage?: 'localStorage' | 'sessionStorage' debounceMs?: number autoSave?: boolean } = {}) { const { storage = 'localStorage', debounceMs = 1000, autoSave = true } = options
const storageKey = `form-data-${formId}` const storageAPI = storage === 'localStorage' ? localStorage : sessionStorage
// Save form data to storage const saveFormData = useDebounceFn(() => { try { const dataToSave = { data: formData, timestamp: Date.now(), version: '1.0' } storageAPI.setItem(storageKey, JSON.stringify(dataToSave)) } catch (error) { console.warn('Failed to save form data:', error) } }, debounceMs)
// Load form data from storage const loadFormData = (): any => { try { const savedData = storageAPI.getItem(storageKey) if (savedData) { const parsed = JSON.parse(savedData)
// Check if data is not too old (optional: 7 days) const isRecent = Date.now() - parsed.timestamp < 7 * 24 * 60 * 60 * 1000
if (isRecent && parsed.data) { return parsed.data } } } catch (error) { console.warn('Failed to load form data:', error) } return null }
// Clear saved form data const clearSavedData = () => { try { storageAPI.removeItem(storageKey) } catch (error) { console.warn('Failed to clear form data:', error) } }
// Auto-save on data changes if (autoSave) { watch( () => formData, () => saveFormData(), { deep: true } ) }
// Load data on mount onMounted(() => { const savedData = loadFormData() if (savedData) { Object.assign(formData, savedData) } })
// Cleanup on unmount onUnmounted(() => { if (autoSave) { saveFormData.cancel() } })
return { saveFormData: saveFormData.trigger, loadFormData, clearSavedData }}
// Usage in componentexport default { setup() { const formData = reactive({ name: '', email: '', message: '' })
// Enable form persistence const { clearSavedData } = useFormPersistence(formData, 'contact-form', { storage: 'localStorage', debounceMs: 500, autoSave: true })
const handleSubmit = async (data: any) => { try { await submitForm(data) // Clear saved data after successful submission clearSavedData() toast.success('Form submitted successfully!') } catch (error) { toast.error('Submission failed') } }
return { formData, handleSubmit } }}
Performance Optimization Strategies
Section titled “Performance Optimization Strategies”// Optimize field rendering for large forms<script setup lang="ts">// ✅ Good: Memoize field configurationsconst memoizedFields = computed(() => { return shallowRef(baseFields.map(field => ({ ...field, // Pre-compute expensive field properties validationRules: compileValidationRules(field.validation), computedOptions: field.type === 'select' ? processSelectOptions(field.options) : undefined })))})
// ✅ Good: Use virtual scrolling for many fieldsconst visibleFields = computed(() => { if (formFields.value.length > 50) { // Implement virtual scrolling or pagination const startIndex = currentPage.value * pageSize.value const endIndex = startIndex + pageSize.value return formFields.value.slice(startIndex, endIndex) } return formFields.value})
// ✅ Good: Lazy load field optionsconst loadFieldOptions = async (fieldName: string) => { if (fieldOptionsCache.has(fieldName)) { return fieldOptionsCache.get(fieldName) }
const options = await fetchFieldOptions(fieldName) fieldOptionsCache.set(fieldName, options) return options}
// ✅ Good: Debounce validation for better UXconst debouncedValidation = useDebounceFn(async (fieldName: string, value: any) => { // Perform expensive validation const isValid = await validateFieldAsync(fieldName, value) updateFieldValidation(fieldName, isValid)}, 300)
// ❌ Avoid: Heavy computations in field watchers// watch(() => formData.complexField, (newValue) => {// // Heavy computation on every change// const result = heavyCalculation(newValue)// })
// ✅ Better: Use computed with proper memoizationconst computedFieldValue = computed(() => { const cachedKey = getCacheKey(formData.inputField) if (computationCache.has(cachedKey)) { return computationCache.get(cachedKey) }
const result = heavyCalculation(formData.inputField) computationCache.set(cachedKey, result) return result})</script>
// Optimize form validation performanceimport { z } from 'zod'
// ✅ Good: Create validation schemas outside componentconst UserValidationSchema = z.object({ email: z.string().email(), password: z.string().min(8), confirmPassword: z.string()}).refine((data) => data.password === data.confirmPassword, { message: "Passwords don't match", path: ["confirmPassword"]})
// ✅ Good: Implement smart validation timingexport function useSmartValidation(schema: z.ZodSchema, options = {}) { const { validateOnMount = false, validateOnChange = true, validateOnBlur = true, debounceMs = 300 } = options
const errors = ref<Record<string, string>>({}) const isValidating = ref(false)
// Validate single field with debouncing const validateField = useDebounceFn(async (fieldName: string, value: any) => { if (!schema.shape || !schema.shape[fieldName]) return
try { isValidating.value = true // Validate only the specific field await schema.shape[fieldName].parseAsync(value)
// Clear error if validation passes if (errors.value[fieldName]) { delete errors.value[fieldName] errors.value = { ...errors.value } } } catch (error) { if (error instanceof z.ZodError) { errors.value = { ...errors.value, [fieldName]: error.errors[0]?.message || 'Validation error' } } } finally { isValidating.value = false } }, debounceMs)
// Validate entire form const validateForm = async (formData: any): Promise<boolean> => { try { isValidating.value = true await schema.parseAsync(formData) errors.value = {} return true } catch (error) { if (error instanceof z.ZodError) { const newErrors: Record<string, string> = {} error.errors.forEach((err) => { const fieldName = err.path[0] as string if (fieldName) { newErrors[fieldName] = err.message } }) errors.value = newErrors } return false } finally { isValidating.value = false } }
// Async validation for server-side checks const validateAsync = async (fieldName: string, value: any) => { // First run local validation await validateField(fieldName, value)
// If local validation passes, run server validation if (!errors.value[fieldName]) { try { const isUnique = await checkFieldUniqueness(fieldName, value) if (!isUnique) { errors.value = { ...errors.value, [fieldName]: `${fieldName} is already taken` } } } catch (error) { console.warn('Server validation failed:', error) } } }
return { errors: readonly(errors), isValidating: readonly(isValidating), validateField, validateForm, validateAsync }}
🎮 Form Control & Events
Section titled “🎮 Form Control & Events”Programmatic form control and event handling:
<template> <div> <div class="mb-4 space-x-2"> <Button @click="validateForm">Validate</Button> <Button @click="resetForm">Reset</Button> <Button @click="fillTestData">Fill Test Data</Button> </div>
<HiForm ref="formRef" :fields="fields" :schema="schema" @submit="handleSubmit" @change="handleChange" /> </div></template>
<script setup lang="ts">const formRef = ref()
// Manually validate formconst validateForm = async () => { const isValid = await formRef.value?.validate() console.log('Form is valid:', isValid)}
// Reset form to initial stateconst resetForm = () => { formRef.value?.reset()}
// Programmatically set form valuesconst fillTestData = () => { formRef.value?.setValues({ name: 'John Doe', age: 25 })}
// Get current form valuesconst getCurrentValues = () => { const values = formRef.value?.getValues() console.log('Current values:', values)}</script>
<template> <HiForm :fields="fields" :schema="schema" @submit="handleSubmit" @change="handleFormChange" @field-change="handleFieldChange" @reset="handleReset" /></template>
<script setup lang="ts">// Form submission (after validation passes)const handleSubmit = (data: FormData) => { console.log('Valid form data:', data) // Submit to API}
// Any form changeconst handleFormChange = (data: FormData) => { console.log('Form changed:', data) // Auto-save, real-time validation, etc.}
// Specific field changeconst handleFieldChange = ({ fieldName, value }: { fieldName: string, value: any }) => { console.log(`Field ${fieldName} changed to:`, value)
// Custom logic based on field if (fieldName === 'country') { // Update other fields based on country selection updateCitiesForCountry(value) }}
// Form resetconst handleReset = () => { console.log('Form was reset') // Clear related state, reset other components, etc.}</script>
📚 API Reference
Section titled “📚 API Reference”Prop | Type | Default | Description |
---|---|---|---|
fields | FormFieldConfig[] | - | Array of field configurations |
schema | z.ZodSchema | undefined | Zod validation schema |
layout | FormLayoutConfig | {columns: 1, labelPosition: 'top'} | Layout configuration |
buttons | FormButtonConfig | Default config | Button customization |
loading | boolean | false | External loading state |
showDebug | boolean | false | Show debug information |
Events
Section titled “Events”Event | Payload | Description |
---|---|---|
submit | FormData | Emitted when form is submitted successfully |
reset | void | Emitted when form is reset |
change | FormData | Emitted when any field value changes |
field-change | {fieldName: string, value: any} | Emitted when specific field changes |
Methods (via ref)
Section titled “Methods (via ref)”Method | Description |
---|---|
validate() | Manually validate the entire form |
reset() | Reset form to initial state |
getValues() | Get current form values |
setValues(values) | Set form values programmatically |
🏗 Real-World Example
Section titled “🏗 Real-World Example”Complete user registration form showcasing all features:
<template> <BaseCard> <BaseCardHeader> <h2 class="text-xl font-semibold">User Registration</h2> <p class="text-sm text-muted-foreground"> Create your account with our dynamic form </p> </BaseCardHeader> <BaseCardContent> <HiForm ref="registrationForm" :fields="registrationFields" :schema="registrationSchema" :layout="formLayout" :buttons="buttonConfig" @submit="handleRegistration" @change="handleFormChange" /> </BaseCardContent> </BaseCard></template>
<script setup lang="ts">import { z } from 'zod'import { toast } from 'vue-sonner'import HiForm from '@/components/HiForm/HiForm.vue'import { BaseCard, BaseCardHeader, BaseCardContent } from '@/components/BaseCard'
// Comprehensive registration schemaconst registrationSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Please enter a valid email address'), age: z.preprocess( (val) => Number(val), z.number().min(18, 'Must be at least 18').max(99, 'Must be less than 100') ), country: z.string().min(1, 'Please select a country'), gender: z.enum(['male', 'female', 'other']), newsletter: z.boolean(), experience: z.number().min(0).max(50), bio: z.string().min(10).max(500), skills: z.string().min(1, 'Please select your skills'), hasJob: z.boolean(), companyName: z.string().optional(),})
const registrationFields: FormFieldConfig[] = [ { type: 'input', name: 'name', label: 'Full Name', placeholder: 'Enter your full name', required: true, }, { type: 'input', name: 'email', label: 'Email Address', inputType: 'email', required: true, }, { type: 'input', name: 'age', label: 'Age', inputType: 'number', placeholder: '25', required: true, }, { type: 'select', name: 'country', label: 'Country', placeholder: 'Select your country', options: [ { label: 'United States', value: 'us' }, { label: 'Canada', value: 'ca' }, { label: 'United Kingdom', value: 'uk' }, { label: 'Germany', value: 'de' }, ], required: true, }, { type: 'radio', name: 'gender', label: 'Gender', options: [ { label: 'Male', value: 'male' }, { label: 'Female', value: 'female' }, { label: 'Other', value: 'other' }, ], required: true, }, { type: 'checkbox', name: 'hasJob', text: 'Currently employed', }, { type: 'input', name: 'companyName', label: 'Company Name', placeholder: 'Enter your company name', condition: { field: 'hasJob', value: true, operator: 'eq', }, }, { type: 'combobox', name: 'skills', label: 'Technical Skills', placeholder: 'Select your skills', options: [ { label: 'Vue.js', value: 'vue' }, { label: 'React', value: 'react' }, { label: 'TypeScript', value: 'typescript' }, { label: 'Node.js', value: 'nodejs' }, ], }, { type: 'slider', name: 'experience', label: 'Years of Experience', min: 0, max: 30, step: 1, }, { type: 'textarea', name: 'bio', label: 'Biography', placeholder: 'Tell us about yourself...', rows: 4, }, { type: 'switch', name: 'newsletter', label: 'Newsletter Subscription', text: 'Receive updates and news via email', },]
const formLayout = { columns: 2, labelPosition: 'top', gap: '1.5rem',}
const buttonConfig = { submitText: 'Create Account', loadingText: 'Creating Account...', resetText: 'Clear Form', showResetButton: true,}
const handleRegistration = (data: z.infer<typeof registrationSchema>) => { toast.success('Account created successfully!') console.log('Registration data:', data)}
const handleFormChange = (data: any) => { console.log('Form data changed:', data)}</script>
// Complete type definitions for form configurationinterface FormFieldConfig { // Base properties name: string label?: string placeholder?: string required?: boolean disabled?: boolean readonly?: boolean class?: string hidden?: boolean
// Conditional logic condition?: { field: string value: any operator?: 'eq' | 'ne' | 'gt' | 'lt' | 'in' | 'notIn' }}
// Field-specific configurationsinterface InputFieldConfig extends FormFieldConfig { type: 'input' inputType?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' maxLength?: number minLength?: number}
interface SelectFieldConfig extends FormFieldConfig { type: 'select' options: SelectOption[] multiple?: boolean}
interface SliderFieldConfig extends FormFieldConfig { type: 'slider' min?: number max?: number step?: number orientation?: 'horizontal' | 'vertical'}
// Layout and button configurationsinterface FormLayoutConfig { columns: 1 | 2 | 3 labelPosition: 'top' | 'left' gap?: string}
interface FormButtonConfig { submitText?: string resetText?: string loadingText?: string showSubmitButton?: boolean showResetButton?: boolean}
🎨 Styling & Customization
Section titled “🎨 Styling & Customization”HiForm uses your application’s theme and provides field-level customization:
Default Styling
Section titled “Default Styling”- Responsive design with mobile-first approach
- Dark/light mode support automatically
- Focus states and accessibility compliance
- Error styling with smooth animations
Field-Level Styling
Section titled “Field-Level Styling”<script setup lang="ts">const formFields: FormFieldConfig[] = [ { type: 'input', name: 'email', label: 'Email Address', class: 'custom-field-wrapper', // Custom field wrapper class placeholder: 'Enter your email', }, // ... other fields]</script>
<template> <HiForm :fields="formFields" :schema="schema" class="custom-form" // Custom form container class /></template>
<style scoped>@import '@/assets/styles/index.css';.custom-form { @apply space-y-6 p-6 bg-card rounded-lg border;}
.custom-field-wrapper { @apply p-4 border rounded-md bg-muted/50;}</style>
🚀 Best Practices
Section titled “🚀 Best Practices”Form Design Guidelines
Section titled “Form Design Guidelines”- Logical Grouping: Group related fields together
- Clear Labels: Use descriptive, concise field labels
- Helpful Placeholders: Provide examples of expected input
- Progressive Disclosure: Use conditional logic to show relevant fields
- Validation Feedback: Show validation errors inline and in real-time