Skip to content

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.

typescript
// 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:

vue
<template>
  <pa-input :rules="[$rules.required(), $rules.minLength(3)]" />
</template>

You can also provide custom messages:

vue
<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:

vue
<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:

typescript
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:

typescript
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:

vue
<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:

vue
<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.

typescript
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 required validator for that. This separates required/optional qualities from the validator to make it more versatile.

Synchronous Custom Rule

typescript
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

typescript
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:

typescript
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:

vue
<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>