Skip to content

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

PropTypeRequiredDefaultDescription
formArray✅ Yes-Array of arrays defining the form structure
baseObject❌ No{}Object with initial form values
loadingObject❌ No{}Object with loading states per field
classString❌ No''Additional CSS classes for the container
styleObject❌ 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

javascript
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:

javascript
{
  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:

javascript
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.

javascript
{
  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.

javascript
{
  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.

javascript
{
  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.

javascript
{
  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.

javascript
{
  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.

javascript
{
  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.

javascript
{
  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.

javascript
{
  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).

javascript
{
  name: 'reminder',
  lang: 'tasks.reminder.label',
  config: {
    type: 'reminder',
    showError: true,
    attrs: {},
    on: {}
  },
  rules: {
    required: false
  }
}

Checkbox (type: 'checkbox')

Multiple checkboxes.

javascript
{
  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
  }
}

Global search field. The onSelectItem callback receives the selected item plus field_id (value of field.id if set).

javascript
{
  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.

javascript
{
  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.

javascript
{
  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 label
  • type: '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

RuleValueDescription
requiredtrueRequired field
emailtrueValidate email format
emailArraytrueValidate array of emails
minnumberMinimum character length
maxnumberMaximum character length
minValuenumberMinimum numeric value
numerictrueNumbers only
integertrueIntegers only
regExpRegExpValidate with regular expression
sameAsstringMust match another field
validbooleanCustom validation

Rules Example

javascript
{
  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
  }
}
javascript
{
  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.

javascript
const form = ref(null);

// Access values
console.log(form.value.state.email);
console.log(form.value.state.name);

// Values update automatically with v-model

v$

Vuelidate instance for validation and error access.

javascript
// 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.

javascript
form.value.$clear();

Slots

titles

Slot to customize section title and subtitle.

vue
<FormGenerator :form="fields" :base="initial">
  <template #titles="{ section }">
    <h2>{{ t(section.title) }}</h2>
    <p>{{ t(section.subtitle) }}</p>
  </template>
</FormGenerator>

Usage Examples

Basic Example

vue
<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

vue
<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

vue
<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

vue
<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

  1. Translations: All labels use translation keys (lang). Ensure translations are defined in your i18n files.

  2. Validation: Validation is automatic but you must call v$.$validate() manually before submitting the form.

  3. Reactive State: The state object is reactive and updates automatically when the user interacts with the fields.

  4. Bootstrap Columns: The column system uses Bootstrap 5. The col value must be between 1 and 12. If not specified, it is calculated automatically by dividing 12 by the number of fields in the row.

  5. Visible Fields: Use the visible property to show/hide fields dynamically.

  6. Initial Values: The base prop is used to set initial values. If you need to update values after initialization, the component watches base for changes and updates state automatically.

Envia Admin