DEV Community

Cover image for Deep dive into Vue Dynamic Forms.
Alvaro Saburido
Alvaro Saburido

Posted on • Updated on • Originally published at alvarosaburido.com

Deep dive into Vue Dynamic Forms.

Is summer 2020 and since we're coming back to quarantine in Barcelona Spain, I decided to take some time to prepare this article about dynamic forms in Vue meanwhile I imagine myself on a beach drinking a mojito 🏝 .

Usually, forms are pretty straightforward to implement especially in vue, you just need a pair of inputs inside of a <form> element in your template and a method that will handle when your form is submitted.

That's ok, but what happens when the form gets bigger and bigger? Or what happens when your client needs to edit those inputs frequently to adapt to their current business model? For the second case that would imply that the developer needs to change the template every time the client needs, which ends up being a pretty mediocre user experience for the client, and for the developer as well.

Frustration

I found myself in that situation a few months ago in a side-project, then I remembered the times that I was working mainly in Angular, there was a way to implement forms dynamically based on metadata that describes the business model, called Dynamic Forms.

That's it, that's the solution. So I started to search for an existing library in vue with a similar implementation, but to my surprise, there wasn't that much. In fact, most of them where over-complicated or part of an extensive library of UI components that I don't really need.

So I started doing a module from scratch following similar concepts from the angular implementation, a <form-component> holding the form info, and a set of input-controls that will iterate using v-for to dynamically create the inputs based of a fields prop.

All you will need to do is pass the <form-component /> the fields using an object array similar to this:

