Use Form-Framework forms in your own extensions

We want to use Extbase/Fluid to create an extension for displaying data records, e.g. products, with a list view and a detailed view.
A product-specific contact form is to be displayed in the detailed view, which also transmits the name of the displayed product.
It should be possible to create the form quickly and easily using the form editor, including all validators and finishers.
We would like to send the form via HTML e-mail using the e-mail finisher.
We do not want to program the whole thing ourselves in an action, but would like to use the entire server-side form logic of the form framework.

The form should be created with the Form Editor:

Preliminary work: Debugging settings
Oops an error occurred in extbase plugins
Instead of the message 'Oops an error occurred', you can also output the stacktrace
. This requires the following TYPOSCRIPT in the template setup:
config.contentObjectExceptionHandler = 0
Creation of the extension
We use the Extension Builder to create a simple demo extension with which you can create simple data records that only contain a title.
In addition, we create a plugin that displays a list view of the data records and a detailed view of a single data record.
Customization of the show action
In the show action, in addition to displaying the item details, we would like to display a contact form which is sent by e-mail and which also transmits the name of the displayed product.

At this point we would like to integrate a form, which we will create
with the Form Framework.
Create path to own form configurations
Configuration/Form/FormSetup.yaml
TYPO3:
CMS:
Form:
persistenceManager:
allowedExtensionPaths:
1591099960: EXT:test1/Resources/Private/Forms/
allowSaveToExtensionPaths: true
allowDeleteFromExtensionPaths: false
Integrate your own configuration via TYPOSCRIPT
for the BE
ext_localconf.php
if (TYPO3_MODE === 'BE') {
/**
* Register custom EXT:form configuration
*/
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(
trim('
module.tx_form {
settings {
yamlConfigurations {
1591100383 = EXT:test_1/Configuration/Form/FormSetup.yaml
}
}
}
')
);
}
Now you can create a new form definition in the form editor in the BE.
for the FE
analogous to that for the BE with the key plugin.tx_form instead of module.tx_form:
Configuration/TypoScript/setup.typoscript
# register forms for FE
plugin.tx_form {
settings {
yamlConfigurations {
# register your own additional configuration
# choose a number higher than 30 (below is reserved)
1591100383 = EXT:test1/Configuration/Form/FormSetup.yaml
}
}
}
Now you can render the form with the `formvh:render` view helper in Frontend.
If you forget this TYPOSCRIPT, you will receive the following error message in the FE:
The file xxx could not be loaded. Please check your configuration option "persistenceManager.allowedExtensionPaths"
Sending the form
When submitting the form for the first time, you will receive the following error message:

We must therefore either allow a new action perform or adapt the rendering of the form so that an existing action is used instead.
Creation of the perform action in the controller
We create the perform action, which does nothing other than redirect to the show action.
typo3conf/ext/test1/Classes/Controller/TestRecordController.php
/**
* action perform (called after submitting the form)
*
* @param \Sitegeist\Test1\Domain\Model\TestRecord $testRecord
* @return void
*/
public function performAction(\Sitegeist\Test1\Domain\Model\TestRecord $testRecord)
{
$this->forward('show');
}
And we register this new action as non-cacheable, as the form submission should be completed with every submit:
typo3conf/ext/test1/ext_localconf.php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'Sitegeist.Test1',
'Plugin1',
[
'TestRecord' => 'list, show, create, edit, update, delete, perform'
],
// non-cacheable actions
[
'TestRecord' => 'edit, update, delete, perform'
]
);
If you submit the form again, you will receive the following new error message:

This is because our action expects the data set as a parameter.
The form does not have this yet and we want to include it in the form in the next step.
Transfer the current data record to the form
To pass the current data record to the form, we can give the formvh:render viewhelper the parameter overrideConfiguration and specify that we want to pass an additional parameter, namely the uid of the active data record:
typo3conf/ext/test1/Resources/Private/Templates/TestRecord/Show.html
<formvh:render
persistenceIdentifier="EXT:test1/Resources/Private/Forms/productContactForm.form.yaml"
overrideConfiguration="{
renderingOptions: {
controllerAction:'perform',
additionalParams: {
'tx_test1_plugin1[testRecord]': testRecord
}
}
}"
/>
The form is now successfully sent and the show action is displayed again, but the form no longer appears.
Alternative: Use of an existing action
Instead of using the default action perform, we can also adapt the definition of the form so that a different action is used.
It is important that this action is not cached so that the finishers are also called each time the form is sent.
It is also important that this action also displays the form again in its view and uses the formvh:render view helper in it. This is because it takes over the complete rendering of the form in the correct state and the execution of the finishers or the correct display of the steps in a multi-step form.
To define your own action, simply enter a different action name for the controllerAction parameter in the above example.
Complete the form field for the title of the data record
However, the product is not included in the email that you receive through the email finisher.
This is because hidden fields are not normally transferred in the email finisher.
We will therefore extend the form so that the name of the data record is also transferred in a visible field. To do this, we can use the form editor to add the field.

