Erstellung eines eigenen Formularfeldes

From Joomla! Documentation

This page is a translated version of the page Creating a custom form field type and the translation is 100% complete.
Other languages:
Deutsch • ‎English • ‎Nederlands • ‎español • ‎français
Joomla! 
3.x
Serie
Copyedit.png
This Article Needs Your Help

This article is tagged because it NEEDS REVIEW. You can help the Joomla! Documentation Wiki by contributing to it.
More pages that need help similar to this one are here. NOTE-If you feel the need is satistified, please remove this notice.


JForm, ein Feature das in Joomla! 2.5 eingeführt wurde, vereinfacht das Erstellen von HTML Formularen (<form>). Formulare die mit JForm erstellt wurden, beinhalten Formularfelder, implementiert als JFormFields. Für jede Art von Feldtyp, den man in einem Formular finden kann, wie ein Text-Feld oder ein Datum-Feld, gibt es ein JFormField. JForm unterstützt eine große Auswahl von Standardfeldtypen. Für eine vollständige Liste, siehe Standardformularfeldtypen.

Joomla! ermöglicht es Standardfeldtypen zu erweitern oder ein eigenes zu definieren. Zum Beispiel könnte die eigene Komponente Telefonbucheinträge verwalten, was dazu führt, dass man einen Formularfeldtypen erstellen will, der eine Liste an Städten ausgibt. Es gibt mehrere Vorteile beim Erstellen eines eigenen Formfeldtypens:

  • Man kann Standardfeldtypen mit den eigenen Feldtypen in einem JForm-basierten Formular kombinieren.
  • Man kann endlich ein wiederverwendbares Code-Paket einfach überall im Code einsetzen.
  • Erweiterungen, die mit der eigenen Erweiterung zusammenarbeiten, sind in der Lage ein Formularfeld zu erstellen, ohne sich in deren Datenbank oder anderen internen Angelegenheiten einzumischen.

Vorraussetzungen für Formularfeldtyp-Klassen

Ein Formularfeldtyp wird in einer Klasse definiert, welche keine Unterklasse von JFormField sein darf (jedenfalls nicht direkt). Um richtig zu funktionieren, müssen zumindest drei Methoden implementiert sein:

  • public function getLabel()
    Diese Funktion wird aufgerufen um das Label zu erstellen, welches zum Feld gehört und gibt eine HTML-Zeichenkette zurück, die dieses beinhaltet. Seitdem JFormField eine ready-to-use getLabel() Methode implementiert, erstellen eigene Formfeldtypen normalerweise nicht mehr ihre eigene. Denn wenn man sie auslässt, wird diese geerbte Methode benutzt. Das wird auch für mehr Konsistenz und Geschwindigkeit empfohlen, außer man will wirklich das HTML des Labels verändern.
  • public function getInput()
    Hiermit wird das Feld selbst erstellt und gibt eine HTML-Zeichenkette zurück, in der es enthalten ist. Hier passiert überlicherweise das Meiste der Entwicklung. In dem Beispiel mit dem Telefonbuch-Städte-Feld holt diese Funktion eine Liste aller verfügbaren Städte und gibt ein HTML <select> mit den Städten als <option> zurück.
  • public function getValue()
    Diese Funktion gibt den Wert des Feldes zurück. Dieser Wert wird von der Funktion LoadFormData des Models geholt.

Innerhalb des Codes, müssen die Attribute aus dem Feld in der XML-Formulardefinition geholt werden, die vom Benutzer gesetzt wurden. Manche dieser Attribute können über protected Variablen von JFormField erreicht werden. Das Attribut name steht zum Beispiel im Code als $this->name zur Verfügung. Genauso sind label, description, default, multiple und class auch als Attribut von $this vorhanden. Andere Parameter die man möglicherweise definiert hat, sind über das Array $this->element erreichbar. Das Attribut size liegt z.B. in $this->element['size'].

Welche Klasse erweitern?

