Form fields

Form fields provides a way to build forms using configuration.

Examples

<script>
  const required = (failMessage) => (val) =>
    val !== '' && val !== null && val !== undefined ? '' : failMessage;
  const mapToNumber = (x) => Number(x || 0);

  export default {
    data() {
      return {
        items: ['Pizza', 'Keyboards', 'Guitars', 'Rocket ships'].map((text) => ({
          text,
          value: text,
        })),
        fields: {
          USERNAME: {
            label: 'NAME (ALL CAPS)',
            mapValue: (x) => (x ? x.toUpperCase() : x),
            validators: [required('NAME IS REQUIRED!!!')],
          },
          password: {
            label: 'Password with group styling',
            inputAttrs: { type: 'password' },
            groupAttrs: { class: 'gl-bg-purple-50 gl-w-20' },
            validators: [required('Password is required')],
          },
          confirmPassword: {
            label: 'Confirm Password',
            inputAttrs: { type: 'password' },
            validators: [required('Confirmed password is required')],
          },
          custom: {
            label: 'Custom input',
            mapValue: mapToNumber,
            validators: [(val) => (val < 1 ? 'Please click this at least once :)' : '')],
          },
          favoriteItem: {
            label: 'Favorite Item',
            groupAttrs: {
              optional: true,
              'optional-text': '(optional)',
            },
          },
          favoriteFood: {
            label: 'Favorite Food',
            fieldset: true,
            groupAttrs: {
              optional: true,
              'optional-text': '(select all that apply)',
            },
            validators: [
              (val) => (!val || val.length === 0 ? 'Please select at least one option' : ''),
            ],
          },
          acknowledge: {
            label: null,
            validators: [(val) => (val === true ? '' : 'Acknowledge before submitting!')],
          },
        },
        formValues: {},
        testFormId: 'form_fields_story',
        serverValidations: {},
        loading: false,
        foodOptions: [
          { text: 'Burgers', value: 'Burgers' },
          { text: 'Pizza', value: 'Pizza' },
          { text: 'Sushi', value: 'Sushi' },
        ],
      };
    },
    created() {
      this.fields.confirmPassword.validators.push(
        (confirmValue) => (confirmValue !== this.formValues.password ? 'Must match password' : '')
      );
    },
    computed: {
      values() {
        const { confirmPassword, ...rest } = this.formValues;
        return rest;
      },
      valuesJSON() {
        return JSON.stringify(this.values, (key, value) => (value === undefined ? null : value), 2);
      },
      favoriteItemToggleText() {
        if (!this.formValues.favoriteItem) {
          return 'Select an item';
        }
        return null;
      },
    },
    methods: {
      onInputField({ name }) {
        this.$delete(this.serverValidations, name);
      },
      async onSubmit() {
        this.loading = true;

        await new Promise((resolve) => {
          setTimeout(resolve, 1000);
        });

        this.loading = false;

        if (this.formValues.USERNAME === 'FOO') {
          this.$set(this.serverValidations, 'USERNAME', 'Username has already been taken.');
          return;
        }

        this.$refs.modal.show();
      },
    },
  };
