October 21, 2024
Chicago 12, Melborne City, USA
PHP

Render form fields in own BE module with TYPO3 FormEngine


Thanks to the power of TCA and FormEngine, it’s no problem to have complex forms with enriched form fields (to enter dates, select links or records, use richtext editors etc.) rendered in the backend.
So we know that the TYPO3 backend can do that. It therefore seems obvious to use the same capabilities to render forms in your own BE module. This makes sense, because you want users to have a consistent experience throughout the BE, where all forms look + behave the same way.

And the (unfortunately terribly outdated) documentation about FormEngine
actually confirms + encourages this approach:

The basic idea is "feed something that looks like TCA and render forms that have the full power of TCA but look like all other parts of the backend".

The Core Team […] encourages developers to solve feature needs based on FormEngine.

In my specific case, I have a BE module that allows users to bulk create vouchers. The form for the create action offers a number of fields required to define the vouchers.
One of the fields allows users to set a fe_user record. I’d like to have this field rendered like a TCA field of type group (just as if you were editing an individual voucher record via list module).
So far, this has unfortunately been quite a struggle. I found an older post that helped as a starting point,
but is no longer up-to-date with current versions of TYPO3.

This is what I have achieved to so far:
I can get the HTML for the field rendered quite well (simplified for readability):

<?php

use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Form\Element\GroupElement;
use TYPO3\CMS\Backend\Form\Behavior\UpdateValueOnFieldChange;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;

[...]

    /**
     * create action
     */
    public function createAction()
    {
        /* ... */

        $feuserField = $this->renderFeUserField($currFeUser);

        foreach ($feuserField['javaScriptModules'] as $module) {
            $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($module);
        }
        $viewVars['renderedFeUserField'] = $feuserField;

        $this->view->assignMultiple($viewVars);
    }


    protected function renderFeUserField(int $currValue)
    {
        // NOTE: we use a simplified naming scheme compared to editing a TCA record
        //       -> nothing gets saved here, we just need the uid value of the fe_user
        $tableName="my_ext_voucher";
        $pseudoUid = 1;
        $value = [];
        if ( !empty($currValue) ) {
            if ( $feUser = Div::getFeuser($currValue) ) {
                $value[] = ['table' => 'fe_users', 'uid' => $currValue, 'title' => $feUser[0]['username'], 'row' => $feUser[0]];
            }
        }
        $onChange = new UpdateValueOnFieldChange($tableName, (string)$pseudoUid, 'fe_user', sprintf('fe_user_%s', $pseudoUid));
        $config = $GLOBALS['TCA']['my_ext_voucher']['columns']['fe_user']['config'];
        $config['clipboardElements'] = [];
        // we disable the element browser for now, because it doesn't work -> see below
        $config['fieldControl']['elementBrowser'] = ['disabled' => true];
        $options = [
            'renderType' => 'group',
            'tableName' => $tableName,
            'fieldName' => 'fe_user',
            'databaseRow' => [
                'uid' => $pseudoUid,
                'pid' => $this->id,
            ],
            'parameterArray' => [
                'fieldConf' => [
                    'label' => 'UID FE user',
                    'config' => $config,
                ],
                'itemFormElValue' => $value,
                'itemFormElName' => sprintf('fe_user_%s', $pseudoUid),
                'itemFormElID' => sprintf('fe_user_%s', $pseudoUid),
                'field' => 'fe_user',
                'fieldChangeFunc' => [
                    'TBE_EDITOR_fieldChanged' => $onChange,
                ],
            ],
            'processedTca' => $GLOBALS['TCA']['my_ext_voucher'],
            'inlineStructure' => [],
        ];

        $nodeFactory = new NodeFactory();
        $groupField = new GroupElement($nodeFactory, $options);
        return $groupField->render();
    }

In the template I do:

<f:be.pageRenderer
    includeJavaScriptModules="{
        0: '@typo3/backend/form-engine/element/group-element.js'
    }"
    includeJsFiles="{0: 'EXT:my_ext/Resources/Public/JavaScript/tx_myext.js'}"
/>

[...]

    <div class="form-section">
        <div class="form-group">
            {renderedFeUserField.html -> f:format.raw()}
        </div>
    </div>

And now it gets messy: in the included JS file tx_myext.js I had to do the following:

var ready = (callback) => {
    if (document.readyState != 'loading') callback();
    else document.addEventListener('DOMContentLoaded', callback);
}

// needed to avoid error on page load
var TYPO3 = {"settings": {"FormEngine": {"formName":"editform"}}};

ready(() => {
    // super ugly, but TYPO3.FormEngine isn't ready on page load
    setTimeout(() => {
        TYPO3.FormEngine.initialize();
    }, 1000);
});

The result of all this is:

  • the form field get’s rendered perfectly
  • the search field works fine; you can search, select + delete fe_user records
  • when the search doesn’t find anything, the text "no records found" isn’t shown, the label text seems to be missing
  • the ElementBrowser doesn’t work (I therefore have it disabled for the moment, see above)

The main problem is the ElementBrowser. When enabled, the popup opens after clicking the folder icon, but it just shows the BE. Inspecting the URL of the iframe reveals the problem:

undefined&mode=db&bparams=fe_user_1|||fe_users|

The problem clearly lies with missing JavaScript. Most likely there are a number of JS modules missing and probably some configuration + initialization as well. This is certainly also the reason for the JS error I get after submitting the form:
Uncaught TypeError: e.target.closeDoc is undefined (form-engine.js:13:7473)

I’ve spent far too many hours trying to resolve that, a lot of trial & error, including various JS modules, trying to reverse engineer the behaviour of regular TCA record editing etc. But while a lot of this works, there are still too many problems to use this in a productive setting.

My questions are:

  • How do I approach this? is there any documentation or example code that I haven’t found?
  • How can I find out, which JS modules are required for this to work?
  • Does anyone have a solution for this?
  • Am I the only one who wants to do that? (it’s surprisingly hard to find information about this)

I wish this would be easier… In a perfect world, there should be BE viewhelpers to render form fields for all sorts of TCA field types. Again, this would greatly help to give users a consistent and familiar experience when using custom BE modules (and would be in sync with the core team’s stated intentions). All the functionality is already there, so why invent it again in your own module?



You need to sign in to view this answers

Leave feedback about this

  • Quality
  • Price
  • Service

PROS

+
Add Field

CONS

+
Add Field
Choose Image
Choose Video