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-
is changed to foo
if the checkbox
checkbox-
is checked.
The following properties can be overwritten by variants within the topmost element (Form
):
label
rendering
Options finishers
renderer
Class Name
The following properties can be overwritten by variants within all of the other form elements:
enabled
label
default
Value properties
rendering
Options 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 application
.
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\
(@see https://docs.typo3.org/typo3cms/extensions/form/ApiReference/Index.html#typo3-cms-form-domain-model-formruntime).
Example
form
.
formValues
(array)
form
holds all of the submitted form element values. Each key within this array represents a form element identifier.
Example
form
.
stepIdentifier
(string)
step
is set to the identifier
of the current step.
Example
step
.
stepType
(string)
step
is set to the type
of the current step.
Example
step
.
finisherIdentifier
(string)
finisher
is set to the identifier
of the current finisher or an empty string (while no finishers are executed).
Example
finisher
.
siteLanguage
(object)
You can access every public method from \TYPO3\
.
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
site
.
applicationContext
(string)
application
is set to the application context (@see GeneralUtility::getApplicationContext()).
Example
application
.
contentObject
(array)
content
is set to the data of the current content object or to an empty array if no content object is available.
Example
content
.
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/
. Make sure your expression
language provider implements \Symfony\
.
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:
class CustomExpressionLanguageProvider extends AbstractProvider
{
public function __construct()
{
$this->expressionLanguageVariables = [
'variableA' => 'valueB',
];
}
}