# Form handling

TYPO3 Headless supports Form Framework (opens new window) and nuxt-typo3 supports form handling.

Forms handling is disabled by default. You can enable it in nuxt.config

  typo3: {
    forms: true
  },

# TYPO3 Headless Limitations

Please note, that currently we are experiencing following limitations on API level:

  1. TYPO3 Headless supports only one step forms (however, you are still able to build multistep forms on frontend side - guide soon).
  2. You have to enable specific headless.elementBodyResponse feature flag (opens new window) to handle POST requests more easily.
  3. You have to add custom finishers to handle success state and redirects (headless.redirectMiddlewares ) (opens new window) in a more sophisticated way.

Look at code example (opens new window)

# Component data flow

To handle Form Framework (opens new window) we have implemented FormFramework Content Element (opens new window) which uses T3Form component (opens new window). This is good example of distribution UI components and Content elements where UI is responsible for displaying interface based on delivered props. In that specific case FormFramework Content Element is responsible for delivering props to UI and submiting form data to API. You can use T3Form component (opens new window) for more specific cases, not only for T3FormFramework - you can build your own forms, not related with FormFramework ContentElement. One thing you have to do is use our T3Form schema. You may also override whole T3Form component to implement complicated scenarios. However, before you decide to do so, please read this documentation. We have provided a lot of ways of customising forms.

For model validation we have used VeeValidate (opens new window) plugin which in our opinion is the best way to validate forms in Vue.js applications.

# FormFramework Content Element

Entry point for form handling is T3CeFormFormframework (opens new window) and this component is responsible for wraping T3Form and exchaning information between them. At this level you can override form template, default I18n labels and customize mapping fields to T3Form schema.

# Customize T3CeFormFormframework

To customize T3CeFormFormframework you have to register a new one with the same name. This is common solution to override global components (opens new window).

To do this please create a new file components/T3CeFormFormframework.vue:

<template>
  <div>form markup</div>
</template>
<script>
import T3CeFormFormframework from '~typo3/components/T3CeFormFormframework/T3CeFormFormframework.vue'
export default {
  extends: T3CeFormFormframework
}
</script>

Please notice it extends logic from T3CeFormFormframework

Now register it as global component in plugins/components.js file

import Vue from 'vue'
import T3CeFormFormframework from '~/components/T3CeFormFormframework.vue'
Vue.component('T3CeFormFormframework', T3CeFormFormframework)

Please remember to add components.js file to nuxt plugins (opens new window). After that you should be able to see your new component.

# Customize markup and logic

This FormFramewor Content element is mainly responsible for the logic of form submiting. If it comes to markup, it wraps the T3Form in order to pass formData elements and init methods to form events. Please remember that whole form is generated based on elements prop which is delivered by API. To override T3Form template on this level you can use slots (Take a look at the snippet below):

  • before - inside <form> tag - before field list render
  • after - inside <form> tag - after field list render
  • cta - submit/reset button template
  • fields - template for fields rendering - not recommended to override here, there is easier way to customize form fields - read it.

If it comes to logic, you may want to handle your forms in more complex scenario. For example currently we support redirect as a finisher of the form (if the finisher exist), but you can add more ways to handle it. You can override onSuccess method.

At this level you can also provide custom css classes for your fields. components/T3CeFormFormframework.vue:

<template>
  <T3Form
    ref="form"
    :elements="elements"
    :classes="css"
    @submit="onSubmit"
  >
    <template #before="{model}">
      {{ model }}
    </template>

    <template #cta>
      <button type="submit">
        Send form
      </button>
    </template>

    <template #after="{state}">
      <p v-if="state.error || state.failture">
        {{ i18n.serverError }}
      </p>
      <p v-if="state.success">
        {{ i18n.serverSuccess }}
      </p>
    </template>
  </T3Form>
</template>
<script>
import T3CeFormFormframework from '~typo3/components/T3CeFormFormframework/T3CeFormFormframework.vue'
export default {
  extends: T3CeFormFormframework,
   data () {
    return {
      css: Object.freeze({
        // field.identifier : class name
        name: 'wrap-my-name-field'
      }),
      // map TYPO3 Form Framework validation rules to vee-validate schema
      rules: {
        NotEmpty: 'required',
        EmailAddress: 'email',
        RegularExpression: {
          identifier: 'regex',
          options: {
            regex: 'pattern'
          }
        }
      }
    }
  },
  methods: {
    onSuccess (actionAfterSuccess) {
      if (actionAfterSuccess?.redirectUri) {
        this.$router.push(actionAfterSuccess.redirectUri)
      } else {
        // do something else
      }
    },
  }
}
</script>

# Customize field templates

Each form field uses T3FormField (opens new window) component as the base template. You can override this template:

Create and register global components components/T3FormField.vue:

<template>
  <ValidationProvider
    v-slot="state"
    :vid="field.identifier"
    :name="field.label"
    :rules="rules"
    :custom-messages="messages"
    slim
  >
    <div class="field-row">
      <label :for="field.identifier"> {{ field.label }} </label>
      <slot :state="state">
        <input
          :id="field.identifier"
          v-model="innerValue"
          :type="field.type"
          :label="field.label"
          :name="field.name || field.identifier"
          :class="state.classes"
          :placeholder="placeholder"
          :required="required"
          @input="(event) => $emit('input', event.target.value)"
        >
      </slot>
      <ul v-if="state.errors.length" class="t3-form-field__errors">
        <li v-for="(error, key) in state.errors" :key="key">
          {{ error }}
        </li>
      </ul>
    </div>
  </ValidationProvider>
