Repeatable Form Element - Teil 1

Zielstellung

Das TYPO3 Form Framework - Teil des TYPO3 Kerns seit Version 8.5 - erlaubt Dir die Erstellung eigener Formularelemente. In diesem ultimativen Walkthrough zeigt Dir Ralf - Lead Developer des Form Frameworks - wie ein solches, komplexes Formularelement in eine handliche TYPO3 Extension gepackt werden kann.

Das Element taufen wir "RepeatableContainer" und stecken es in eine Extension namens repeatable_form_elements. Im Formular Editor (Backend) soll das Element - wie der Name bereits vermuten lässt - als Container agieren. Der Redakteur soll beliebige Felder in das Element ablegen und die minimale und maximale Zahl an Kopien definieren können.

Im Frontend sollen das Element als Fieldset inkl. der enthaltenen Felder ausgegeben werden. Durch das Klicken eines Buttons wird ein Prozess angestoßen, der eine Kopie des RepeatableContainers mit dessen Kinderelementen, deren Eigenschaften und Validatoren anstößt. Sowohl client- als auch serverseitig werden die Minmal- und Maximalzahl an Kopien geprüft. So lassen sich bspw. dynamische Listen für Teilnehmer oder Uploads umsetzen.

Der Walkthrough ist sehr lang, weshalb wir ihn in zwei Teile gegliedert haben. In diesem Beitrag erfährst du alles über das grundlegende Konfiguration. Im zweiten Teil behandeln wir die Erstellung der Business Logik. Den gesamten Code findest du auf Github und auf Packagist.

Das folgende Video zeigt Dir was wir erreichen wollen.

Teil 1: Grundlegende Konfiguration

Um das neue Formularelement RepeatableContainer dem TYPO3 Form Framework hinzufügen zu können, muss das Element in den Setup Dateien konfiguriert werden. Da das Form Framework auf  Extbase basiert, müssen die Setup Dateien an zwei Stellen definiert werden. Diese zwei Stellen betreffen zum einen das Frontend Rendering und zum anderen das Backend Modul "Forms".

1. TypoScript Setup

Frontend

Die Konfiguration für das Frontend Rendering geschieht in der Datei ./Configuration/TypoScript/setup.typoscript. Dem Form Framework wird mitgeteilt, dass das Setup um die Datei ./Configuration/Yaml/FormSetup.yaml erweitert werden soll. Sobald die Datei ./Configuration/TypoScript/setup.typoscript dem Template einer Seite hinzugefügt wurde, ist dem Form Framework die Erweiterung des Setups auf dieser Seite und deren Unterseiten bekannt.

plugin.tx_form {
    settings {
        yamlConfigurations {
            1511193633 = EXT:repeatable_form_elements/Configuration/Yaml/FormSetup.yaml
        }
    }
}

Backend

Da Extbase die Registrierung von TypoScript Dateien für Frontend und Backend trennt, müssen wir die Erweiterung des Setups nun auch für das Backend hinzufügen. Die Konfiguration für das Backend Modul geschieht über die Datei ./ext_localconf.php. Durch das Hinzufügen des TypoScript Codes durch die API Methode addTypoScriptSetup() wird sichergestellt, dass im Backend an jeder Stelle die Erweiterung des Setups bekannt ist.

Eine Alternative wäre gewesen, die Datei ext_typoscript_setup.txt zu nutzen, welche aber nicht empfohlen wird, da die Zukunft dieser Datei und der damit verbundenen Funktionalität ungewiss ist. Im Gegensatz zum Frontend werden hier zwei Erweiterungsdateien für das Setup hinzugefügt. Das Backendmodul des Form Frameworks benötigt neben der Konfiguration für selbiges (./Configuration/Yaml/FormSetupBackend.yaml) auch die Setup Definitionen für das Frontend (./Configuration/Yaml/FormSetup.yaml), andernfalls würde die Vorschau eines Formulars im Form Editor nicht funktionieren.

