You’ll find us on:
27.06.26 3 min read Technology

How to create dynamic forms with Storyblok and Vue.js/Nuxt?

Storyblok and Vue.js logos

Dynamic forms in Storyblok allow editors to add and configure form fields directly in the CMS, while Nuxt handles their rendering on the frontend. For validation, we can use Vuelidate, so rules such as required, email, and minLength can also be driven by the configuration in Storyblok. In this tutorial, we’ll show you how to combine Storyblok, Nuxt, and Vuelidate to build a form whose fields and validation rules are defined dynamically.

ElementRole
Storyblokdefines form fields and validation rules
Nuxt / Vuerenders the form
Vuelidatevalidates field values
DynamicForm.vuebuilds the form and validation rules
InputField.vuerenders an individual field

 

 

 

 

 

 

 

 

 

How do you configure Vuelidate in Nuxt?

To handle validation for dynamic fields, we’ll use Vuelidate. First, add the two required packages to your project:

npm install @vuelidate/core @vuelidate/validators

# or

yarn add @vuelidate/core @vuelidate/validators

In the current version of Vuelidate, the library is no longer registered as a global Nuxt plugin. Instead, useVuelidate() is imported and called directly inside the component where the form state and validation rules are defined. This means you do not need a plugins/vuelidate.ts file.

In the form component, we’ll use:

import { useVuelidate } from '@vuelidate/core'

import * as validators from '@vuelidate/validators'

Configuring a dynamic form in Storyblok

After installing Vuelidate, we can define the form structure in Storyblok. Storyblok will store the configuration of the fields and validation rules, while Nuxt and Vuelidate will use that data to render the form and validate the values entered by the user.

Validation Blocks
To make validation configurable directly in Storyblok, we create separate blocks that correspond to Vuelidate rules. In this example, we’ll use:

  • required - requires the field to contain a value,
  • email - requires a valid email address,
  • numeric - allows digits only,
  • minLength - requires at least the specified number of characters,
  • maxLength - allows up to the specified number of characters.
Storyblok Block Library showing reusable form validation blocks for email, maximum and minimum length, numeric fields and required fields

In this example, the block names match the names of the Vuelidate validators because we’ll use them later to dynamically assign validation rules in the Vue component.

The email, numeric, and required blocks have an errorMessage field where the editor can specify the message shown to the user when validation fails.

Storyblok editor for an email validation block with an error message field configuration

For minLength and maxLength, we also add a param field that stores the required number of characters.

Storyblok editor for a maxLength validation block with error message and numeric parameter fields

Note: The Required setting in Storyblok’s field configuration applies to content completeness within the CMS itself. The rules described above are used to validate form input in the application.

The form field block
Next, we create an inputField block that describes a single field in the dynamic form.

Storyblok editor for an input field block with name, type, label, placeholder and validation settings

The component consists of the following fields:

  • name: a text field that allows us to assign a unique identifier to the form field <input name="" />,
  • type: a single-choice field with predefined options: text, tel, email. It determines the type of the form field <input type="" />,
  • label: a text field used as the label for the form field,
  • placeholder: a text field that will be displayed in the form field when it is empty <input placeholder="" />,
  • validators: blocks containing a list of previously created validations.

Form Component
Now, let's create the dynamicForm component for the form.

Storyblok editor for a dynamic form block with input fields, form endpoint and submit button text settings

It consists of:

  • inputs: contains a list of form fields (inputField),
  • formEndpoint: the URL where the form will be submitted,
  • submitButtonText: the name that will be displayed on the submit button.

Vue Components
Now that we have prepared the components and fields on the Storyblok side, we can proceed to create Vue components. We need two components: DynamicForm.vue, which will contain the dynamic form, and InputField.vue, where each individual form field will be located.

DynamicForm.vue
Logic
First, import the validators from the Vuelidate package and the useVuelidate function to activate the validation.

import { useVuelidate } from '@vuelidate/core'

import * as validators from '@vuelidate/validators'

In the component, you should also declare props coming from Storyblok.

const DynamicFormProps = defineProps({

 blok: {

   type: Object,

   default: () => ({})

 }

})

The initial form data is generated using the form function. It creates an object using the unique name of the form field as the key and an empty string as the value.

const form = reactive(DynamicFormProps.blok.inputs.reduce(

 (prevFields, inputField) => ({

   ...prevFields,

   [inputField.name]: ''

 }),

 {}

))

The generateFieldRules() function is responsible for generating validators for a specific form field. It creates an object consisting of the validator name as the key and its corresponding value. The value is either the default error message provided by the Vuelidate package or a function if the validator requires a parameter. For example, for a validation that defines a minimum text length, you would invoke the minLength() function and pass the parameter specifying the number of characters.

const generateFieldRules = (fieldValidators) => {

 return fieldValidators.reduce(

   (prevValidators, validator) => ({

     ...prevValidators,

     [validator.component]: validator.param ? validators[validator.component](validator.param) : validators[validator.component]

   }),

   {}

 )

}

The variable fieldRules stores the field names of the form along with the generated validators.

const fieldRules = computed(() => {

 return DynamicFormProps.blok.inputs.reduce(

   (prevFields, inputField) => ({

     ...prevFields,

     [inputField.name]: generateFieldRules(inputField.validators)

   }),

   {}

 )

})

To activate Vuelidate and make the validation work, you need to invoke the useVualidate method, passing the variables you created earlier. By doing this, you can access the data and options through v$.

const v$ = useVuelidate(fieldRules, form)

The last function is handling form submission - formSubmit(). By checking the value of the $invalid variable, we can determine if any field in the form failed validation. In such case, we invoke the $touch() method, which displays error messages in the respective form fields.

