Feature: #84133 - Introduce variants

See forge#84133

Description

Short Description

Variants allow you to change properties of a form element and can be activated based on conditions.

This makes it possible to manipulate form element properties, validator options, and finisher options based on conditions.

This allows you among other things:

  • translate form element values depending on the frontend language

  • set and remove validators of one form element depending on the value of another form element

  • hide entire steps (form pages) depending on the value of a form element

  • set finisher options depending on the value of a form element

  • hiding a form element in certain finishers and on the summary step

This feature implements variants for frontend rendering and the ability to define variants in form definitions. The implementation to define variants graphically in the form editor is out of scope of this patchset.

Basics

Variants allow you to change properties of form elements, validators, and finishers and are activated by conditions. They are defined on the form element level either statically in form definitions or created programmatically through an API.

The variants defined within a form definition are automatically applied to the form based on their conditions at runtime. Programmatically, variants can be applied at any time.

Furthermore, the conditions of a variant can be evaluated programmatically at any time. However, some conditions are only available at runtime, for example a check for a form element value.

Custom conditions and operators can be added easily.

Only the form element properties listed in a variant are applied to the form element, all other properties are retained. An exception to this rule are finishers and validators. If finishers or validators are not defined within a variant, the original finishers and validators will be used. If at least one finisher or validator is defined in a variant, the originally defined finishers or validators are overwritten by the list of finishers and validators of the variant.

Variants defined within a form definition are all processed and applied in the order of their condition matches. This means if variant 1 sets the label of a form element to "X" and variant 2 sets the label to "Y", then variant 2 is applied, i.e. the label will be "Y".

Variants definition

Variants are defined on the form element level. Check the following - incomplete - example:

type: Text
identifier: text-1
label: ''
variants:
  -
    identifier: variant-1
    condition: 'formValues["checkbox-1"] == 1'
    # If the condition matches, the label property of the form element is set to the value 'foo'
    label: foo

As usual identifier must be a unique name of the variant on the form element level.

Each variant has a single condition which lets the variants' changes get applied if it matches.

If the condition of a variant matches, the remaining properties are applied to the form element. In the aforementioned example the label of the form element text-1 is changed to foo if the checkbox checkbox-1 is checked.

The following properties can be overwritten by variants within the topmost element (Form):

  • label

  • renderingOptions

  • finishers

  • rendererClassName

The following properties can be overwritten by variants within all of the other form elements:

  • enabled

  • label

  • defaultValue

  • properties

  • renderingOptions

  • validators

Conditions

