Validation
Original Storybook Validation Docs.
This documentation was retained originally from storybook, pass required to ensure it is all still valid information.
The PayAdvantage UI component library provides a comprehensive validation system for form components. This guide covers how to implement validation in your components and use the built-in validation rules.
Adding Validation to a Component
To add validation support to a component, include the Validation mixin and extend the getValueForValidation method.
Note: The mixin does not support the Composition API. A Composition API version will be made available in the future.
// This is the bare minimum that you need to add validation support to a component.
export default defineComponent({
mixins: [Validation],
template: '<input type="text" v-model="inputValue">',
data() {
return {
inputValue: ''
}
},
methods: {
getValueForValidation(): string {
return this.inputValue
}
}
})Using Rules
Validation is no fun without rules. There are two ways to define rules for a component: explicitly and implicitly.
Explicit Rules
Explicit rules are added by the parent component. When defining the component, you specify the rules that should be applied.
Rules are always provided as an array and all validation rules are functions:
<template>
<pa-input :rules="[$rules.required(), $rules.minLength(3)]" />
</template>You can also provide custom messages:
<template>
<pa-input :rules="[$rules.required('The field is required'), $rules.minLength(3)]" />
</template>Some validators support custom formatting of the value within the default message:
<template>
<pa-input :rules="[$rules.required(), $rules.minLength(3, null, formatCurrency)]" />
</template>Implicit Rules
Implicit rules are rules that the component adds automatically based on its props or configuration.
An example would be a component that has a boolean required prop and automatically applies the required validation rule.
To add implicit rules, override the implicitRules computed property and return an array of rules:
export default defineComponent({
mixins: [Validation],
template: '<input type="text" v-model="inputValue">',
props: {
required: { type: Boolean, default: false }
},
computed: {
implicitRules(): ValidationRule[] {
const rules: ValidationRule[] = []
if (this.required) {
rules.push(this.$rules.required())
}
return rules
}
}
})Validating on Change / Blur
We use validation on field change to update the validity of fields as the user works through the form.
When implementing this, always use async validation as it has the greatest support for validation rules:
export default defineComponent({
mixins: [Validation],
template: '<input type="text" v-model="inputValue" @blur="onBlur">',
data() {
return {
inputValue: ''
}
},
methods: {
getValueForValidation(): string {
return this.inputValue
},
onBlur(): void {
this.checkValidityAsync()
}
}
})HTML5 Validation
To add support for HTML5 validation, override the validityChanged() method and set the validity on the element.
Below is a snippet from pa-input:
<template>
<input ref="input" />
</template>
<script lang="ts">
export default defineComponent({
name: 'PaInput',
mixins: [Validation],
methods: {
validityChanged(): void {
// validityChanged is called when the validation state changes.
// Copy the new validation message onto the element to set its validity.
(this.$refs.input as HTMLInputElement).setCustomValidity(this.validationMessage)
}
}
})
</script>HTML Attribute Mapping
For cases when HTML5 validation needs to be connected to an HTML element, you may need to examine the validators.
Below is a snippet from pa-input that allows required to be set by a prop or by a rule:
<template>
<input
ref="input"
v-model="internalValue"
:required="requiredInternal"
/>
</template>
<script lang="ts">
export default defineComponent({
name: 'PaInput',
mixins: [Validation],
props: {
required: {
type: Boolean,
default: false
}
},
computed: {
implicitRules(): ValidationRule[] {
const rules: ValidationRule[] = []
if (!hasRule('required', this.rules) && this.required) {
rules.push($rules.required())
}
return rules
},
requiredInternal(): boolean {
return !!this.required || hasRule('required', this.allRules)
}
}
})
</script>Async Validation
Async validation is supported. However, the component performing validation must be aware of this. If it uses sync validation, it will throw an error if it encounters an async validator.
For the best compatibility, components should always use the async version for validation.
Sync validation is the default as it allows backwards compatibility with existing code and HTML compliant forms. For most cases this is fine as async validators are still rare.
await this.submitAsync()
await this.checkValidityAsync()Built-in Validation Rules
All of these rules are available in components by accessing the $rules property.
String and Array Rules
required(message?: string)- Checks if a value is required- Supports: string, array, Date
notSet(message?: string)- Checks if a value is not set- Supports: string, array, Date
minLength(minimumLength: number, message?: string | null, formatter?: ValueFormatter | null)- Ensures minimum length- Supports: string, Array
maxLength(maximumLength: number, message?: string | null, formatter?: ValueFormatter | null)- Ensures maximum length- Supports: string, Array
Numeric and Date Rules
minValue(minimumValue: number | Date, message?: string | null, formatter?: ValueFormatter | null)- Validates minimum value- Supports: number, Date
maxValue(maximumValue: number | Date, message?: string | null, formatter?: ValueFormatter | null)- Validates maximum value- Supports: number, Date
between(minimumValue: number | Date, maximumValue: number | Date, message?: string | null, formatter?: ValueFormatter | null)- Validates value is between min and max- Supports: number, Date
Currency Rules
minCurrency(minimumValue: number, message?: string | null, formatter?: ValueFormatter | null)- Validates minimum currency value- Supports: number
maxCurrency(maximumValue: number, message?: string | null, formatter?: ValueFormatter | null)- Validates maximum currency value- Supports: number
betweenCurrency(minimumValue: number, maximumValue: number, message?: string | null)- Validates currency is between min and max- Supports: number
Equality Rules
equals(equalToValue: ValidatableType, message?: string | null, formatter?: ValueFormatter | null)- Validates equality- Supports: number, Date, string, boolean, Array, object (deep)
notEquals(equalToValue: ValidatableType, message?: string | null, formatter?: ValueFormatter | null)- Validates inequality- Supports: number, Date, string, boolean, Array, object (deep)
equalsCurrency(equalToValue: ValidatableType, message?: string | null, formatter?: ValueFormatter | null)- Validates currency equality- Supports: number
notEqualsCurrency(equalToValue: ValidatableType, message?: string | null, formatter?: ValueFormatter | null)- Validates currency inequality- Supports: number
String Pattern Rules
regex(expression: RegExp, message?: string | null)- Validates against regular expression- Supports: string
hasNonSpace(message?: string | null)- Validates string contains at least one non-space character- Supports: string
hasWord(message?: string | null)- Validates string contains a word- Supports: string
email(message?: string | null)- Validates email address format- Supports: string
dob(message?: string | null)- Validates date of birth format (dd/mm/yyyy)- Supports: string
Custom Rules
You can build your own validation rules using the buildRule and buildRuleAsync methods.
Note: Rules should not validate that a value is set. There is a
requiredvalidator for that. This separates required/optional qualities from the validator to make it more versatile.
Synchronous Custom Rule
export function required(message?: string | null): ValidationRuleSync {
return buildRule(
{ type: 'required' },
(value: ValidatableType) => {
const failedMessage = message || 'Required'
if (value === null || value === undefined) {
return failedMessage
}
if (typeof value === 'string') {
return value.length > 0 ? '' : failedMessage
}
throw new ValidationError(`Cannot validate ${format(value)}`, value)
}
)
}Asynchronous Custom Rule
export function validBsb(message?: string | null): ValidationRuleAsync {
return buildRuleAsync(
{ type: 'validBsb' },
async (value: ValidatableType) => {
const failedMessage = message || 'Invalid BSB Number'
// Use the required rule if it needs to have a value
if (value === null || value === undefined) {
return ''
}
try {
const bsb = await bsbService.lookup(value)
return ''
} catch (e) {
if (e.status === 404) {
return 'Invalid BSB Number'
}
throw new ValidationError(e.message)
}
}
)
}Value Formatters
Some validators allow for custom formatting of the value within the default message.
For example: <pa-input :rules="[minValue(null, currencyFormatter)]"> will tell the minValue validator to use the currencyFormatter to render the failure message.
Custom Value Formatters
Writing a custom formatter is straightforward:
export function formatTime(value: ValidatableType): string {
if (value === null || value === undefined) {
return ''
}
if (value instanceof Date) {
const hour = value.getHours()
return `${(hour % 12) || 12}:${value.getMinutes().toString().padStart(2, '0')}:${value.getSeconds().toString().padStart(2, '0')} ` +
(hour >= 12 ? 'pm' : 'am')
}
return `Cannot format the value ${value}`
}Example Usage
Here's a complete example of using validation in a form:
<template>
<pa-form>
<pa-input
v-model="email"
label="Email"
:rules="[$rules.required('Email is required'), $rules.email()]"
/>
<pa-currency-input
v-model="amount"
label="Amount"
:rules="[$rules.required(), $rules.minCurrency(0.01, 'Amount must be at least $0.01')]"
/>
<pa-input
v-model="password"
type="password"
label="Password"
:rules="[$rules.required(), $rules.minLength(8, 'Password must be at least 8 characters')]"
/>
</pa-form>
</template>
<script setup>
import { ref } from 'vue'
const email = ref('')
const amount = ref(null)
const password = ref('')
</script>