const formSubmit = (e) => {

 if (v$.value.$invalid) {

   v$.value.$touch()

   e.preventDefault()

 }

}

In order for the InputFields.vue component to use form validation, the v$ variable needs to be passed to it.

provide('v$', v$)

Combining all the described elements, we get the following logic:

<script setup>

import { useVuelidate } from '@vuelidate/core'

import * as validators from '@vuelidate/validators'

const DynamicFormProps = defineProps({

blok: {

  type: Object,

  default: () => ({})

}

})

 

const form = reactive(DynamicFormProps.blok.inputs.reduce(

(prevFields, inputField) => ({

  ...prevFields,

  [inputField.name]: ''

}),

{}

))

const generateFieldRules = (fieldValidators) => {

return fieldValidators.reduce(

  (prevValidators, validator) => ({

    ...prevValidators,

    [validator.component]: validator.param ? validators[validator.component](validator.param) : validators[validator.component]

  }),

  {}

)

}

 

const fieldRules = computed(() => {

return DynamicFormProps.blok.inputs.reduce(

  (prevFields, inputField) => ({

    ...prevFields,

    [inputField.name]: generateFieldRules(inputField.validators)

  }),

  {}

)

})

 

const formSubmit = (e) => {

if (v$.value.$invalid) {

  v$.value.$touch()

  e.preventDefault()

}

}

 

const v$ = useVuelidate(fieldRules, form)

provide('v$', v$)

</script>

Form Component Template
In this template, we create a form that has:

  • action - an attribute specifying where to send the form data after submission,
  • formSubmit - invoking the method when attempting to submit the form,
  • InputField - a component containing a single-form field,
  • button - a button triggering the form submission.

<template>

 <form v-if="v$" :id="blok._uid" class="form" method="post" :action="blok.formEndpoint" @submit="formSubmit">

   <InputField v-for="inputField in blok.inputs" :key="inputField.name" :inputField="inputField" />

   <button type="submit" class="btn">

     {{ blok.submitButtonText }}

   </button>

 </form>

</template>

InputField.vue
Logic
First, import the necessary functions for accepting data.

import { inject } from 'vue'

Then declare the props coming from the DynamicForm.vue component.

defineProps({ inputField: Object })

And finally, create a variable to have access to form validation.

const v$ = inject('v$')

InputField.vue component template
The template of a single-form field consists of:

  • Checking if the field has been filled correctly using the variable v$[inputField.name].$error. If the content is invalid, an additional class is dynamically added.
  • Binding the field data using the v-model directive between the element and the Vuelidate data model ($model).
  • Handling validation errors. This is done by iterating over the errors. If the field has a specific problem, it is displayed.

<template>

<div

  :class="{

    'form__group': true,

    'form__group--error': v$[inputField.name].$error,

  }"

>

  <label class="form__label" :for="inputField._uid">{{ inputField.label }}</label>

  <input

    :id="inputField._uid"

    v-model.trim="v$[inputField.name].$model"

    :type="inputField.type"

    :name="inputField.name"

    :placeholder="inputField.placeholder"

    class="form__input"

  >

  <div v-if="v$[inputField.name].$error">

    <div v-for="{ component, errorMessage } in inputField.validators" :key="component" class="form__group__warninig">

      <div v-if="v$[inputField.name][component].$invalid">

        {{ errorMessage }}

      </div>

    </div>

  </div>

</div>

</template>

Summary

Every developer sooner or later faces the challenge of creating dynamic forms. Thanks to them, clients can add and edit fields in forms without the need for a programmer's intervention.
The presented example can be expanded by creating additional types of fields such as radio, checkboxes, or textareas.

Tandemite team: Monika_avatar
Monika Harewska
Frontend developer @ Tandemite

FAQ

How do you connect Storyblok with Vuelidate?

In Storyblok, you define the form structure, fields, and validation rules, while Vuelidate checks the values in the application. Validation block names, such as required, email, and minLength, can match the names of Vuelidate validators. The Vue component reads the configuration from Storyblok, dynamically builds the rules, and passes them to useVuelidate().

Can editors add form fields without changing the code?

Yes, provided the form component already supports that field type. Editors can then add more inputField blocks, set their names, labels, placeholders, and types, and assign validation rules without editing the Vue code. Adding an entirely new field type that the frontend does not yet support requires a developer to extend the component first.

How do you add a new validation rule to a dynamic form?

If the validator is available in Vuelidate, you can create a corresponding block in Storyblok using the same name as in the library. Where needed, the block can also store a parameter, such as the minimum text length, and a custom error message. The rule-generation logic in Vue reads this configuration and assigns the appropriate validator. Custom validation rules require additional logic in the code.

How do you add checkbox, radio, or textarea fields?

The current example uses <input>-based fields. You can extend the same architecture to support checkbox, radio, and textarea fields. First, add the corresponding field types to the Storyblok configuration, then implement their rendering in the InputField.vue component. Once this is done, editors can select the new field types in Storyblok just as they select the existing text fields.

Questions? Curiosities? Every question you ask is a step closer to success with us

Start with a free consultation
4.9 rated by our clients on clutch

Take the first step to digital success. Get a complete guide to PIM systems for free!

Write to us

We are waiting for your message

Tandemite icon: clock

Fast contact

We will contact you within 24 hours to talk about your business needs.

Tandemite icon: paper airplane

Precise response

We will prepare an estimation of your project, considering the costs and execution time.

* Fields marked with an asterisk are required
or drop your company brief here. PDF or DOCX
You will find more information, also on your rights, in Privacy and Cookie Policy
This website is protected by reCAPTCHA and Google. Privacy policy