</template>
<script>
import { T3FormField } from '~typo3/components/T3Form'
export default {
  name: 'T3FormField',
  extends: T3FormField
}
</script>
<style scoped>
.field-row {
  display: flex;
  align-items: center;
  margin: 10px 0;
}

label {
  font-weight: bold;
  margin-right: 10px;
}

ul {
  margin: 0;
  color: red;
}
</style>

This component uses <ValidationProvider/> (opens new window) for field validation. This template will be suitable for most regular input elements. You can use this template to provide more form types like textarea, checkbox, etc.

# Field list rendering

To be able to easily customize template fields, it's important to understand how we generate field list.


All fields are generated in the loop, based on T3Form schema. T3FormFormFramework component is responsible for mapping fields to this strategy.

{
  "elements": [
    {
      "type": "fieldset",
      "fieldlist": true,
      "identifier": "fieldset-1",
      "label": "Personal Information",
      "value": null,
      "elements": [
        {
          "value": "",
          "validators": [
            {
              "identifier": "email",
              "message": "You must enter a valid email address."
            },
            {
              "identifier": "required",
              "message": "This field is mandatory."
            }
          ],
          "type": "email",
          "identifier": "email",
          "label": "Email",
          "placeholder": "your email",
          "required": true,
          "name": "tx_form_formframework[email]"
        },
        {
          "value": "",
          "type": "text",
          "identifier": "name",
          "label": "Name",
          "description": "",
          "required": true,
          "validators": [
            {
              "identifier": "required",
              "message": "This field is mandatory."
            }
          ],
          "name": "tx_form_formframework[name]"
        },
        {
          "value": "+49",
          "type": "text",
          "identifier": "phone",
          "label": "Telephone",
          "placeholder": "00-000-000-000",
          "validators": [
            {
              "options": {
                "regex": "^[0-9-+]+$"
              },
              "identifier": "regex",
              "message": "You must enter a valid value. Please refer to the description of this field."
            }
          ],
          "name": "tx_form_formframework[phone]"
        }
      ]
    }
  ]
}

You may have noticed that name and email and phone fields are nested in fieldset-1 which is nested in fieldset-1 and fieldset-1 is fieldlist type to generate nested elements. It means we have to render component fields in recursion. T3FormFieldList component (opens new window) is responsible for rendering this list.

This part of the template is responsible for matching frontend component with field.type or field.identifier.

<component
  :is="getComponentField(field)"
  v-model="model[field.name]"
  :field="field"
/>

For example: to display and render input type="hidden" we had to register T3FormFieldHidden.

If you want to add custom textarea field with type "textarea" then you have to register T3FormFieldTextarea

You can also register custom field component for specific field - T3FormFieldName if your field.identifier === 'name'

Default one is T3FormField

# Add new field type

At this moment we support

  1. regular input fields like text, email, number
  2. single select
  3. fieldset
  4. honeypot == hidden

But you can easliy add new field type.

For example we can add FormFieldCheckbox. In that case add new global component components/T3FormFieldCheckbox.vue:

<template>
  <T3FormField v-bind="$props">
    <template #default="{state}">
      <input
        :id="field.identifier"
        v-model="innerValue"
        type="checkbox"
        :label="field.label"
        :name="field.name || field.identifier"
        :class="state.classes"
        :required="required"
        :true-value="true"
        false-value=""
        @input="(event) => $emit('input', event.target.value)"
      >
    </template>
  </T3FormField>
</template>
<script>
import { T3FormField } from '~typo3/components/T3Form'
export default {
  name: 'T3FormFieldCheckbox',
  components: {
    T3FormField
  },
  extends: T3FormField
}
</script>

TIP

Notice that we have used default T3FormField as the base template for new form field type. Thanks to that we can create multiple form types with the same template. On the other hand you can put all input types in one FormField component and match them by v-if.

# Add new validation rule

For model validation we use vee-validate.

If you want to use validation rules from vee-validate you have to map validation rules which come from API to vee-validate naming.

Example in T3FormFormFramework:

rules: {
  NotEmpty: 'required',
  EmailAddress: 'email',
  RegularExpression: {
    identifier: 'regex',
    options: {
      expression: 'regex'
    }
  }
}

If you want to extend these rules please override T3FormFormFramework component and rules data object.

For example:

<script>
import T3CeFormFormframework from '~typo3/components/T3CeFormFormframework/T3CeFormFormframework.vue'
export default {
  extends: T3CeFormFormframework,
   data () {
    return {
      // map TYPO3 Form Framework validation rules to vee-validate schema
      rules: {
        Match: 'confirmed',
        NotEmpty: 'required',
        EmailAddress: 'email',
        RegularExpression: {
          identifier: 'regex',
          options: {
            regex: 'pattern'
          }
        }
      }
    }
  }
}
</script>

If you don't want to map rules, then you can add own rule:

import { extend } from 'vee-validate';

extend('Length', {
  validate(value, args) {
    return value.length >= args.length;
  },
  params: ['length']
});

All additional rules are available here (opens new window).

# Add new rule

Instruction here (opens new window)