Um ein Formularfeld in JForm nutzen zu können, muss es eine Unterklasse von JFormField sein. Dennoch muss es kein direkter Ableger der Klasse sein. Man kann auch bereits vorhandene (Standard oder eigene) Formfeldtypen und somit nützlichen Code vererben.

Wenn der eigene Formfeldtyp ziemlich ähnlich mit einem existierenden Typ ist, sollte man ihn als Unterklasse umsetzen. Insbesondere wenn der Formfeldtyp eine Liste ist, bitte von JFormFieldList erben. Die getOptions() Methode kann ganz einfach überschrieben werden, um die anzuzeigende Auswahl zurückzugeben. Die getInput() Methode wird diese Auswahl dann zu HTML umwandeln.

Um einen existierenden Typen zu erweitern, z.B. JFormFieldList, lädt man den folgenden Code hinter jimport('joomla.form.formfield');:

jimport('joomla.form.helper');
JFormHelper::loadFieldClass('list');

Wenn der Formfeldtyp keinem existierenden Typen ähnelt, erbt man direkt von JFormField.

Speicherort der Dateien

  • Standard Formfeldtypen findet man in libraries/joomla/form/fields/. Dort sollen weder eigene Felder abgelegt, noch dieser Pfad im eigenen Code verwendet werden. Die Standardtypen sind für gewöhnlich gute Beispiele.
  • Erstellte Feldtypen die zur eigenen Komponente gehören, liegen normalerweise in administrator/components/<Name der Komponente>/models/fields. Man kann diesen Pfad oder einen anderen im Code nutzen:
JForm::addFieldPath(JPATH_COMPONENT . '/models/fields');
  • Die XML-Dateien die Formulare definieren, sind normalerweise in administrator/components/<Name der Komponente>/models/forms zu finden. So etwas wie im folgenden Abschnitt kann genutzt werden, um einen Pfad zum eigenen Formular zu setzen:
JForm::addFormPath(JPATH_COMPONENT . '/models/forms');

Namenskonventionen und Grundgerüste

In diesem Abschnitt repräsentiert <KomponentenName> den camel-case Namen der Komponente und <FeldName> den camel-case Namen des Formfeldtypens. Die Klasse des Feldes sollte in administrator/components/<Name der Komponente>/models/fields/<Name des Feldes>.php liegen und so aussehen:

<?php
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die('Restricted access');

jimport('joomla.form.formfield');

// The class name must always be the same as the filename (in camel case)
class JFormField<FieldName> extends JFormField {

	//The field class must know its own type through the variable $type.
	protected $type = '<FieldName>';

	public function getLabel() {
		// code that returns HTML that will be shown as the label
	}

	public function getInput() {
		// code that returns HTML that will be shown as the form field
	}
}

Gruppierung eigener Feldtypen

Warnung: Diese Information ist teilweise falsch und benötigt Verbesserung.

Eigene Feldtypen können gruppiert werden, indem ein Unterstrich im Feldnamen benutzt wird. Eine Feld-Klasse mit einem Namen wie "JFormFieldMy_randomField" muss zum Beispiel in administrator/components/<Name der Komponente>/models/fields/my/randomField.php angelegt werden. Man kann einem Formfeldnamen einen Gruppennamen voranstellen, dann kann man einen Unterstrich setzen und dann erst den Namen des Feldes.

Beispiel eines eigenen Feldtyps

Angenommen man arbeitet an einer Komponente namens com_phonebook und man möchte ein Feld definieren, dass Städte beinhaltet. Dann erstellt man die Datei administrator/components/com_phonebook/models/fields/city.php und schreibt so etwas wie das Folgende:

<?php
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die('Restricted access');

jimport('joomla.form.formfield');

class JFormFieldCity extends JFormField {
	
	protected $type = 'City';

	// getLabel() left out

