In TYPO3 9 enthält das TYPO3 eigene Kontakt Formular System "Form Framework" (EXT:form) zwar bereits 22 Element Typen, die man beim Erstellen von Formularen benutzen kann. Ein Dropdown Element, bei dem der Nutzer im Formular sein Land aus einer Länderliste auswählen kann, ist aber leider nicht mit dabei, obwohl das sicher in sehr vielen Projekten immer wieder eine Anforderung ist. Daher haben wir uns an dem Artikel "How to create TYPO3 Form select element with options selected from database" von Daniel Siepmann orientiert und ein neues Formular Element entwickelt, dass diese Möglichkeit nun bietet.
Voraussetzungen
Für das folgende Beispiel basiert auf TYPO3 9 und 10 und man benötigt eine Extension, die wir im Folgenden mit EXT:extension bezeichnen. Diese kann die Extension sein, die ihr ohnehin schon im System für euer Templating als Provider Extension habt. Zusätzlich wird mindestens die Extension EXT:static_info_tables benötigt als Datenquelle für die Länder der Welt. Zusätzlich machen weitere Extensions wie EXT:static_info_tables_de, EXT:static_info_tables_es, usw. Sinn, wenn man die Ländernamen auch in anderen Sprachen als englisch haben möchte.
Die php Klasse
Für ein neues Formular Element in EXT:form benötigt man eine php Klasse, die sich vor allem um das Füllen der Dropdown Optionen mit Daten kümmert, diese liegt in unserem Beispiel dann in EXT:extension/Classes/FormElements/CountryOptions.php:
<?php
namespace Vendor\Extension\FormElements;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement;
use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;
class CountryOptions extends GenericFormElement
{
protected $valueField = '';
protected $labelField = '';
public function setProperty(string $key, $value)
{
// see form element config for the static country column to be used for the select option value
if ($key === 'valueField') {
$this->valueField = $value;
}
// see form element config for the static country column to be used for the select option label
if ($key === 'labelField') {
$this->labelField = $value;
$this->setProperty('options', $this->getOptions($this->valueField, $this->labelField));
return;
}
parent::setProperty($key, $value);
}
protected function getOptions(string $valueField, string $labelField = 'cn_short_en') : array
{
$options = [];
foreach ($this->getCountries($uid) as $country) {
$label = $country[$labelField];
// dynamic label field generation based on current language isocode if marker {currentLanguage} is present
if (strstr($labelField, '{currentLanguage}') !== false) {
$siteLanguage = $GLOBALS['TYPO3_REQUEST']->getAttribute('language')->getTwoLetterIsoCode();
if (isset($country[str_replace('{currentLanguage}', $siteLanguage, $labelField)]) && $country[str_replace('{currentLanguage}', $siteLanguage, $labelField)]) {
$label = $country[str_replace('{currentLanguage}', $siteLanguage, $labelField)];
}
}
$options[$country[$valueField]] = $label;
}
asort($options);
return $options;
}
protected function getCountries() : array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('static_countries');
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
return $queryBuilder->select('*')->from('static_countries')->execute()->fetchAll();
}
}
Die Element Defintion im Frontend
Dann muss das Element im Form Framework angemeldet werden über folgenden YAML Code. Dieser Code kann bspw. in EXT:extension/Configuration/Form/Frontend.yaml geschrieben werden:
TYPO3:
CMS:
Form:
prototypes:
standard:
formElementsDefinition:
SingleSelectWithCountries:
__inheritances:
10: 'TYPO3.CMS.Form.prototypes.standard.formElementsDefinition.SingleSelect'
implementationClassName: 'Vendor\Extension\FormElements\CountryOptions'
renderingOptions:
templateName: 'SingleSelect'
Mit dem folgenden Typoscript wird diese YAML Konfiguration dann beim Formular Framework angemeldet:
plugin.tx_form {
settings {
yamlConfigurations {
1490817689 = EXT:extension/Configuration/Form/Frontend.yaml
}
}
}
Die Element Defintion im Backend
Der folgende Teil ist grundsätzlich optional. Dieser Teil ist nur relevant, wenn man das Element im Formular Editor im TYPO3 CMS auch editieren können möchte, einen Eintrag im Anlegen-Assistent wünscht, ein Icon definieren möchte, usw. Dieser Code kann bspw. in EXT:extension/Configuration/Form/Backend.yaml geschrieben werden:
TYPO3:
CMS:
Form:
prototypes:
standard:
formEditor:
formEditorPartials:
FormElement-SingleSelectWithCountries: 'Stage/SimpleTemplate'
dynamicRequireJsModules:
additionalViewModelModules:
- 'TYPO3/CMS/Extension/Backend/FormEditor/SingleSelectWithCountriesViewModel'
formElementsDefinition:
SingleSelectWithCountries:
formEditor:
label: 'Countries'
group: custom
groupSorting: 1000
iconIdentifier: form-single-select
editors:
100:
identifier: header
templateName: Inspector-FormElementHeaderEditor
200:
identifier: label
templateName: Inspector-TextEditor
label: formEditor.elements.FormElement.editor.label.label
propertyPath: label
230:
identifier: elementDescription
templateName: Inspector-TextEditor
label: formEditor.elements.FormElement.editor.elementDescription.label
propertyPath: properties.elementDescription
250:
identifier: inactiveOption
templateName: Inspector-TextEditor
label: formEditor.elements.SelectionMixin.editor.inactiveOption.label
propertyPath: properties.prependOptionLabel
fieldExplanationText: formEditor.elements.SelectionMixin.editor.inactiveOption.fieldExplanationText
doNotSetIfPropertyValueIsEmpty: true
700:
identifier: gridColumnViewPortConfiguration
templateName: Inspector-GridColumnViewPortConfigurationEditor
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.label
configurationOptions:
viewPorts:
10:
viewPortIdentifier: xs
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.xs.label
20:
viewPortIdentifier: sm
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.sm.label
30:
viewPortIdentifier: md
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.md.label
40:
viewPortIdentifier: lg
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.lg.label
numbersOfColumnsToUse:
label: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.label
propertyPath: 'properties.gridColumnClassAutoConfiguration.viewPorts.{@viewPortIdentifier}.numbersOfColumnsToUse'
fieldExplanationText: formEditor.elements.FormElement.editor.gridColumnViewPortConfiguration.numbersOfColumnsToUse.fieldExplanationText
800:
identifier: requiredValidator
templateName: Inspector-RequiredValidatorEditor
label: formEditor.elements.FormElement.editor.requiredValidator.label
validatorIdentifier: NotEmpty
propertyPath: properties.fluidAdditionalAttributes.required
propertyValue: required
configurationOptions:
validationErrorMessage:
label: formEditor.elements.FormElement.editor.requiredValidator.validationErrorMessage.label
propertyPath: properties.validationErrorMessages
fieldExplanationText: formEditor.elements.FormElement.editor.requiredValidator.validationErrorMessage.fieldExplanationText
errorCodes:
10: 1221560910
20: 1221560718
30: 1347992400
40: 1347992453
9999:
identifier: removeButton
templateName: Inspector-RemoveElementEditor
properties:
containerClassAttribute: input
elementClassAttribute: ''
elementErrorClassAttribute: error
Mit dem folgenden Typoscript wird diese YAML Konfiguration dann beim Formular Framework angemeldet:
module.tx_form {
settings {
yamlConfigurations {
1490817689 = EXT:extension/Configuration/Form/Backend.yaml
}
}
}
Dann gibt es aktuell auch in TYPO3 Version 10 noch eine Besonderheit. Um das Element vollständig im Formular Editor bedienen zu können und eine korrekte Darstellung zu erhalten, muss man noch eine JavaScript Datei anlegen. Wir hoffen dass dieser uns doch ein wenig unnütz erscheinende Schritt in zukünftigen TYPO3 Versionen einmal entfallen wird. Die JavaScript Datei muss gleichlautend wie das Element benannt sein und in folgendem Ordner liegen: EXT:extension/Resources/Public/JavaScript/Backend/FormEditor/SingleSelectWithCountriesViewModel.js. Dies ist der Inhalt:
define([
'jquery',
'TYPO3/CMS/Form/Backend/FormEditor/Helper'
], function ($, Helper) {
'use strict';
return (function ($, Helper) {
/**
* @private
*
* @var object
*/
var _formEditorApp = null;
/**
* @private
*
* @return object
*/
function getFormEditorApp() {
return _formEditorApp;
};
/**
* @private
*
* @return object
*/
function getPublisherSubscriber() {
return getFormEditorApp().getPublisherSubscriber();
};
/**
* @private
*
* @return object
*/
function getUtility() {
return getFormEditorApp().getUtility();
};
/**
* @private
*
* @param object
* @return object
*/
function getHelper() {
return Helper;
};
/**
* @private
*
* @return object
*/
function getCurrentlySelectedFormElement() {
return getFormEditorApp().getCurrentlySelectedFormElement();
};
/**
* @private
*
* @param mixed test
* @param string message
* @param int messageCode
* @return void
*/
function assert(test, message, messageCode) {
return getFormEditorApp().assert(test, message, messageCode);
};
/**
* @private
*
* @return void
* @throws 1491643380
*/
function _helperSetup() {
assert('function' === $.type(Helper.bootstrap),
'The view model helper does not implement the method "bootstrap"',
1491643380
);
Helper.bootstrap(getFormEditorApp());
};
/**
* @private
*
* @return void
*/
function _subscribeEvents() {
/**
* @private
*
* @param string
* @param array
* args[0] = formElement
* args[1] = template
* @return void
*/
getPublisherSubscriber().subscribe('view/stage/abstract/render/template/perform', function (topic, args) {
if (args[0].get('type') === 'SingleSelectWithCountries') {
getFormEditorApp().getViewModel().getStage().renderSimpleTemplateWithValidators(args[0], args[1]);
}
});
};
/**
* @public
*
* @param object formEditorApp
* @return void
*/
function bootstrap(formEditorApp) {
_formEditorApp = formEditorApp;
_helperSetup();
_subscribeEvents();
};
/**
* Publish the public methods.
* Implements the "Revealing Module Pattern".
*/
return {
bootstrap: bootstrap
};
})($, Helper);
});
Nutzung des Elements
Das Element selber kann man dann mit folgendem YAML Code in einem Formular einsetzen. Alternativ kann man den Neuanlegen Assistent des Formular Editors im Backend nutzen, wenn man den oben genannten Backend Code Teil auch eingesetzt hat.
Mit der Konfigurationsoption "valueField" kann man die Spalte der static_countries Tabelle definieren (diese Tabelle ist Teil der Extension EXT:static_info_tables), die für zu übertragenden Wert genutzt werden soll. Im folgenden Beispiel ist das der englische Landesname.
Mit der Konfigurationsoption "labelField" kann man die Spalte der static_countries Tabelle definieren, die für den anzuzeigenden Wert verwendet werden soll. Hier haben wir eine kleine Besonderheit entwickelt und zwar den Platzhalter "{currentLanguage}", welcher dann in obiger php Klasse automatisch anhand der aktuellen Sprache des Frontends mit "de", "es", usw. befüllt wird. Dafür werden dann aber für jede Sprache der Web-Site die weiteren Extensions wie EXT:static_info_tables_de, EXT:static_info_tables_es, usw. benötigt. Man kann aber auch statt dieses Markers einfach einen fixen Wert einsetzen.
...
-
properties:
fluidAdditionalAttributes:
required: required
valueField: cn_short_en
labelField: 'cn_short_{currentLanguage}'
prependOptionLabel: 'please choose'
defaultValue: ''
type: SingleSelectWithCountries
identifier: country
label: Country
validators:
-
identifier: NotEmpty
...