if (TYPO3_MODE === 'BE') {
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(trim('
        module.tx_form {
            settings {
                yamlConfigurations {
                    1511193633 = EXT:repeatable_form_elements/Configuration/Yaml/FormSetup.yaml
                    1511193634 = EXT:repeatable_form_elements/Configuration/Yaml/FormSetupBackend.yaml
                }
            }
        }
    '));
}

2. YAML Setup

Frontend

Fluid Template-Suchpfade erweitern

Im Gegensatz zu anderen TYPO3 Erweiterungen werden die Fluid-Suchpfade beim Form Framework nicht im TypoScript, sondern in den YAML Setup Dateien vorgenommen. Für jedes Formularelement können eigene Fluid-Suchpfade konfiguriert werden. Ist dies nicht der Fall, so nimmt das Form Framework die Fluid-Suchpfade, welche für das Formularelement Form konfiguriert wurden.

Für jedes Formularelement wird standardmäßig nach einem Partial gesucht, welches den Namen des Formulartyps hat (eine Ausnahme stellt der Formulartyp Form dar). Definieren wir also ein neues Formularelement vom Typ RepeatableContainer, so wird nach einem Partial namens RepeatableContainer.html gesucht.

Es gibt noch eine alternative Methode, um den Namen des Partials zu bestimmen, welcher verwendet werden soll, was in diesem Fall aber nicht benutzt wird. Wir erweitern nun also die Fluid-Suchpfade für Partials im Setup des Form Frameworks, damit wir ein Partial für das Formularelement RepeatableContainer in unserer Extension ablegen können. Die Konfiguration für die Fluid-Suchpfade geschieht in der Datei ./Configuration/Yaml/FormSetup.yaml.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            Form:
              renderingOptions:
                partialRootPaths:
                  20: 'EXT:repeatable_form_elements/Resources/Private/Frontend/Partials'

Übersetzungsdatei hinzufügen

Um Formularelementeigenschaften im Frontend übersetzen zu können, müssen die Übersetzungsdateien dem Form Framework bekannt gemacht werden. Für jedes Formularelement können eigene Übersetzungsdateien konfiguriert werden. Ist dies nicht der Fall, so nimmt das Form Framework die Übersetzungsdateien, welche für den Formulartyp Form konfiguriert wurden. Die Konfiguration für die Übersetzungsdateien geschieht in der Datei ./Configuration/Yaml/FormSetup.yaml.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            Form:
              renderingOptions:
                translation:
                  translationFile:
                    - 'EXT:form/Resources/Private/Language/locallang.xlf'
                    - 'EXT:repeatable_form_elements/Resources/Private/Language/locallang.xlf'

Formulartyp RepeatableContainer definieren

Jedes Element unter dem Konfigurationspfad TYPO3.CMS.Form.prototypes.standard.formElementsDefinition wird als neues Formularelementinterpretiert. Der hier angegebene Name ist der Formularelementtyp, welcher später in der Form Definition genutzt werden kann. Dem Form Framework wird hier mitgeteilt, wie sich der Formulartyp RepeatableContainer standardmäßig verhalten und aussehen soll. Bei der späteren Verwendung dieses Formulartyps in der Form Definition können dann diverse Voreinstellungen, welche hier getätigt werden, überschrieben werden.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            RepeatableContainer:
              __inheritances:
                10: 'TYPO3.CMS.Form.prototypes.standard.formElementsDefinition.Fieldset'
              implementationClassName: 'TRITUM\RepeatableFormElements\FormElements\RepeatableContainer'
              properties:
                minimumCopies: 0
                maximumCopies: 10
                showRemoveButton: true
                elementClassAttribute: 'repeatable-container'

Eigenschaft "implementationClassName"

Die Minimalausprägung eines Formularelements benötigt nur die Eigenschaft implementationClassName. Das Form Framework selbst bietet eine Standardimplementation für einfache Formularelemente an TYPO3CMSFormDomainModelFormElementsGenericFormElement, welche in den meisten Fällen für eigene Formularelemente verwendet werden kann. Unser RepeatableContainer ist aber ein Formularelement, welches Kinderelemente enthalten kann sowie ein Fieldset Formularelement. Darum könnten wir in unserem Fall die Standardimplementation für Fieldsets TYPO3CMSFormDomainModelFormElementsSection verwenden. Dies ist die aktuelle Implementation des RepeatableContainer Formularelements (TRITUMRepeatableFormElementsFormElementsRepeatableContainer).

class RepeatableContainer extends \TYPO3\CMS\Form\Domain\Model\FormElements\Section implements RepeatableContainerInterface
{
}

Warum wird hier dennoch eine eigene Implementation TRITUMRepeatableFormElementsFormElementsRepeatableContainer konfiguriert, welche sich doch am Ende auch nur von der Klasse TYPO3CMSFormDomainModelFormElementsSection ableitet und keine weitere Logik enthält? Die kurze Antwort: beim Form Framework führen viele Wege zum Ziel und es ist Geschmackssache.

In unserer Businesslogik müssen wir später dieses Formularelement anhand seines Types identifizieren können. Dies könnte über die API Methode getType() geschehen, welche uns RepeatableContainer liefern würde. Da ich es lieber mag, so etwas anhand des implementierten Interfaces zu prüfen, wählte ich diese Variante. Ein RepeatableContainer Objekt enthält in meiner Variante also die komplette Logik eines Section Objektes (z.B. Fieldset) und ist durch die Implementierung des RepeatableContainerInterface dann im Code identifizierbar.

if ($renderable instanceof RepeatableContainerInterface) {
}

Alternativ hätte man also die Eigenschaft implementationClassName auf TYPO3CMSFormDomainModelFormElementsSection stellen können (bzw. gar nicht angeben müssen, weil RepeatableContainer die Konfiguration von Fieldset erbt, welches diese Eigenschaft bereits gesetzt hat) und sich die Klasse RepeatableContainer und das Interface implementationClassName sparen können. In der Businesslogik hätte man dann den folgenden Code schreiben können.

if ($renderable->getType() === 'RepeatableContainer') {
}

Eigenschaft "properties"

In der Eigenschaft properties befinden sich Formularelement-spezifische Einstellungen, welche beliebig definierbar sind (in Abhängigkeit zur Implementierung und des Templates des Formularelements). Die im RepeatableContainer verwendeten properties werden teilweise im Template und teilweise in der Businesslogik verwendet. Im Template benötigen wir folgende Konfiguration.

properties:
  containerClassAttribute: input
  elementClassAttribute: repeatable-container
  elementErrorClassAttribute: error

Und in der Businesslogik benötigen wir folgende Konfiguration.

properties:
  minimumCopies: 0
  maximumCopies: 10
  showRemoveButton: true

Eigenschaft "renderingOptions"

In der Eigenschaft renderingOptions befinden sich Formularelement-spezifische Einstellungen, welche hauptsächlich das Verhalten deren Ausgabe im Frontend steuern. In unserem Fall wird nur die vom Fieldset benötigte interne Steueranweisung _isCompositeFormElement verwendet.

"__inheritances" Operator

Unser RepeatableContainer soll sich im großen und ganzen wie ein Fieldset Formularelement verhalten, vor allem im Form Editor. Damit nun nicht die gesamte Konfiguration des Fieldset Formularelements in die RepeatableContainer Konfiguration kopiert werden muss, gibt es den sogenannten __inheritances Operator. Um den Rahmen dieses Tutorials nicht gänzlich zu sprengen, kannst Du in einem gesonderten Beitrag mehr zum __inheritances Operator erfahren.

Backend (Form Editor)

Allgemeine Konfiguration

Hier werden allgemeine Einstellungen zur Darstellung des Formularelements im Form Editor vorgenommen. Die Eigenschft label definiert den Text, welcher im "New Element" Modal angezeigt werden soll. Da wir die Konfiguration vom Fieldset Formularelement mittels __inheritances in unser RepeatableContainer Formularelement kopiert haben, sind hier schon einige Werte vorhanden. Da sich unser RepeatableContainer im "New Element" Modal in der selben Gruppe "Container" wie das Fieldset einordnen soll, wird hier nichts anderes definiert. Die Eigenschaft groupSorting definiert dann die Position innerhalb der Gruppe. Der iconIdentifier definiert das Icon, das für das Formularelement an verschiedenen Stellen des Form Editors angezeigt werden soll und muss zuvor registriert werden. Dies geschieht in der Datei EXT:repeatable_form_elements/ext_localconf.php.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            RepeatableContainer:
              formEditor:
                label: 'formEditor.elements.RepeatableContainer.label'
                groupSorting: 150
                iconIdentifier: 't3-form-icon-repeatable-container'
New Element" Modal konfigurieren
if (TYPO3_MODE === 'BE') {
    $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
    $iconRegistry->registerIcon(
        't3-form-icon-repeatable-container',
        \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
        ['source' => 'EXT:repeatable_form_elements/Resources/Public/Icons/t3-form-icon-repeatable-container.svg']
    );
}
EXT:repeatable_form_elements/ext_localconf.php erweitern und Icon registrieren

Übersetzungsdatei hinzufügen

Um Eigenschaften im Form Editor übersetzen zu können, müssen die Übersetzungsdateien dem Form Framework bekannt gemacht werden. Die Konfiguration für die Übersetzungsdateien geschieht in der Datei EXT:repeatable_form_elements/Configuration/Yaml/FormSetupBackend.yaml.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formEditor:
            translationFile:
              10: 'EXT:form/Resources/Private/Language/Database.xlf'
              20: 'EXT:repeatable_form_elements/Resources/Private/Language/Database.xlf'
Übersetzungsdatei hinzufügen

Stage konfigurieren

Eine aktuell noch etwas umständliche Stelle ist die Konfiguration und Implementierung des Renderings eines eigenen Formularelements im Form Editor. Um was geht es hier?

Im mittleren Teil des Form Editors befindet sich die sogenannte "Stage". Diese Arbeitsfläche kann in 2 Modi geschaltet werden. Es gibt den "Abstract View" und den "Preview". Während der "Preview" versucht, das Formular so darzustellen, wie es im Frontend der Fall wäre, versucht der "Abstract View" einen schnellen Überblick über die Eigenschaften der Formularelemente einer Seite zu liefern. So muss z.B. nicht jedes Formularelement angeklickt werden, um zu sehen, ob an ihm z.B. ein Validator definiert ist.

Während der "Preview" auf die bereits hinterlegten Frontend-Partials des Formularelements zurückgreifen kann, um das entsprechende Formularelement darzustellen, benötigt der "Abstract View" nun eigene Partials für die einzelnen Formularelementtypen, um sie in dieser abstrakten Form in der "Stage" visualisieren zu können. Technisch wird das Ganze dann so gelöst, dass das HTML der Stage-Partials der Formularelementtypen als inline HTML Template in den Form Editor gerendert werden. Eine zugehöriger JavaScript Rendermethode holt sich das entsprechenden inline HTML Template und befüllt Platzhalter mit Werten des Formularelements und manipuliert das inline HTML Template ggf. mittels JavaScript.

Jetzt kommen wir zu der oben angekündigten umständlichen Stelle: Während die Verknüpfung zwischen Formularelementtyp und Stage-Partials konfiguriert werden kann, so kann die Verknüpfung zwischen Formularelementtyp und zugehöriger JavaScript Rendermethode - welche das Stage-Partial nun manipulieren soll - nicht konfiguriert werden. Dies lässt sich aktuell nur mit JavaScript lösen, indem man ein bestimmtes JavaScript Event auswertet und diese Verknüpung dort dann selbst herstellt. Um dieses JavaScript Event auszuwerten, ist es notwendig, ein überschaubares JavaScript Modul zu erstellen und zu registrieren.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formEditor:
            formEditorPartials:
              FormElement-RepeatableContainer: 'Stage/Fieldset'
Verknüpfung "Formularelementtyp -> Stage-Partials"

Wir verwenden in unserem Fall das vom Form Framework bereits mitgelieferte Stage/Fieldset Partial, weshalb an dieser Stelle keine eigenen Fluid Partial-Suchpfade erweitern werden müssen.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formEditor:
            dynamicRequireJsModules:
              additionalViewModelModules:
                - 'TYPO3/CMS/RepeatableFormElements/Form/Backend/FormEditor/ViewModel'
JavaScript Modul registrieren um die Verknüpfung "Formularelementtyp -> JavaScript Rendermethode" vornehmen zu können

Im JavaScript Modul wird nun das Event view/stage/abstract/render/template/perform konsumiert, welches für die Verknüpfung "Formularelementtyp -> JavaScript Rendermethode" zuständig ist. Es wird dabei abgefragt, ob das aktuell zu rendernde Formularelement vom Typ 'RepeatableContainer' ist. Wenn ja, dann wird das inline HTML vom Stage/Fieldset Partial mit der JavaScript Methode StageComponent.renderSimpleTemplate() gerendert.

getPublisherSubscriber().subscribe('view/stage/abstract/render/template/perform', function (topic, args) {
    if (args[0].get('type') === 'RepeatableContainer') {
        StageComponent.renderSimpleTemplate(args[0], args[1]);
    }
});

Inspector konfigurieren

Der "Inspector" ist der rechte Bereich im Form Editor. Hier können Eigenschaften eines Formularelements editiert werden. Das Editieren von Formularelementeigenschaften erfolgt mittels sogenannter "Inspector Editors". Das Konzept ist hier sehr ähnlich von dem der Stage. Jeder Inspector Editor besteht aus einem konfigurierbaren Partial, welches als inline HTML Template in den Form Editor gerendert wird. Eine zugehöriger JavaScript Rendermethode holt sich dann das entsprechenden inline HTML Template und befüllt Platzhalter mit Werten des Formularelements und manipuliert das inline HTML Template ggf. mittels JavaScript. Das TYPO3 Form Framework liefert bereits eine große Anzahl solcher Inspector Editors, die für die meisten Anwendungsfälle ausreichend seien sollten. Falls nötig, ist das ganze System erweiterbar. In unserem Fall können wir komplett auf die vom Form Framework mitgelieferten Inspector Editors bauen und benötigen keine eigene Implementierung.

Da wir die Konfiguration vom Fieldset Formularelement mittels __inheritances in unser RepeatableContainer Formularelement kopiert haben, sind hier schon einige Inspector Editors vom Fieldset kopiert worden. Das folgende Snippet zeigt den Zustand der Konfiguration direkt nach dem internen Auflösen der __inheritances.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            RepeatableContainer:
              formEditor:
                editors:
                  100:
                    identifier: 'header'
                    templateName: 'Inspector-FormElementHeaderEditor'
                  200:
                    identifier: 'label'
                    templateName: 'Inspector-TextEditor'
                    label: 'formEditor.elements.Fieldset.editor.label.label'
                    propertyPath: 'label'
                  9999:
                    identifier: 'removeButton'
                    templateName: 'Inspector-RemoveElementEditor'

Nun wird diese Konfigurationskopie vom Fieldset verändert (siehe Beschreibung zu __inheritances). Beispielsweise wird die Eigenschaft label vom Inspector Editor 200 verändert und es werden weitere Inspector Editors hinzugefügt.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            RepeatableContainer:
              formEditor:
                editors:
                  200:
                    label: 'formEditor.elements.RepeatableContainer.editor.label.label'
                  400:
                    identifier: 'copyButtonLabel'
                    templateName: 'Inspector-TextEditor'
                    label: 'formEditor.elements.RepeatableContainer.editor.copyButtonLabel.label'
                    propertyPath: 'properties.copyButtonLabel'
                  500:
                    identifier: 'removeButtonLabel'
                    templateName: 'Inspector-TextEditor'
                    label: 'formEditor.elements.RepeatableContainer.editor.removeButtonLabel.label'
                    propertyPath: 'properties.removeButtonLabel'
                  600:
                    identifier: 'minimumCopies'
                    templateName: 'Inspector-TextEditor'
                    label: 'formEditor.elements.RepeatableContainer.editor.minimumCopies.label'
                    propertyPath: 'properties.minimumCopies'
                    propertyValidatorsMode: 'OR'
                    propertyValidators:
                      10: 'Integer'
                      20: 'FormElementIdentifierWithinCurlyBracesExclusive'
                  700:
                    identifier: 'maximumCopies'
                    templateName: 'Inspector-TextEditor'
                    label: 'formEditor.elements.RepeatableContainer.editor.maximumCopies.label'
                    propertyPath: 'properties.maximumCopies'
                    propertyValidatorsMode: 'OR'
                    propertyValidators:
                      10: 'Integer'
                      20: 'FormElementIdentifierWithinCurlyBracesExclusive'
                  800:
                    identifier: 'showRemoveButton'
                    templateName: 'Inspector-CheckboxEditor'
                    label: 'formEditor.elements.RepeatableContainer.editor.showRemoveButton.label'
                    propertyPath: 'properties.showRemoveButton'

Standardwerte vordefinieren

Wird ein Formularelement im Form Editor dem Formular hinzugefügt, so können (und sollten) Werte des Formularelements mit Standardwerten vorbelegt werden. Diese Werte stehen dann direkt in den zugehörigen Inspector Editors. Die Verknüpfung ("vordefinierter Wert" => "Inspector Editor") wird anhand der propertyPath Eigenschft eines Inspector Editors festgestellt.

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:
            RepeatableContainer:
              formEditor:
                predefinedDefaults:
                  properties:
                    minimumCopies: 0
                    maximumCopies: 10
                    showRemoveButton: true
                    copyButtonLabel: 'formEditor.elements.RepeatableContainer.editor.copyButtonLabel.value'
                    removeButtonLabel: 'formEditor.elements.RepeatableContainer.editor.removeButtonLabel.value'

3. Hook registrieren

Der Einstieg unserer Businesslogik zum dynamischen Erstellen von Formularelementen ist ein Hook. Wir verwenden den Hook "afterInitializeCurrentPage", welcher in der Datei EXT:repeatable_form_elements/ext_localconf.php konsumiert wird.

if (TYPO3_MODE === 'FE') {
    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'][1511196413]
        = \TRITUM\RepeatableFormElements\Hooks\FormHooks::class;
}

Fertig... fürs Erste

Mit all diesen Schritten haben wir nunmehr die grundlegende Konfiguration unseres komplexen Formularelements für das TYPO3 Form Framework abgeschlossen. Damit können wir uns der Business Logik widmen. Wir hoffen, dieser ultimative Walkthrough hat dir viel Neues zeigen können und freuen uns auf Dein Feedback. All the best and happy coding!