After saving, this leads to the following extension in our Yaml form definition:
Resources/Private/Forms/productContactForm.form.yaml
renderables:
-
renderables:
-
defaultValue: ''
type: Text
identifier: text-4
label: Product
properties:
elementDescription: 'Name of the Product'
This leads to the following new form after a reload of our detail page:

Now we have to make sure that this field is correctly pre-assigned with the title of our data record.
We will do this in the next step.
Pre-assignment of a field in the form
We would now like to pre-assign the Product field with the title of our data record. In our Fluid template, the data record is available as the Fluid variable testRecord. We therefore receive the title of the data record with testRecord.title.
We can now pass this to the formvh:render view-helper in the parameter overrideConfiguration and must look at the exact hierarchical structure of our yaml form definition and reproduce this via fluid in the renderables area:
<formvh:render
persistenceIdentifier="EXT:test1/Resources/Private/Forms/productContactForm.form.yaml"
overrideConfiguration="{
renderingOptions: {
controllerAction:'perform',
additionalParams: {
'tx_test1_plugin1[testRecord]': testRecord
}
},
renderables: {
0: {
renderables: {
4: {
defaultValue: testRecord.title
}
}
}
}
}"
/>
Note: If you make an error in the inline configuration of the `overrideConfiguration` parameter so that Fluid can no longer create an object from it, the following error message appears:

This can happen, for example, if there is a comma after the last element.
It is very unpleasant that you have to count exactly the numerical indices (starting at 0) for the individual elements of the form from the yaml definition.
If you change the order of the elements later in the editor or insert another one before the product field, the configuration must be adjusted accordingly.
In our example above, a 4
is specified as the index, as the product field is the 5th element within our form and the counting of the elements starts at 0.

Alternatively, you can also omit the form field in the yaml configuration and instead create a completely new field in the overrideConfiguration
parameter of the formvh:render
view helper, which is given a unique, alphanumeric index. For this field, however, you must also specify all attributes analogous to the yaml configuration of the field:
<formvh:render
persistenceIdentifier="EXT:test1/Resources/Private/Forms/productContactForm.form.yaml"
overrideConfiguration="{
renderingOptions: {
controllerAction:'perform',
additionalParams: {
'tx_test1_plugin1[testRecord]': testRecord
}
},
renderables: {
0: {
renderables: {
productField: {
defaultValue: testRecord.title,
identifier: 'productField',
type: 'Text',
label: 'Dynamic Productfield',
properties: {
elementDescription: 'Name of the Product'
}
}
}
}
}
}"
/>
There are 2 disadvantages to this procedure:
- The field is not visible and customizable in the form editor.
- You cannot control the order of this field, it is always appended at the end of the form.
Debugging the form configuration
Overwriting the form configuration can be a tedious task if a field does not receive the values or the overwriting does not work as desired.
In these cases, it is a good idea to have the form framework display what the final configuration looks like after applying our overrideConfiguration.
To do this, you can temporarily output a debug message by adding a debug output to the renderStatic method in the view helper typo3/sysext/form/Classes/ViewHelpers/RenderViewHelper.php
in line 90:
/**
* @param array $arguments
* @param \Closure $renderChildrenClosure
* @param RenderingContextInterface $renderingContext
* @return string
*/
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
{
$persistenceIdentifier = $arguments['persistenceIdentifier'];
$factoryClass = $arguments['factoryClass'];
$prototypeName = $arguments['prototypeName'];
$overrideConfiguration = $arguments['overrideConfiguration'];
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
if (!empty($persistenceIdentifier)) {
$formPersistenceManager = $objectManager->get(FormPersistenceManagerInterface::class);
$formConfiguration = $formPersistenceManager->load($persistenceIdentifier);
ArrayUtility::mergeRecursiveWithOverrule(
$formConfiguration,
$overrideConfiguration
);
$overrideConfiguration = $formConfiguration;
$overrideConfiguration['persistenceIdentifier'] = $persistenceIdentifier;
}
// *******************************
// debug the merged configuration in FE
// *******************************
debug($overrideConfiguration);
// ...
}
The output then looks like this:

Advantages compared to a form in Extbase/Fluid
- created faster
- Validators and finishers from the form framework can be used
- (Almost) no separate action, i.e. no server-side logic required
