Form handling

TYPO3 Headless supports Form Framework and nuxt-typo3 supports form handling.

Please note that forms are a separate module available through the EAP program. Contact us to gain access.

Configuration

Please read EAP configuration section before.

npm
npm install @t3headless/nuxt-typo3-forms
yarn
echo //git.macopedia.pl/api/v4/projects/892/packages/npm/:_authToken=${NPM_TOKEN} >> .npmrc
yarn add @t3headless/nuxt-typo3-forms
pnpm
echo //git.macopedia.pl/api/v4/projects/892/packages/npm/:_authToken=${NPM_TOKEN} >> .npmrc
pnpm i @t3headless/nuxt-typo3-forms
nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@t3headless/nuxt-typo3',
    '@t3headless/nuxt-typo3-forms'
  ],
  typo3: {
    api: {
      baseUrl: 'https://api.t3pwa.com'
    }
  }
})

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 to handle POST requests more easily.
  3. You have to add custom finishers to handle success state and redirects (headless.redirectMiddlewares ) in a more sophisticated way.

Look at code example

Component data flow

To handle Form Framework we have implemented FormFramework Content Element which uses T3Form component. 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 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 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 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.

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

<template>
 <div class="t3-ce-form-framework">
  <T3Form
    v-if="props.form"
    ref="t3form"
    :elements="elements"
    :i18n="{}"
    @submit="onSubmit"
  />
 </div>
</template>
<script setup lang="ts">
 import { ref } from 'vue'
 import { useT3CeFormFormframework } from '#typo3/components/T3CeFormFormframework/useT3CeFormFormframework'
 import type { T3CeFormFormframeworkProps, Form } from '@t3headless/nuxt-typo3'
 import T3Form from '#typo3/components/T3Form/T3Form.vue'

 const t3form = ref<Form | null>(null)
 const props = defineProps<T3CeFormFormframeworkProps>()
 const { elements, onSubmit, responseMessage } = useT3CeFormFormframework(props, t3form)
</script>

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.

Override internalization messages

You can override the internationalization (i18n) message used in the T3FormFormFramework by passing a custom i18n object via the i18n prop.

<template>
 <div class="t3-ce-form-framework">
  <T3Form
    v-if="props.form"
    ref="t3form"
    :elements="elements"
    :i18n="customI18n"
    @submit="onSubmit"
  />
 </div>
</template>
<script setup lang="ts">
 import { ref } from  'vue'
 import { useT3CeFormFormframework } from '#typo3/components/T3CeFormFormframework/useT3CeFormFormframework'
 import type { T3CeFormFormframeworkProps, Form } from '@t3headless/nuxt-typo3'
 import T3Form from '#typo3/components/T3Form/T3Form.vue'

 const t3form = ref<Form | null>(null)
 const props = defineProps<T3CeFormFormframeworkProps>()
 const { elements, onSubmit, responseMessage } = useT3CeFormFormframework(props, t3form)
 const customI18n = { 
  submitButton: 'Submit',
  sendingLabel: 'Sending',
  resetButton: 'Reset form',
  serverSuccess: 'The form was sent, thank you.',
  serverError: 'We can not process form right now, please try again later.',
  validationErrors: 'There were some errors, review the form'
 };
</script>

Accessing Properties from the meta Object

The meta object, exposed by the T3Form component, contains various properties that provide insights into the form's state. You can use these properties to gain valuable information about your form's status and conditions.

const t3form = ref<Form | null>(null)
const meta = computed(() => t3form.value?.meta)
Using the dirty Property

The dirty property is a boolean that indicates whether the form has been modified. It becomes true when a field within the form has been changed by the user.

  <p v-if="t3form.meta.dirty">Form has been modified.</p>

In this example, the v-if directive is used to conditionally display a message when the form has been modified.

Using the initialValues Property

The initialValues property contains a proxy object with the initial values of the form fields. This can be helpful if you need to reference the original values of the form fields.

  <p>Initial value of a specific field: {{ t3form.meta.initialValues.fieldName }}</p>
Using the touched Property

The touched property is a boolean that becomes true when the form fields have been interacted with by the user. It helps determine whether the user has engaged with the form.

 <p v-if="t3form.meta.touched">User has interacted with the form.</p>

In this example, the v-if directive is used to conditionally display a message when the user has interacted with the form fields.

Using the pending Property

The pending property is a boolean that indicates whether the form is currently in a pending state. In this context, "pending" means that the form is actively processing operations. It is false when the form is not pending.

  <p v-if="t3form.meta.pending">Form is currently processing. Please wait.</p>
Using the valid Property

The valid property is a boolean that becomes true when all form fields meet their validation criteria. It provides a quick way to check if the form is currently valid.

  <p v-if="t3form.meta.valid">Form is valid. You can submit it.</p> <p v-else>Form is not valid. Please correct errors before submitting.</p>

In this example, the v-if directive is used to conditionally display a message based on the form's current validity status. If the form is valid, it provides a message indicating that it can be submitted; otherwise, it asks the user to correct errors.

Customize field templates

Each form field uses T3FormField component as the base template. You can override this template:

Create and register global components components/T3FormField.vue:

<template>
 <div class="t3-form-field">
  <label>{{ field.label }}</label>
  <slot :value="value">
   <input
    v-model="value"
    :name="name"
    :type="type"
    :placeholder="field.properties?.fluidAdditionalAttributes?.placeholder">
  </slot>
  <slot name="error">
   <div v-if="showErrorMessage" style="color: red;">
   {{ customErrorMessage }}
   </div>
  </slot>
 </div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useIsFormTouched, useField } from 'vee-validate'
import type { T3FormField } from '@t3headless/nuxt-typo3'

const props = defineProps<T3FormField>()
const { value, meta, errorMessage } = useField(() => props.name)
const customErrorMessage = computed(() => {
  if (!errorMessage.value) {
    return ''
  }
  if (!props.field?.label) {
    return errorMessage
  }
  const  strArr = errorMessage.value.split(' ')
  strArr[0] = props.field.label
  return strArr.join(' ')
})

const isFormTouched = useIsFormTouched()
const showErrorMessage = computed(() => meta.dirty || isFormTouched.value)

</script>

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": [
        {
          "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]"
        }
      ]
}

T3FormFieldList component is responsible for rendering this list.

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

<template>
  <component
    :is="resolveFieldComponent(field)"
    v-if="field.name && !field.fieldlist"
    :name="field.name"
    :field="field"
  />
</template>

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. textarea
  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
      class="t3-form-field-textarea"
      v-bind="props">
        <input type="checkbox" v-model="value" :name="name">
    </T3FormField>
</template>

<script setup lang="ts">
import { useField } from 'vee-validate'
import T3FormField from '#typo3/components/T3Form/T3FormField/T3FormField.vue'
import type { T3FormField as FieldType } from '../../../src/types'

const props = defineProps<FieldType>()
const { value } = useField<string>(() => props.name)
</script>

Notice that we have used default T3FormField as the wrapper 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.

You can add custom validation rules to the T3FormFormFramework by passing them in the options object.

Example in T3FormFormFramework:

// In the setup section of your component  
const options = {
  rules: {
    // Define your custom validation rules here
    CustomRule: (value: any) => {
      if (/* your custom validation logic */) {
        return true // Validation passed
      } else {
        return 'Custom validation failed' // Validation failed
      }
    }
    // Add other custom rules as needed
  }
}
// Then, when initializing T3FormFormFramework:  
const { elements, onSubmit, responseMessage, t3form } = useT3CeFormFormframework(props, t3form, options);