{
  fields: [
    {
      label: 'Email',
      name: 'email',
      type: 'email',
    },
    {
      label: 'Password',
      name: 'password',
      type: 'password',
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

Eventually, I found myself using this module in several projects, with little modifications in each one and the problem of having to update them all manually whenever I improved the module. So I decided to transform it into a library that I could install as a dependency using npm.

That is how Vue Dynamic Forms was born.

it's alive

In this article, we'll learn how to easily implement dynamic forms in vue using this library. The good thing? I already pass trough the difficult parts so you don't need to reinvent the wheel 😉.

Instalation

To install just run:

yarn add @asigloo/vue-dynamic-forms

# or, using NPM
npm install @asigloo/vue-dynamic-forms
Enter fullscreen mode Exit fullscreen mode

Then, we can install it as a plugin between the vue app:

import Vue from 'vue';
import VueDynamicForms from '@asigloo/vue-dynamic-forms';

Vue.use(VueDynamicForms);
Enter fullscreen mode Exit fullscreen mode

If you're using Nuxt.js you can install it as a module (Nuxt.js version >= 2.12.2 is recommended).

// nuxt.config.js
module.exports = {
  modules: ['@asigloo/vue-dynamic-forms/nuxt'],
};
Enter fullscreen mode Exit fullscreen mode

Form Composition

Let's start with the basics, a simple login form. Inside our page/component template add the <dynamic-form /> component

<dynamic-form
  :id="loginForm.id"
  :fields="loginForm.fields"
  :options="loginForm.options"
  @submit="handleSubmit"
/>
Enter fullscreen mode Exit fullscreen mode

Just bellow we will create a call to action button for the form submitting. For it to work make sure it has the attribute submit to true and form with the same id that you passed to the dynamic-form component.

<button submit="true" :form="loginForm.id" class="btn btn-primary">
   Login
</button>
Enter fullscreen mode Exit fullscreen mode

We'll create a data object containing the form id and the form fields, to create the fields we can import the factory function FormField from the library core:

import { FormField } from '@asigloo/vue-dynamic-forms';

const loginPage = {
  data() {
    return {
      loginForm: {
        id: 'login-form',
        fields: [
          new FormField({ type: 'email', label: 'Email', name: 'email' }),
          new FormField({
            type: 'password',
            label: 'Password',
            name: 'password',
          }),
        ],
      },
    };
  },
};

export default loginPage;
Enter fullscreen mode Exit fullscreen mode

With a little bit of CSS makeup you should end up to something similar to this:

Login Form

Options

The main component comes with a set of default options you can override by passing an object using FormOptions function trough the options prop. There is more
info in the documentation here

import { FormField, FormOptions } from '@asigloo/vue-dynamic-forms';

const loginPage = {
  data() {
    return {
      loginForm: {
        id: 'login-form',
        fields,
        options: new FormOptions({
          customClass = 'row',
          netlify: true,
        })
      },
    };
  },
};

export default loginPage;
Enter fullscreen mode Exit fullscreen mode

The next step is to handle the form's submission, for that, the library has a special called submit event (documentation here) which will trigger after clicking the button and the form has no errors. Let's create a method on our loginPage component called onLogInSubmit

import { FormField } from '@asigloo/vue-dynamic-forms';

const loginPage = {
  data() {
    return {
      loginForm,
      isLoggedIn: false
      },
    };
  },
  methods() {
   onLogInSubmit()  {
      this.isLoggedIn  =  true;
   }
  }
};

export default loginPage;
Enter fullscreen mode Exit fullscreen mode

Styling

The library is completely framework-agnostic in terms of UI, the components are unstyled by default, so you can customize them with your own. If you want a more "ready to go" solution you can import one of the themes we have included in src/styles/themes/

@import '~@asigloo/vue-dynamic-forms/src/styles/themes/default.scss';
Enter fullscreen mode Exit fullscreen mode

or with Nuxt Module options in nuxt.config.js:

module.exports = {
  modules: ['@asigloo/vue-dynamic-forms/nuxt'],
  dynamicForms: {
    theme: 'default',
  },
};
Enter fullscreen mode Exit fullscreen mode

For the moment there are two themes available:

  • Bootstrap alike themes/default.scss
  • Material Design theme themes/material.scss

Vue dynamic forms themes

I wanted the styling process to be as customizable as the client needed, you can add customClass to both the main form trough the FormOptions and each field trough the FormField option. This is pretty handy for example if you want to have a row of fields each one in a column.

import { FormField, FormOptions } from '@asigloo/vue-dynamic-forms';

const loginPage = {
  data() {
    return {
      loginForm: {
        id: 'login-form',
        fields: [
          new FormField({
            type: 'email',
            label: 'Email',
            name: 'email',
            customClass: 'col-6'
          }),
          new FormField({
            type: 'password',
            label: 'Password',
            name: 'password',
            customClass: 'col-6'
          }),
        ],
        options: new FormOptions({
          customClass = 'row',
        })
      },
    };
  },
};

export default loginPage;
Enter fullscreen mode Exit fullscreen mode

Inline custom classes

Another way you can customize it is by overriding the scss variables, you only need to set them just before importing the theme like this:

$input-bg: #e2eb5d52;
$input-border-color: #aec64c;

@import '~@asigloo/vue-dynamic-forms/src/styles/themes/default.scss';
Enter fullscreen mode Exit fullscreen mode

Validations and error handling.

One of the things I missed in the majority of the libraries was a built-in validation to avoid having the necessity of adding an external library like VeeValidate into the bundle.

For this, I added to the library the ability to define an array of validations you want the field control to have and the error message in addition to a set of default validators (which are in their core just simple arrow functions) that you can import to your component as you need like this:

import {
  FormField,
  FormValidation,
  required,
  email,
} from '@asigloo/vue-dynamic-forms';
Enter fullscreen mode Exit fullscreen mode

Then add the validations array to the FormField of your choice, for this example, we are going to use email one. Each validation can be created using FormValidationwith the validator function and the message linked to it in case it fails.

loginForm: {
  ...
  fields: [
    new FormField({
      type: 'email',
      label: 'Email',
      name: 'email',
      validations: [
          new FormValidation(required, 'This field is required'),
          new FormValidation(email, 'Format of email is incorrect'),
      ],
    }),
  ],
},
Enter fullscreen mode Exit fullscreen mode

email-validation

By default, Vue Dynamic Forms contains the following validations:

  • required
  • min
  • max
  • email
  • url
  • minLength
  • maxLength
  • pattern

The complete documentation for this --> Validation

But... what about custom validations 🤔? Let's use the password field as an example:

new FormField({
  type: 'password',
  label: 'Password',
  name: 'password',
  validations: [
    new FormValidation(
      pattern(
        '^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$^+=!*()@%&]).{8,10}$',
      ),
      'Password must contain at least 1 Uppercase, 1 Lowercase, 1 number, 1 special character and min 8 characters max 10',
    ),
  ],
}),
Enter fullscreen mode Exit fullscreen mode

Under the hood, pattern validator uses regex to check if the value of the input matches the acceptance criteria. This is very powerful because it allows the user to create their custom validations outside of the library defaults.

Thanks for following along, I hope this tutorial helps you implement forms in a more comfortable way. If you have any question let's talk in the comments.

Like this article? Follow @alvarosaburido1 on Twitter.

Discussion (0)