Skip to content

HiForm Builder

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.

🔧 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

Simple Form Example
<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>

HiForm supports 9 different field types, each with specific configuration options:

For text, email, password, number inputs:

Input Field Configuration
{
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
}

For multi-line text input:

Textarea Field Configuration
{
type: 'textarea',
name: 'bio',
label: 'Biography',
placeholder: 'Tell us about yourself...',
rows: 4,
maxLength: 500,
required: true
}

Create dynamic forms that adapt based on user input:

Basic Conditional Logic
// 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'
}
}

Customize form layout for optimal user experience:

Column Layout Configuration
<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>

Robust form validation using Zod schemas:

import { z } from 'zod'
// Simple validation schema
const 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 transforms
const 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'
)
})

🏗️ Component Architecture & Development Practices

Section titled “🏗️ Component Architecture & Development Practices”

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

Create forms dynamically based on data schemas or API responses:

Dynamic Field Generation
// utils/formGenerator.ts
import { z } from 'zod'
import type { FormFieldConfig } from '@/components/HiForm/types'
// Generate form fields from Zod schema
export 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 inference
function 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 component
const 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))
Field Rendering Optimization
// Optimize field rendering for large forms
<script setup lang="ts">
// ✅ Good: Memoize field configurations
const 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 fields
const 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 options
const 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 UX
const 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 memoization
const 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>

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 form
const validateForm = async () => {
const isValid = await formRef.value?.validate()
console.log('Form is valid:', isValid)
}
// Reset form to initial state
const resetForm = () => {
formRef.value?.reset()
}
// Programmatically set form values
const fillTestData = () => {
formRef.value?.setValues({
name: 'John Doe',
age: 25
})
}
// Get current form values
const getCurrentValues = () => {
const values = formRef.value?.getValues()
console.log('Current values:', values)
}
</script>
PropTypeDefaultDescription
fieldsFormFieldConfig[]-Array of field configurations
schemaz.ZodSchemaundefinedZod validation schema
layoutFormLayoutConfig{columns: 1, labelPosition: 'top'}Layout configuration
buttonsFormButtonConfigDefault configButton customization
loadingbooleanfalseExternal loading state
showDebugbooleanfalseShow debug information
EventPayloadDescription
submitFormDataEmitted when form is submitted successfully
resetvoidEmitted when form is reset
changeFormDataEmitted when any field value changes
field-change{fieldName: string, value: any}Emitted when specific field changes
MethodDescription
validate()Manually validate the entire form
reset()Reset form to initial state
getValues()Get current form values
setValues(values)Set form values programmatically

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 schema
const 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',
placeholder: '[email protected]',
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>

HiForm uses your application’s theme and provides field-level customization:

  • Responsive design with mobile-first approach
  • Dark/light mode support automatically
  • Focus states and accessibility compliance
  • Error styling with smooth animations
<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>
  1. Logical Grouping: Group related fields together
  2. Clear Labels: Use descriptive, concise field labels
  3. Helpful Placeholders: Provide examples of expected input
  4. Progressive Disclosure: Use conditional logic to show relevant fields
  5. Validation Feedback: Show validation errors inline and in real-time