Feature: Uniform

Overview

Basic forms can be constructed using the styled form elements available in tui/components/form, however doing things this way does not have any built-in validation or state management and is not easily composable.

To solve this problem, there is another layer on top of the form elements named Uniform that ties the plain form elements together with a declarative form state management library, Reform.

Uniform exposes a root form component, <Uniform>, plus various wrapped form inputs that can be used to create a form. These form elements do not need to be manually wired up to the root Uniform component as this happens automatically.

Implementation

Quick start

A simple Uniform form could look like this.

<template>
  <Uniform @submit="submit">
    <FormRow label="Name" required>
      <FormText name="name" :validations="v => [v.required()]" />
    </FormRow>
    <FormRow label="Favourite Battleship" required>
      <FormRadioGroup name="battleship" :validations="v => [v.required()]">
        <Radio value="hms-victory">HMS Victory</Radio>
        <Radio value="uss-enterprise">USS Enterprise</Radio>
      </FormRadioGroup>
    </FormRow>
    <FormRowActionButtons @cancel="back" />
  </Uniform>
</template>

<script>
import { Uniform, FormRow, FormText, FormRadioGroup } from 'tui/components/uniform';
import Radio from 'tui/components/form/Radio';
import FormRowActionButtons from 'tui/components/form/FormRowActionButtons';

export default {
  components: { Uniform, FormRow, FormText, FormRadioGroup, Radio, FormRowActionButtons },
  methods: {
    submit(values) {
      console.log(values);
      // => { name: "Winston Churchill", battleship: "hms-victory" }
    },
  },
};
</script>

As the form inputs are changed they will update the state inside the Uniform component, which is also where validation happens.

By default, inputs are validated when focus moves away from them, and also on every keypress after returning to the input.

Once the submit button is pressed, all validations will be run again, and if there are no errors a "submit" event will be emitted from the Uniform component with the form values as an argument.

Inputs

In Uniform forms, you need to use Uniform-connected input components. These are generally just wrapped versions of the regular form inputs, automatically generated using the createUniformInputWrapper() function, which works with any input that v-model does.

tui/components/uniform has a number of wrapped components ready to use.

Uniform inputs operate by passing and receiving data from the root Uniform component instance through Vue's provide/inject mechanism. This avoids the need to manually pass information. They also render their own field errors, passed down from the root instance.

Each input has a "name" prop that controls the name under which its data is stored.

Validation

There are several ways you can define validations for Uniform forms.

At field level, validation can be defined in one of two ways:

  • Using the validate prop, which takes a function that is passed the field value and returns null/undefined if successful or an error message if not.
  • Using the validations prop, which takes a function that is called with an object containing predefined validations as an argument, and returns an array containing validations. These validations can be of the same pattern as the validate prop takes, or they can be objects of the shape { validate(val), message() }.
<FormText :validations="v => [v.required(), customValidator(), customValidate]">
<FormText :validate="customValidate" />

<script>
export default {
  methods: {
    customValidator() {
      return {
        validate: val => val != 'no',
        message: () => 'value must not be "no"',
      };
    },
    customValidate(val) {
      if (val == 'false') return 'value must not be "false"';
    }
  }
};
</script>

Validators can also be attached at the Form and FormScope level (see "Nesting" below) by providing a validate prop. This function gets passed all the values and is expected to return errors in a matching structure.

Predefined validators

  • v.required() - field must not be empty (generally, the other validators do not fail if the field is empty)
  • v.email() - must contain a valid email address
  • v.number() - must be a number
  • v.integer() - must be a whole number
  • v.minLength(len) - must be at least len characters
  • v.maxLength(len) - must be at most len characters
  • v.min(min) - must be a number at least min
  • v.max(max) - must be a number at least max
  • v.date() - valid ISO 8601 date
  • v.colorValueHex() - valid CSS hex color (including the leading #)

Nesting

The name of an input is not just restricted to being a simple string. It can either be a string, a number, or an array of strings or numbers, that specifies a nested path under which to store the data. E.g. `:name="['users', 0, 'name']"` would produce `{ users: [{ name: 'Bob' }] }`.

This is useful to make the structure of your form match the data, e.g. you can have an "address" object inside your form, rather than having everything in a flat object.

Initial values

It is possible to pass initial values into the Uniform component to use for the fields. This is done by passing an object to the initial-values prop.

Scopes

It is possible to have nested subforms with their own validation and pre-submit transformation. The mechanism used for this is the FormScope component.

FormScope takes a path that the names of all fields inside of it will be prefixed with. It also allows you to provide a validator covering all of the fields using the "validate" prop. Like the "validate" prop at Form level, this function gets passed all of the values inside the FormScope and should return errors in a matching structure, if there are any. On submit, the "process" function is called with the values and may return any value. This will be used as the value at the FormScope's "path" in the final form result.

Lists

The FieldArray component can be used to manage lists. It exposes a slot that is called with the items in the array and provides several methods to update the list.

<FieldArray v-slot="{ items, push, remove }" path="answers">
  <Repeater
    :rows="items"
    @add="push({ answer: '' })"
    @remove="(item, i) => remove(i)"
  >
    <template v-slot="{ row, index }">
      <FormText
        :name="[index, 'answer']"
        :validations="v => [v.required()]"
        aria-label="Answer text"
      />
    </template>
  </Repeater>
</FieldArray>

Accessibility

Uniform contains built-in accessibility features. In general, fields in a FormRow will automatically be linked up to to the FormRow's label without having to manually pass around IDs.

If you have multiple inputs in a FormRow, you should make use of the InputSet or FieldGroup containers to maintain accessibility. The difference between the two is that InputSet contains layout features whereas FieldGroup is a plain container.

Tips and known limitations

  • Initial values are taken when the Uniform component is mounted. If the initial values are calculated later on, e.g. as the result of a GraphQL query, you should delay the mounting of the Uniform component until they are available, e.g. with a v-if on the <Uniform>.
  • Presently there is no way to directly set the value of fields inside Uniform state. This will be improved in the future.