</script>
<template>
  <div>
    <h3>Fields</h3>
    <form :id="testFormId" @submit.prevent>
      <gl-form-fields
        :fields="fields"
        v-model="formValues"
        :form-id="testFormId"
        :server-validations="serverValidations"
        validate-on-blur
        @input-field="onInputField"
        @submit="onSubmit"
      >
        <template #group(confirmPassword)-label>
          <div class="gl-flex gl-items-center gl-gap-x-3">
            <span>Confirm Password</span>
            <gl-icon name="information-o" />
          </div>
        </template>
        <template #group(confirmPassword)-description>
          Description using <code>group(confirmPassword)-description</code> slot.
        </template>
        <template #after(confirmPassword)>
          <gl-alert class="gl-mb-5" :dismissible="false"
            >Custom content using <code>after(confirmPassword)</code> slot.</gl-alert
          >
        </template>
        <template #input(custom)="{ id, value, input, blur }">
          <button :id="id" @click="input(value + 1)" @blur="blur" type="button">{{ value }}</button>
        </template>
        <template #input(favoriteItem)="{ id, value, input, blur }">
          <gl-listbox
            :toggle-id="id"
            :items="items"
            :selected="value"
            :toggle-text="favoriteItemToggleText"
            @select="input"
            @hidden="blur"
          />
        </template>
        <template #group(favoriteItem)-label-description>
          Label description using <code>group(favoriteItem)-label-description</code> slot.
        </template>
        <template #input(favoriteFood)="{ id, value, input, validation }">
          <gl-form-checkbox-group
            :id="id"
            :options="foodOptions"
            :checked="value || []"
            :state="validation.state"
            @input="input"
          />
        </template>
        <template #input(acknowledge)="{ id, input, validation, value }">
          <gl-form-checkbox :state="validation.state" :id="id" :checked="value" @input="input">
            I accept the terms and conditions
          </gl-form-checkbox>
        </template>
      </gl-form-fields>
      <gl-button type="submit" category="primary" :loading="loading">Submit</gl-button>
    </form>
    <gl-modal ref="modal" modal-id="submission-modal" title="Form submission"
      ><pre>{{ valuesJSON }}</pre></gl-modal
    >
  </div>
</template>

Structure

TODO:
Add structure image. Create an issue

Guidelines

TODO:
Add guidelines. Create an issue

Appearance

TODO:
Add appearance. Create an issue

Behavior

TODO:
Add behavior. Create an issue

Accessibility

TODO:
Add accessibility. Create an issue

Code reference

Usage

GlFormFields provides form builder functionality for ease of building simple forms out of other GitLab UI form components.

For a code example, look at the story. It covers usage of mapValue, validators, custom form elements, and inputAttrs.

Fields type

Each value of fields prop is expected to be a FieldDefinition. See below for the shape of this type:

interface FieldDefinition<TValue> {
  // Label text to show for this field.
  // When explicitly set to null, the label is suppressed.
  // When undefined or not provided, the field name is used as the label.
  label?: string | null;

  // Collection of validator functions
  validators?: Array<(value: TValue) => string | undefined>;

  // Function that maps the inputted string value to the field's actual value
  // (e.g. a Number).
  mapValue?: (input: string) => TValue;

  // Properties that are passed to the actual input for this field.
  inputAttrs?: {};

  // Properties that are passed to the group wrapping this field.
  groupAttrs?: {};

  // When true, renders the form group as a fieldset with legend instead of div with label.
  fieldset?: boolean;
}

Label behavior

  • label: "Custom Label" - Renders the provided label
  • label: null - Suppresses the label (no label rendered)
  • label: undefined or not provided - Uses the field name as the label (default behavior)

Slots

NameDescription
input(<fieldName>)Used to render components other than GlFormInput.
group(<fieldName>)-labelUsed for label slot on GlFormGroup of a specific field.
group(<fieldName>)-descriptionUsed for description slot on GlFormGroup of a specific field.
group(<fieldName>)-label-descriptionUsed for label-description slot on GlFormGroup of a specific field.
after(<fieldName>)Used to render content after GlFormGroup of a specific field.

GlFormFields

import { GlFormFields } from '@gitlab/ui';

Props

Name
Description
Default

fields Required

{ [key: string]: FieldDefinition } Object of keys to FieldDefinitions. The shape of the keys will be the same for `values` and what's emitted by the `input` event.

v-model Required

object The current value for each field, by key. Keys should match between `values` and `fields`.

formId Required

string The id of the form element to handle "submit" listening.

serverValidations

object Validation errors from the server. Generally passed to the component after making an API call.

{}

validateOnBlur

boolean Whether to validate fields on blur. When set to false, validation will only occur on form submission.

true

Slots

Name
Description
slotName

Can be used to pass slots to `GlFormGroup`.

field.inputSlot.slotName

Scoped slot that can be used for components other than `GlFormInput`. The name of the slot is `input(<fieldName>)`.

field.afterSlotName

Can be used to add content the form group of a field. The name of the slot is `after(<fieldName>)`.

Events

Name
Description
input

undefined Emitted when any of the form values change. Used by `v-model`.

field-validation

undefined

input-field

undefined Emitted when a form input emits the `input` event.

submit

undefined Emitted when the form is submitted and all of the form fields are valid.

Last updated at: