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.
{
"username": "",
"email": "",
"age": null,
"role": "",
"bio": ""
}Source Code
The form
<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:
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:
{
"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.
errorTextlooks the code up in the current language and substitutes the params withinterpolatefrom@dynamicforms/translatable. A code the language has no message for renders as the error'sdetail. - Markdown. With the checkbox on,
errorTextreturns the code'sMdvariant as anMdString. A code without one, such asrequired, renders as plain text. - Numbers.
formatParamsformats 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, anderrorTextnames 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-providerwithrtlset for it.
Key Features Demonstrated
- Required Validator: Fails on an empty value - a zero-length string, an empty array, an empty plain object,
nullorundefined. 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
Requiredpassesnull,undefined,'',[]and{}, so each field that must be filled also carriesRequired - Form-level Validation:
group.validcovers 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:
- Leave the username empty, or enter only spaces
- Enter an invalid email address
- Set the age outside the valid range
- Select different roles
- Enter a bio that is too short or too long
- Switch the language or the markdown setting while errors are shown