	public function getInput() {
		return '<select id="'.$this->id.'" name="'.$this->name.'">'.
		       '<option value="1" >New York</option>'.
		       '<option value="2" >Chicago</option>'.
		       '<option value="3" >San Francisco</option>'.
		       '</select>';
	}
}

Ein eher fortgeschrittener Ansatz ist, die Klasse JFormFieldList zu erweitern. Angenommen man möchte ein Dropdown-Feld erstellen, welches Städte dynamisch aus der Datenbank lädt, basierend auf einer dynamischen Bedingung, kann man es auf die folgende Art und Weise machen:

<?php
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die('Restricted access');

JFormHelper::loadFieldClass('list');

class JFormFieldCity extends JFormFieldList {

	protected $type = 'City';

	public function getOptions() {
                $app = JFactory::getApplication();
                $country = $app->input->get('country'); //country is the dynamic value which is being used in the view
                $db = JFactory::getDbo();
                $query = $db->getQuery(true);
                $query->select('a.cityname')->from('`#__tablename` AS a')->where('a.country = "'.$country.'" ');
		$rows = $db->setQuery($query)->loadObjectlist();
                foreach($rows as $row){
                    $cities[] = $row->cityname;
                }
                // Merge any additional options in the XML definition.
		$options = array_merge(parent::getOptions(), $cities);
                return $options;
	}
}

Das obige Beispiel zeigt eine einfache Abfrage für eine Liste an Städten aus einer Tabelle, in der Städtenamen mit dem entsprechenden Land angezeigt werden. Man kann ein Dropdown-Feld erstellen, dass auf komplexeren Abfragen basiert.

Werte einer Listenoption setzen und JSON oder eine API anstelle eines Datenbankaufrufs verwenden

Wenn ein API-Aufruf anstelle eines Datenbankaufrufs verwendet werden soll, um ein benutzerdefiniertes Listenelement zu erstellen, ist folgender Code zu empfehlen.

Dieses Feld wurde in einem Modul erstellt. Damit es funktioniert, muss es gespeichert werden in

  • mod_modulename/models/fields/stackexchangesites.php

Die Namenskonvention ist wichtig, da sie innerhalb des Funktionsnamens verwendet wird.

<?php
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die('Restricted access');

// call the list field type
JFormHelper::loadFieldClass('list');

// The class name must always be the same as the filename (in camel case)
// extend the list field type
class JFormFieldStackexchangesites extends JFormFieldList
{
    //The field class must know its own type through the variable $type.
    protected $type = 'Stackexchangesites';

    // get the options for the list field
    public function getOptions()
    {
       // insert your JSON here or else call an API
        $json = {"api_site_parameter":"meta.stackoverflow","site_url":"https://meta.stackoverflow.com"~}
        // decode the JSON
        $sites = json_decode($json, true);
       
        // use a for each to iterate over the JSON
        foreach($sites['items'] as $site)
        {
           // choose the element of the JSON we want, and set it as a variable so we can use it in our array.
            $site = $site['api_site_parameter'];
            $site_url = $site['site_url'];
            // set an array and start adding values to it.  Set another array within our array to set our value / text items.
            $stackExchangesSitesOptions[] = array("value" => $site, "text" => $site_url);
        }

        // Merge any additional options in the XML definition.
        $options = array_merge(parent::getOptions(), $stackExchangesSitesOptions);
        return $options;
    }
}

Auf dem Frontend liefert der einfache Aufruf des Parameters den Wert.

$stackexchangesites = $params->get('stackexchangesites');

Fallen

Ein eigenes Feld zu laden kann in einem Fatal Error enden, wenn ein Core-Feld mit demselben Feldnamen existiert und das eigene Feld dieses Core-Feld erweitert.

Betrachtet man die Datei testfields/radio.php mit dem Inhalt

<?php

class TestFormFieldRadio extends JFormFieldRadio {}

Der Aufruf von JFormHelper::loadFieldClass('radio') wird einen Fatal Error liefern: Class 'JFormFieldRadio' not found.

