Skip to content

Validation and i18n ​

The validators of @dynamicforms/vue-forms on this library's components.

Demo ​

A form using various validators. The errors are rendered by the docs' errorText setting: translated into eight languages and, with the checkbox on, as markdown. Changing either setting re-renders the errors already on the fields without revalidating.

Validators Demo
Form Validation Status
{
  "username": "",
  "email": "",
  "age": null,
  "role": "",
  "bio": ""
}

Source Code ​

The form ​

vue
<template>
  <v-locale-provider :rtl="errorLanguage === 'fa'" class="validators-form-demo">
    <v-card class="mb-4">
      <v-card-title>Validators Demo</v-card-title>
      <v-card-text>
        <div class="d-flex flex-wrap ga-4 mb-4">
          <df-select
            v-model="errorLanguage"
            :choices="languages"
            :allow-null="false"
            label="Language of the errors"
            style="max-width: 220px"
          />
          <df-checkbox v-model="markdownErrors" label="Markdown in errors" />
        </div>
        <v-form @submit.prevent>
          <!-- Username field (Required) -->
          <df-input :control="validatedForm.fields.username" label="Username" hint="Enter your username" class="mb-2" />

          <!-- Email field (Pattern) -->
          <df-input :control="validatedForm.fields.email" label="Email" hint="Enter your email address" class="mb-2" />

          <!-- Age field (ValueInRange) -->
          <df-input
            :control="validatedForm.fields.age"
            input-type="number"
            label="Age"
            :min="1"
            :max="200"
            class="mb-2"
          />

          <!-- Role field (InAllowedValues) -->
          <df-select :control="validatedForm.fields.role" :choices="roleChoices" label="Role" class="mb-2" />

          <!-- Bio field (LengthInRange) -->
          <df-text-area
            :control="validatedForm.fields.bio"
            label="Bio"
            :rows="4"
            :max-rows="10"
            hint="Enter a short bio (10-200 characters)"
            class="mb-2"
          />
        </v-form>
      </v-card-text>

      <v-card-actions class="justify-end">
        <v-btn color="secondary" @click="resetForm"> Reset </v-btn>
        <v-btn color="primary" :disabled="!formValid"> Submit </v-btn>
      </v-card-actions>
    </v-card>

    <v-card>
      <v-card-title>Form Validation Status</v-card-title>
      <v-card-text>
        <v-alert :type="formValid ? 'success' : 'error'" class="mb-3">
          Form is {{ formValid ? 'valid' : 'invalid' }}
        </v-alert>
        <pre class="output">{{ JSON.stringify(validatedForm.value, null, 2) }}</pre>
      </v-card-text>
    </v-card>
  </v-locale-provider>
</template>

<script setup>
import { Group, Field, transaction, ValueChangedAction, Validators } from '@dynamicforms/vue-forms';
import { computed } from 'vue';

import { DfCheckbox, DfInput, DfSelect, DfTextArea } from '../../src';

import { errorLanguage, languages, markdownErrors, roleName } from './demo-error-text';

// a role is stored as its code and shown by its name in the language of the errors
const roles = ['admin', 'user', 'guest'];
const roleChoices = computed(() => roles.map((id) => ({ id, text: roleName(id) })));

// Create a form group with validated fields
const validatedForm = new Group({
  // Required field - cannot be empty, and a value of spaces alone is empty too
  username: new Field({
    value: '',
    validators: [new Validators.Required()],
  }),

  // Email field with pattern validation
  email: new Field({
    value: '',
    validators: [
      new Validators.Required(),
      new Validators.Pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, {
        code: 'invalid_email',
        detail: 'Please enter a valid email address',
      }),
    ],
  }),

  // Number field with range validation
  age: new Field({
    value: null,
    validators: [new Validators.Required(), new Validators.ValueInRange(18, 100)],
  }),

  // Field with allowed values validation
  role: new Field({
    value: '',
    validators: [new Validators.Required(), new Validators.InAllowedValues(roles)],
  }),

  // Text field with length validation
  bio: new Field({
    value: '',
    validators: [new Validators.Required(), new Validators.LengthInRange(10, 200)],
  }),
});

// group.valid covers the group's own errors and those of every field it holds
const formValid = computed(() => validatedForm.valid);

// Function to reset the form. The five writes are one transaction, so the group announces one change rather than
// five, and a handler that throws leaves the form as it was.
function resetForm() {
  transaction(() => {
    validatedForm.fields.username.value = '';
    validatedForm.fields.email.value = '';
    validatedForm.fields.age.value = null;
    validatedForm.fields.role.value = '';
    validatedForm.fields.bio.value = '';
  });
}

// A value changed action on the group fires whenever what the group holds changes: a field written. The handler
// stays synchronous: the write that triggers it returns once the chain has run, so nothing
// awaits a promise it hands back.
validatedForm.registerAction(
  new ValueChangedAction((field, supr, newValue, oldValue) => {
    console.log('Form value has changed');
    return supr(field, newValue, oldValue);
  }),
);
</script>

<style>
.validators-form-demo {
  margin: 2rem 0;
}
.output {
  background-color: #f5f5f5;
  padding: 1rem;
  border-radius: 4px;
  white-space: pre-wrap;
}

