FormGenerator
Dynamic form generation component with built-in validation using Vuelidate 2.x.
Description
FormGenerator is a component that lets you build complex forms declaratively via a configuration schema. It handles validation, form state, and rendering of different field types automatically.
Location: @/components/Forms/Generator.vue
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
form | Array | ✅ Yes | - | Array of arrays defining the form structure |
base | Object | ❌ No | {} | Object with initial form values |
loading | Object | ❌ No | {} | Object with loading states per field |
class | String | ❌ No | '' | Additional CSS classes for the container |
style | Object | ❌ No | {} | Inline styles for the container |
Form Schema Structure
The form prop must be an array of arrays. Each inner array represents a row of fields. Fields in the same row are rendered side by side using Bootstrap’s column system.
Basic Structure
const fields = computed(() => [
// First row: two fields side by side
[
{ /* field 1 */ },
{ /* field 2 */ }
],
// Second row: one full-width field
[
{ /* field 3 */ }
]
]);Field Structure
Each field is an object with the following properties:
{
name: 'field_name', // Unique field name (required)
lang: 'translation.key', // Translation key for the label (required)
type: 'text', // HTML input type (optional, default: 'text')
config: { // Field configuration (required)
type: 'input', // Component type to render
col: 6, // Bootstrap column width (1-12)
placeholder: '...', // Input placeholder
disabled: false, // Disable field
// ... more options per type
},
rules: { // Validation rules (optional)
required: true,
email: true,
min: 6,
// ... more rules
},
visible: true, // Show/hide field (optional, default: true)
class: 'custom-class', // Additional CSS classes (optional)
subTitle: 'translation.key', // Field subtitle (optional)
hint: 'Help text', // Help text below input (optional, input/text only)
hintClass: 'text-muted', // CSS class for hint (optional, default: 'text-muted')
id: 123 // Optional ID; useful for callbacks (e.g. search, remove-button)
}Sections with Title
You can group fields into sections with title and subtitle:
const fields = computed(() => [
{
title: 'section.title.key',
subtitle: 'section.subtitle.key',
prependIcon: 'fa fa-info-circle', // Icon before title (optional)
appendIcon: 'fa fa-chevron-down', // Icon after title (optional)
actions: [ // Action buttons in header (optional)
{ type: 'add', label: 'actions.add', icon: 'fa fa-plus', class: 'btn btn-link', onClick: () => {} }
],
rows: [
[
{ /* field 1 */ },
{ /* field 2 */ }
]
]
},
// You can also mix sections and simple rows
[
{ /* field 3 */ }
]
]);Supported Field Types
Input (type: 'input')
Standard text field.
{
name: 'email',
lang: 'general.email',
type: 'email', // text, email, password, number, etc.
config: {
type: 'input',
placeholder: 'Enter your email',
col: 6,
disabled: false,
attrs: {}, // Additional HTML attributes
on: {}, // Event listeners
showError: true // Show error messages (default: true)
},
rules: {
required: true,
email: true
},
hint: 'general.email.hint', // Optional: help text below input
hintClass: 'text-muted' // Optional: hint CSS class (default: 'text-muted')
}Input Group (type: 'input-group')
Input with prepend/append elements.
{
name: 'price',
lang: 'general.price',
config: {
type: 'input-group',
prepend: '$', // Text before input
prependicon: 'fa-dollar', // Icon before input
append: 'USD', // Text after input
appendicon: 'fa-check', // Icon after input
col: 6
},
rules: {
required: true,
numeric: true
}
}Select (type: 'select')
Dropdown selector using Select2.
{
name: 'country',
lang: 'general.country',
config: {
type: 'select',
options: [
{ value: 'mx', label: 'Mexico' },
{ value: 'us', label: 'United States' }
],
placeholder: 'Select a country',
mode: 'single', // 'single' | 'tags' | 'multiple'
disabled: false,
selectAll: false, // Show "Select All" option
limit: 99, // Selection limit
groups: false, // Group options
createTag: false, // Allow creating tags
addTagOn: ['enter'], // Events to add tag
openDirection: 'bottom', // 'top' | 'bottom'
textBreak: 27, // Text truncation length
selectedBreak: 27,
col: 6,
action: { // Optional action button
label: 'Add New',
icon: 'fa fa-plus',
onClick: () => {}
}
},
rules: {
required: true
}
}Textarea (type: 'textarea')
Multiline text field.
{
name: 'description',
lang: 'general.description',
config: {
type: 'textarea',
rows: 4,
maxlength: 500,
placeholder: 'Enter description',
col: 12
},
rules: {
required: true,
max: 500
}
}Switch (type: 'switch')
On/off toggle.
{
name: 'is_active',
lang: 'general.active',
config: {
type: 'switch',
options: [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
],
variant: 'primary',
disabled: false,
col: 6
}
}Switch Default (type: 'switch-default')
Switch with default styling.
{
name: 'notifications',
lang: 'general.notifications',
config: {
type: 'switch-default',
options: [
{ value: true, label: 'Enabled' },
{ value: false, label: 'Disabled' }
],
line: true,
outline: false,
col: 6
}
}File Upload (type: 'file')
File upload with preview.
{
name: 'document',
lang: 'general.document',
config: {
type: 'file',
attrs: {
accept: 'image/*,.pdf'
},
textBreak: 30,
col: 12
},
rules: {
required: true
}
}File Multiple (type: 'file-multiple')
Multiple file/image upload using the MultiImageUpload component. Supports existing files and validation.
{
name: 'images',
lang: 'general.images',
config: {
type: 'file-multiple',
placeholder: 'general.upload.images',
attrs: {
accept: 'image/*'
},
existingFiles: [], // Array of existing files (preview)
disabled: false,
col: 12
},
rules: {
required: true
}
}Reminder (type: 'reminder')
Field to configure reminders (frequency, date/time, weekdays, day of month). Uses the Reminder component with support for repetition (once, daily, weekly, monthly).
{
name: 'reminder',
lang: 'tasks.reminder.label',
config: {
type: 'reminder',
showError: true,
attrs: {},
on: {}
},
rules: {
required: false
}
}Checkbox (type: 'checkbox')
Multiple checkboxes.
{
name: 'permissions',
lang: 'general.permissions',
config: {
type: 'checkbox',
options: [
{ id: 1, value: 'read', label: 'Read' },
{ id: 2, value: 'write', label: 'Write' },
{ id: 3, value: 'delete', label: 'Delete' }
],
disabled: false,
col: 12
},
rules: {
required: true
}
}Search (type: 'search')
Global search field. The onSelectItem callback receives the selected item plus field_id (value of field.id if set).
{
name: 'search',
id: 1, // Optional; passed as field_id in onSelectItem
lang: 'header.search',
config: {
type: 'search',
showLabel: true,
disabled: false,
search_value: 'name', // Item property to display in list (default: 'name')
on: {
onUpdate: (value) => {},
onSelectItem: (item) => {} // item includes field_id when field.id is set
}
}
}Select Input (type: 'select-input')
Combination of select and input.
{
name: 'custom_field',
lang: 'general.custom',
config: {
type: 'select-input',
select: {
model: 'type',
options: [],
placeholder: 'Select type'
},
input: {
model: 'value',
placeholder: 'Enter value',
attrs: {}
},
col: 12
}
}Shortcut Textarea (type: 'shortcut-textarea')
Textarea with keyboard shortcuts.
{
name: 'message',
lang: 'general.message',
config: {
type: 'shortcut-textarea',
rows: 5,
placeholder: 'Enter message',
categories: [],
showShortcutButton: false,
ticket: null,
guide: null,
col: 12
}
}Other Types
type: 'label'- Read-only labeltype: 'button'- Custom button (config: label, align, class, tooltip, disabled, prependicon, appendicon, on, attrs)type: 'alert'- Alert message (config: variant, message, icon, class, align)type: 'divider'- Visual divider (config: attrs)type: 'document-preview'- Document preview (config: file, labels, on.open)type: 'remove-button'- Delete button (config: class, style, attrs, on.click(field.id)`)type: 'action-button'- Custom action button (config: label, icon, variant, class, style, attrs, on.click(field)`)
Validation Rules
Validation rules are defined in each field’s rules object. The component uses Vuelidate 2.x internally.
Available Rules
| Rule | Value | Description |
|---|---|---|
required | true | Required field |
email | true | Validate email format |
emailArray | true | Validate array of emails |
min | number | Minimum character length |
max | number | Maximum character length |
minValue | number | Minimum numeric value |
numeric | true | Numbers only |
integer | true | Integers only |
regExp | RegExp | Validate with regular expression |
sameAs | string | Must match another field |
valid | boolean | Custom validation |
Rules Example
{
name: 'password',
lang: 'general.password',
type: 'password',
config: {
type: 'input',
col: 6
},
rules: {
required: true,
min: 8,
max: 50,
regExp: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
errorMessage: 'custom.error.key' // Custom error message
}
}Related Field Validation
{
name: 'confirm_password',
lang: 'general.confirm_password',
type: 'password',
config: {
type: 'input',
col: 6
},
rules: {
required: true,
sameAs: 'password' // Must match the 'password' field
}
}Exposed Methods and Properties
The component exposes the following methods and properties via ref:
state
Reactive object with the current form values.
const form = ref(null);
// Access values
console.log(form.value.state.email);
console.log(form.value.state.name);
// Values update automatically with v-modelv$
Vuelidate instance for validation and error access.
// Validate form
const isValid = await form.value.v$.$validate();
// Check if a field has errors
if (form.value.v$.email.$error) {
console.log(form.value.v$.email.$errors);
}
// Check if form is valid
if (form.value.v$.$invalid) {
// Show errors
}$clear()
Clears the form and resets validation.
form.value.$clear();Slots
titles
Slot to customize section title and subtitle.
<FormGenerator :form="fields" :base="initial">
<template #titles="{ section }">
<h2>{{ t(section.title) }}</h2>
<p>{{ t(section.subtitle) }}</p>
</template>
</FormGenerator>Usage Examples
Basic Example
<template>
<FormGenerator
ref="form"
:form="fields"
:base="initial"
/>
<button @click="handleSubmit">Submit</button>
</template>
<script setup>
import { ref, computed } from 'vue';
import FormGenerator from '@/components/Forms/Generator.vue';
const form = ref(null);
const initial = computed(() => ({
name: '',
email: '',
password: ''
}));
const fields = computed(() => [
[
{
name: 'name',
lang: 'general.name',
config: {
type: 'input',
placeholder: 'Enter your name',
col: 6
},
rules: {
required: true
}
},
{
name: 'email',
lang: 'general.email',
config: {
type: 'input',
placeholder: 'Enter your email',
col: 6
},
rules: {
required: true,
email: true
}
}
],
[
{
name: 'password',
lang: 'general.password',
type: 'password',
config: {
type: 'input',
col: 12
},
rules: {
required: true,
min: 8
}
}
]
]);
const handleSubmit = async () => {
const isValid = await form.value.v$.$validate();
if (isValid) {
console.log('Form data:', form.value.state);
// Send data to server
}
};
</script>Example with Sections
<template>
<FormGenerator
ref="form"
:form="fields"
:base="initial"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import FormGenerator from '@/components/Forms/Generator.vue';
const form = ref(null);
const initial = computed(() => ({
name: '',
email: '',
phone: '',
address: ''
}));
const fields = computed(() => [
{
title: 'form.section.personal_info',
subtitle: 'form.section.personal_info.description',
info: true,
rows: [
[
{
name: 'name',
lang: 'general.name',
config: {
type: 'input',
col: 6
},
rules: {
required: true
}
},
{
name: 'email',
lang: 'general.email',
config: {
type: 'input',
col: 6
},
rules: {
required: true,
email: true
}
}
]
]
},
{
title: 'form.section.contact_info',
rows: [
[
{
name: 'phone',
lang: 'general.phone',
config: {
type: 'input',
col: 6
},
rules: {
required: true
}
},
{
name: 'address',
lang: 'general.address',
config: {
type: 'textarea',
rows: 3,
col: 6
}
}
]
]
}
]);
</script>Example with Select and Complex Validation
<template>
<FormGenerator
ref="form"
:form="fields"
:base="initial"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import FormGenerator from '@/components/Forms/Generator.vue';
const form = ref(null);
const countries = ref([]);
const initial = computed(() => ({
name: '',
country: null,
age: null
}));
const fields = computed(() => [
[
{
name: 'name',
lang: 'general.name',
config: {
type: 'input',
col: 6
},
rules: {
required: true,
min: 2,
max: 50
}
},
{
name: 'country',
lang: 'general.country',
config: {
type: 'select',
options: countries.value,
placeholder: 'Select a country',
mode: 'single',
col: 6
},
rules: {
required: true
}
}
],
[
{
name: 'age',
lang: 'general.age',
config: {
type: 'input',
col: 6
},
rules: {
required: true,
integer: true,
minValue: 18
}
}
]
]);
onMounted(() => {
// Load select options
countries.value = [
{ value: 'mx', label: 'Mexico' },
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' }
];
});
</script>Example with Loading States
<template>
<FormGenerator
ref="form"
:form="fields"
:base="initial"
:loading="loadingStates"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import FormGenerator from '@/components/Forms/Generator.vue';
const form = ref(null);
const loadingStates = ref({
email: false,
username: false
});
const initial = computed(() => ({
email: '',
username: ''
}));
const fields = computed(() => [
[
{
name: 'email',
lang: 'general.email',
config: {
type: 'input',
col: 6
},
rules: {
required: true,
email: true
}
},
{
name: 'username',
lang: 'general.username',
config: {
type: 'input',
col: 6
},
rules: {
required: true
}
}
]
]);
// Example: enable loading when validating email
const checkEmail = async () => {
loadingStates.value.email = true;
// Simulate validation
await new Promise(resolve => setTimeout(resolve, 1000));
loadingStates.value.email = false;
};
</script>Important Notes
Translations: All labels use translation keys (
lang). Ensure translations are defined in your i18n files.Validation: Validation is automatic but you must call
v$.$validate()manually before submitting the form.Reactive State: The
stateobject is reactive and updates automatically when the user interacts with the fields.Bootstrap Columns: The column system uses Bootstrap 5. The
colvalue must be between 1 and 12. If not specified, it is calculated automatically by dividing 12 by the number of fields in the row.Visible Fields: Use the
visibleproperty to show/hide fields dynamically.Initial Values: The
baseprop is used to set initial values. If you need to update values after initialization, the component watchesbasefor changes and updates state automatically.