Es gibt dafür zwei Gründe.

  1. JLoader kann JFormFieldRadio nicht automatisch laden, weil der Klassenname (JFormField*) nicht mit dem Pfadnamen (joomla/form/fields/* - man beachte den Plural von Feldern) übereinstimmt.
  2. JFormHelper kann JFormFieldRadio nicht automatisch laden, weil eigens erstellte Pfade zuerst gescannt werden und der angeforderte Feldtyp ('radio') aufgelöst wird, bevor die Core-Klassen erreicht wurden.

Lösung

Das Core-Feld direkt verlangen:

<?php
require_once JPATH_LIBRARIES . '/joomla/form/fields/radio.php';

class TestFormFieldRadio extends JFormFieldRadio {}

und JFormHelper::loadFieldClass richtig mit 'test.radio' anstatt 'radio' benutzen.

Benutzen des eigenen Feldtyps

Verbunden mit einem Formular

Um den Feldtypen City zu benutzen, ist es nötig die XML-Datei zu aktualisieren, welche das Formularfeld beinhaltet. Man öffnet die XML-Datei in dem Ordner administrator/components/com_phonebook/models/forms und fügt das Feld üblicherweise hinzu:

<field name="title" type="City" label="JGLOBAL_TITLE"
	description="JFIELD_TITLE_DESC"
	required="true" />

Der Attributname ist cAsE-sEnSiTiVe.

Zuzüglich ist es nötig einen Feldpfad zum Eltern-<fieldset> hinzuzufügen.

<fieldset addfieldpath="/administrator/components/<component name>/models/fields">

Nicht mit einem Formular verbunden

Wenn man zum Beispiel ein Feld als Dropdown, in einer Komponente als admin/site Filter, braucht:

//Get custom field
JFormHelper::addFieldPath(JPATH_COMPONENT . '/models/fields');
$cities = JFormHelper::loadFieldType('City', false);
$cityOptions=$cities->getOptions(); // works only if you set your field getOptions on public!!

Überschreiben von getLabel()

Wie bereits in dem Abschnitt Vorraussetzungen für Formularfeldtyp-Klassen erwähnt, definieren eigene Formfeldtypen normalerweise nicht ihre eigene getLabel() Methode. Möchte man ein eigenes Label erstellen, kann man immer noch getLabel() in Anspruch nehmen, was jede Feldtyp-Klasse von JFormField erbt indem man es wie folgt definiert:

public function getLabel() {
     return '<span style="text-decoration: underline;">' . parent::getLabel() . '</span>';
}

Dieser Code unterstreicht Formular-Labels. (Bitte beachten, dass CSS der bevorzugte Weg ist, um Labels zu unterstreichen.)

Wenn man etwas völlig anderes machen möchte, kann man es natürlich auch komplett überschreiben:

public function getLabel() {
	// Initialize variables.
	$label = '';
	$replace = '';

	// Get the label text from the XML element, defaulting to the element name.
	$text = $this->element['label'] ? (string) $this->element['label'] : (string) $this->element['name'];

	// Build the class for the label.
	$class = !empty($this->description) ? 'hasTip' : '';
	$class = $this->required == true ? $class.' required' : $class;
		
	// Add replace checkbox
	$replace = '<input type="checkbox" name="update['.$this->name.']" value="1" />';
		
	// Add the opening label tag and main attributes attributes.
	$label .= '<label id="'.$this->id.'-lbl" for="'.$this->id.'" class="'.$class.'"';

	// If a description is specified, use it to build a tooltip.
	if (!empty($this->description)) {
		$label .= ' title="'.htmlspecialchars(trim(JText::_($text), ':').'::' .
				JText::_($this->description), ENT_COMPAT, 'UTF-8').'"';
	}

	// Add the label text and closing tag.
	$label .= '>'.$replace.JText::_($text).'</label>';
	
	return $label; 
}

Dieses Beispiel fügt eine Checkbox innerhalb des Labels ein.

Sample Component Code

Below is the code for a small component which you can install and run and can adapt to experiment further with custom fields. The component includes two custom fields

  1. A "City" field as described above (except that the options are hard-coded rather than being selected from a database). We'll also allow multiple Cities to be selected and pre-select two of them as default.
  2. A custom "time" field which maps to the HTML "time" input type and includes underlining the label and support for setting a minimum and maximum time.

Create the following five files in a folder called com_custom_fields. Then zip up this folder to create com_custom_fields.zip. Install this component on your Joomla instance. Once installed, navigate on your browser to your Joomla site and add to the URL the parameter &option=com_custom_fields. This should display the form with the two custom fields. You can submit the form and see the HTTP POST parameters using the browser development tools but the component doesn't contain any code which handles those parameters.

(As described in Basic form guide, the approach below isn't the recommended way to design Joomla components but it's written in this minimalist fashion to focus on the custom fields aspects).

com_custom_fields.xml Manifest file for the component

<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="3.1.0" method="upgrade">

	<name>com_custom_fields</name>
	<version>1.0.0</version>
	<description>Custom Fields demo component</description>
	
	<administration>
	</administration>

	<files folder="site">
		<filename>custom_fields.php</filename>
		<filename>form_definition.xml</filename>
		<filename>City.php</filename>
		<filename>Time.php</filename>
	</files>
</extension>

custom_fields.php The main code file which is run when an HTTP GET or POST is directed towards this component.

<?php
defined('_JEXEC') or die('Restricted Access');

use Joomla\CMS\Form\Form;

$form = Form::getInstance("sample", __DIR__ . "/form_definition.xml", array("control" => "jform"));
Form::addFieldPath(__DIR__);
?>
<form action="<?php echo JRoute::_('index.php?option=com_custom_fields'); ?>"
    method="post" name="adminForm" id="adminForm" enctype="multipart/form-data">

	<?php echo $form->renderField('mytime');  ?>
	
	<?php echo $form->renderField('mycity');  ?>

	<button type="submit">Submit</button>
</form>

form_definition.xml File containing the XML for the form definition, basically the two custom fields.

<?xml version="1.0" encoding="utf-8"?>
<form name="myForm">
	<field
		name="mytime"
		type="time"
		label="Time:"
		class="inputbox"
		min="09:00"
		max="17:30"
		required="true" />
	<field name="mycity" 
		type="City"
		label="City:"
		required="true"
		multiple="true"
		class="inputbox" />
</form>

City.php PHP code for the City custom field.

<?php
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\Form\FormHelper;

FormHelper::loadFieldClass('list');

class JFormFieldCity extends JFormFieldList {

	protected $type = 'City';

	public function getOptions() {
		$cities = array(
					array('value' => 1, 'text' => 'New York'),
					array('value' => 2, 'text' => 'Chicago'),
					array('value' => 3, 'text' => 'San Francisco'),
					);
		// Merge any additional options in the XML definition.
		$options = array_merge(parent::getOptions(), $cities);

		// pre-select values 2 and 3 by setting the protected $value property
		$this->value = array(2, 3);

		return $options;
	}
}

Time.php PHP code for the time custom field.

<?php
defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;

class JFormFieldTime extends FormField
{
	protected $type = 'time';

	protected function getInput()
	{
		// get relevant attributes which were defined in the XML form definition
		$attr = !empty($this->class) ? ' class="' . $this->class . '"' : '';
		$attr .= !empty($this->element['min']) ? ' min="' . $this->element['min'] . '"' : '';
		$attr .= !empty($this->element['max']) ? ' max="' . $this->element['max'] . '"' : '';

		// set up html, including the value and other attributes
		$html = '<input type="time" name="' . $this->name . '" value="' . $this->value . '"' . $attr . '/>';

		return $html;
	}
	
	public function getLabel() {
		return '<span style="text-decoration: underline;">' . parent::getLabel() . '</span>';
	}
}