The form framework uses the Symfony component expression language to match the conditions. (@see https://symfony.com/doc/4.1/components/expression_language.html) An expression is a one-liner that returns a boolean value like applicationContext matches "#Production/Local#". Please read https://symfony.com/doc/4.1/components/expression_language/syntax.html to learn more about this topic. The form framework extends the expression language with some variables which can be used to access form values and environment settings.

formRuntime (object)

You can access every public method from the \TYPO3\CMS\Form\Domain\Runtime\FormRuntime (@see https://docs.typo3.org/typo3cms/extensions/form/ApiReference/Index.html#typo3-cms-form-domain-model-formruntime).

Example

formRuntime.getIdentifier() == "test".

formValues (array)

formValues holds all of the submitted form element values. Each key within this array represents a form element identifier.

Example

formValues["text-1"] == "yes".

stepIdentifier (string)

stepIdentifier is set to the identifier of the current step.

Example

stepIdentifier == "page-1".

stepType (string)

stepType is set to the type of the current step.

Example

stepType == "SummaryPage".

finisherIdentifier (string)

finisherIdentifier is set to the identifier of the current finisher or an empty string (while no finishers are executed).

Example

finisherIdentifier == "EmailToSender".

siteLanguage (object)

You can access every public method from \TYPO3\CMS\Core\Site\Entity\SiteLanguage. The most needed ones are probably:

  • getLanguageId() / Aka sys_language_uid.

  • getLocale() / The language locale. Something like 'en_US.UTF-8'.

  • getTypo3Language() / The language key for XLF files. Something like 'de' or 'default'.

  • getTwoLetterIsoCode() / Returns the ISO-639-1 language ISO code. Something like 'de'.

Example

siteLanguage("locale") == "de_DE".

applicationContext (string)

applicationContext is set to the application context (@see GeneralUtility::getApplicationContext()).

Example

applicationContext matches "#Production/Local#".

contentObject (array)

contentObject is set to the data of the current content object or to an empty array if no content object is available.

Example

contentObject["pid"] in [23, 42].

Working with variants programmatically

Create a variant with conditions through the PHP API:

/** @var TYPO3\CMS\Form\Domain\Model\Renderable\RenderableVariantInterface $variant */
$variant = $formElement->createVariant([
    'identifier' => 'variant-1',
    'condition' => 'formValues["checkbox-1"] == 1',
    'label' => 'foo',
]);

Get all variants of a form element:

/** @var TYPO3\CMS\Form\Domain\Model\Renderable\RenderableVariantInterface[] $variants */
$variants = $formElement->getVariants();

Apply a variant to a form element regardless of its defined conditions:

$formElement->applyVariant($variant);

Examples

Translate form element values depending on the frontend language:

type: Form
identifier: test
prototypeName: standard
label: DE
renderingOptions:
  submitButtonLabel: Abschicken
variants:
  -
    identifier: language-variant-1
    condition: 'siteLanguage("locale") == "en_US.UTF-8"'
    label: EN
    renderingOptions:
      submitButtonLabel: Submit
renderables:
  -
    type: Page
    identifier: page-1
    label: DE
    renderingOptions:
      previousButtonLabel: 'zurück'
      nextButtonLabel: 'weiter'
    variants:
      -
        identifier: language-variant-1
        condition: 'siteLanguage("locale") == "en_US.UTF-8"'
        label: EN
        renderingOptions:
          previousButtonLabel: 'Previous step'
          nextButtonLabel: 'Next step'
    renderables:
      -
        type: Text
        identifier: text-1
        label: DE
        properties:
          fluidAdditionalAttributes:
            placeholder: Platzhalter
        variants:
          -
            identifier: language-variant-1
            condition: 'siteLanguage("locale") == "en_US.UTF-8"'
            label: EN
            properties:
              fluidAdditionalAttributes:
                placeholder: Placeholder

Set validators of one form element depending on the value of another form element:

type: Form
identifier: test
label: test
prototypeName: standard
renderables:
  -
    type: Page
    identifier: page-1
    label: Step
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: text-1
        label: 'Email address'
        variants:
          -
            identifier: variant-1
            condition: 'formValues["checkbox-1"] == 1'
            properties:
              fluidAdditionalAttributes:
                required: 'required'
            validators:
              -
                identifier: NotEmpty
              -
                identifier: EmailAddress
      -
        type: Checkbox
        identifier: checkbox-1
        label: 'Subscribe to newsletter'

Hide entire steps depending on the value of a form element:

type: Form
identifier: test
prototypeName: standard
label: Test
renderables:
  -
    type: Page
    identifier: page-1
    label: 'Page 1'
    renderables:
      -
        type: Text
        identifier: text-1
        label: 'Text 1'
      -
        type: Checkbox
        identifier: checkbox-1
        label: 'Skip page 2'
        variants:
          -
            identifier: hide-1
            condition: 'stepType == "SummaryPage"'
            renderingOptions:
              enabled: false
  -
    type: Page
    identifier: page-2
    label: 'Page 2'
    variants:
      -
        identifier: variant-1
        condition: 'formValues["checkbox-1"] == 1'
        renderingOptions:
          enabled: false
    renderables:
      -
        type: Text
        identifier: text-2
        label: 'Text 2'
  -
    type: SummaryPage
    identifier: summarypage-1
    label: 'Summary step'

Set finisher values depending on the application context:

type: Form
identifier: test
prototypeName: standard
label: Test
renderingOptions:
  submitButtonLabel: Submit
finishers:
  -
    identifier: Confirmation
    options:
      message: 'Thank you'
variants:
  -
    identifier: variant-1
    condition: 'applicationContext matches "#Production/Local#"'
    finishers:
      -
        identifier: Confirmation
        options:
          message: 'ouy knahT'
renderables:
  -
    type: Page
    identifier: page-1
    label: 'Page 1'
    renderingOptions:
      previousButtonLabel: 'Previous step'
      nextButtonLabel: 'Next step'

Hide a form element in certain finishers and on the summary step:

type: Form
identifier: test
prototypeName: standard
label: Test
finishers:
  -
    identifier: EmailToReceiver
    options:
      subject: Testmail
      recipientAddress: tritum@example.org
      recipientName: 'Test'
      senderAddress: tritum@example.org
      senderName: tritum@example.org
renderables:
  -
    type: Page
    identifier: page-1
    label: 'Page 1'
    renderables:
      -
        type: Text
        identifier: text-1
        label: 'Text 1'
        variants:
          -
            identifier: hide-1
            renderingOptions:
              enabled: false
            condition: 'stepType == "SummaryPage" || finisherIdentifier in ["EmailToSender", "EmailToReceiver"]'
      -
        type: Text
        identifier: text-2
        label: 'Text 2'
  -
    type: SummaryPage
    identifier: summarypage-1
    label: 'Summary step'

Adding own expression language providers

If you need to extend the expression language with custom functions you can extend it. For more information check the official docs and the appropriate TYPO3 implementation details.

Register the expression language provider in the extension file Configuration/ExpressionLanguage.php. Make sure your expression language provider implements Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface.

EXT:some_extension/Configuration/ExpressionLanguage.php
return [
    'form' => [
        Vendor\MyExtension\ExpressionLanguage\CustomExpressionLanguageProvider::class,
    ],
];

Adding own expression language variables

If you need to add custom variables to the expression language you can extend it. Then the variables are ready to be checked in conditions.

Register a custom expression language provider as written above and provide the expression language variables:

EXT:some_extension/Classes/ExpressionLanguage/CustomExpressionLanguageProvider.php
class CustomExpressionLanguageProvider extends AbstractProvider
{
    public function __construct()
    {
        $this->expressionLanguageVariables = [
            'variableA' => 'valueB',
        ];
    }
}