hr {
  margin: 0 !important;
}
</style>

The errorText setting ​

The docs install the plugin with app.use(DynamicFormsInputs, { errorText }), with errorText from this module:

ts
import { formatParams, interpolate } from '@dynamicforms/translatable';
import { ref } from 'vue';

import { type ErrorText, MdString } from '../../src';

import messages from './demo-error-messages.json';

type Language = keyof typeof messages;

export const languages: { id: Language; text: string }[] = [
  { id: 'en', text: '🇬🇧 English' },
  { id: 'sl', text: '🇸🇮 Slovenščina' },
  { id: 'de', text: '🇩🇪 Deutsch' },
  { id: 'es', text: '🇪🇸 Español' },
  { id: 'ja', text: '🇯🇵 日本語' },
  { id: 'zh', text: '🇨🇳 中文' },
  { id: 'fa', text: '🇮🇷 فارسی' },
  { id: 'bn', text: '🇧🇩 বাংলা' },
];

// the language and the markdown setting every demo's errors are rendered in
export const errorLanguage = ref<Language>('en');
export const markdownErrors = ref(true);

// the message for a key in the current language with the params substituted, or the key where there is none
const t = (key: string, params: Record<string, unknown>) => {
  const template = (messages[errorLanguage.value].errors as Record<string, string>)[key];
  return template === undefined ? key : interpolate(template, params);
};

// numbers in the language's digits
const tf = formatParams(t, (value) =>
  typeof value === 'number' ? new Intl.NumberFormat(errorLanguage.value).format(value) : value,
);

// a role is stored as its code and shown by its name in the current language
export const roleName = (role: string) => (messages[errorLanguage.value].roles as Record<string, string>)[role] ?? role;

/**
 * The docs' errorText setting: the `Md` variant of the code's message as markdown while `markdownErrors` is set, the
 * plain message otherwise, and the error's detail for a code without a message.
 */
export const errorText: ErrorText = (error) => {
  const params =
    error.code === 'in_allowed_values'
      ? {
          allowedAsText: (error.params.allowedValues as string[]).map(roleName).join(', '),
        }
      : error.params;
  if (markdownErrors.value) {
    const markdown = tf(`${error.code}Md`, params);
    if (markdown !== `${error.code}Md`) return new MdString(markdown);
  }
  const text = tf(error.code, params);
  return text === error.code ? undefined : text;
};

The messages, in demo-error-messages.json, hold each code's message and, for some codes, a markdown variant under the code with Md appended:

json
{
  "sl": {
    "errors": {
      "required": "Prosimo, vnesite vrednost",
      "value_in_range": "Vrednost mora biti med {minValue} in {maxValue}",
      "value_in_rangeMd": "Vrednost mora biti med **{minValue}** in **{maxValue}**",
      "invalid_email": "Vnesite veljaven e-poštni naslov",
      "invalid_emailMd": "Vnesite veljaven **e-poštni naslov**"
    },
    "roles": { "admin": "Skrbnik", "user": "Uporabnik", "guest": "Gost" }
  }
}

Errors in This Demo ​

Every error has a code, params and an English detail. The validators built without options carry the validator's own code (required, value_in_range, …); the email pattern is given { code: 'invalid_email', detail }.

  • Messages. errorText looks the code up in the current language and substitutes the params with interpolate from @dynamicforms/translatable. A code the language has no message for renders as the error's detail.
  • Markdown. With the checkbox on, errorText returns the code's Md variant as an MdString. A code without one, such as required, renders as plain text.
  • Numbers. formatParams formats the params before they are substituted, so Persian and Bengali show the age range in their own digits.
  • Role names. A role is stored as its code. The error's params carry the codes in allowedValues, and errorText names them in the current language, as the select's items do.
  • Right to left. Persian is written right to left; the demo wraps itself in Vuetify's v-locale-provider with rtl set for it.

Key Features Demonstrated ​

  • Required Validator: Fails on an empty value - a zero-length string, an empty array, an empty plain object, null or undefined. A string is measured after it is trimmed, so a field holding nothing but spaces is empty and invalid; new Validators.Required({ trim: false }) measures the string untrimmed. Only strings are trimmed - an array, an object or any other value is measured as it is
  • Pattern Validator: Tests String(value) against a regular expression (email format here)
  • ValueInRange Validator: Fails when the value is below the minimum or above the maximum, or cannot be compared with the bounds. Numbers compare with numbers, strings with strings and dates with dates
  • InAllowedValues Validator: Restricts input to a predefined set of values, read at each validation rather than at construction
  • LengthInRange Validator: Validates that the length of the value is within the given bounds; strings, arrays and plain objects are measured by their own length, anything else by the length of String(value)
  • Empty values: every validator except Required passes null, undefined, '', [] and {}, so each field that must be filled also carries Required
  • Form-level Validation: group.valid covers the group's own errors and those of every field it holds
  • Error display: the shown errors of each field, rendered by errorText

Try It Yourself ​

In the demo:

  1. Leave the username empty, or enter only spaces
  2. Enter an invalid email address
  3. Set the age outside the valid range
  4. Select different roles
  5. Enter a bio that is too short or too long
  6. Switch the language or the markdown setting while errors are shown

Released under the MIT License.