Difference between revisions of "Creating a custom form field type"

From Joomla! Documentation

m (moved Overriding JFormFields to Creating a custom form field type: Moved page for consistency)
(Several markup changes.)
 
(55 intermediate revisions by 22 users not shown)
Line 1: Line 1:
A new feature in Joomla 1.6 is called JForm which lets you to easily create forms, one part of JForm is JFormFields. JFormFields takes care of all the different fields you can find in a form. JFormFields supports a large selection of fields, see the [[Standard form field types]] for in-depth info.
+
<noinclude><languages /></noinclude>
  
Joomla 1.6 makes it possible to override and/or extend these field types, here is how to do that.
+
<noinclude>{{Joomla version|version=3.x|comment=<translate><!--T:1-->
 +
series</translate>}}</noinclude>
 +
{{-}}
 +
{{page|needs review}}
  
== Step 1: Location of files ==
+
<translate>
 +
<!--T:2-->
 +
[https://api.joomla.org/cms-3/classes/Joomla.CMS.Form.Form.html JForm], a feature introduced in Joomla! 2.5, lets you easily create HTML forms (''<form>''). Forms created using JForm consist of [[S:MyLanguage/Form field|form fields]], implemented as [https://api.joomla.org/cms-3/classes/Joomla.CMS.Form.FormField.html JFormField]. There is a JFormField for each different field type you can find in a form, such as a text field type and a date field type. JForm supports a large selection of standard field types. For a full list, see [[S:MyLanguage/Standard form field types|Standard form field types]].
 +
</translate>
  
* The default form fields are located in "joomla/libraries/joomla/form/fields/"
+
<translate>
* The overridden fields are usually located in "administrator/components/<name of your component>/models/fields", you can specify this or other path from your code:
+
<!--T:3-->
<source lang="php">JForm::addFieldPath(JPATH_COMPONENT . DS . 'models' . DS . 'fields');</source>
+
Joomla! makes it possible to extend standard field types or define your own. For example, if your component manages phone book entries, you might want to define a form field type that outputs a select list of cities. There are several advantages to defining a custom form field type:
* The XML files with form fields are usually located in "administrator/components/<name of your component>/models/forms". Use something like the following snippet to specify a path to your forms:
+
</translate>
<source lang="php">JForm::addFormPath(JPATH_COMPONENT . DS . 'models' . DS . 'forms');</source>
 
  
== Step 2: Setting up base ==
+
<translate>
 +
<!--T:4-->
 +
* You will be able to mix standard field types with your custom field type in a JForm-based form.
 +
* You will eventually have a reusable code package that can be used easily throughout your code.
 +
* Extensions that collaborate with your extension will be able to create form fields without meddling with your database tables and other internals.
 +
</translate>
  
The first file to create is your base class. The base class is your starting point for overriding JFormField.
+
<translate>
 +
== Form Field Type Class Requirements == <!--T:5-->
 +
</translate>
  
Create a new file called myform.php and place it in the folder administrator/components/<name of your component>/models/fields. Add the following code to the file:
+
<translate>
<source lang="php"><?php
+
<!--T:6-->
 +
A form field type is defined in a [[wikipedia:Class (computer programming)|class]] that must be a (not necessarily direct) subclass of JFormField. To work correctly, the class must define at least three methods:
 +
</translate>
 +
 
 +
* ''public function getLabel()''
 +
<translate><!--T:7-->
 +
*: This function will be called to create the label that belongs to your field and must return an HTML string containing it. Since JFormField defines a ready-to-use ''getLabel()'' implementation, custom form field types usually do not define their own ''getLabel()''. If you leave it out, the inherited method of creating labels will be used. It is recommended to leave out the ''getLabel()'' method for consistency and speed unless you actually want to modify the label's HTML.</translate>
 +
* ''public function getInput()''
 +
<translate><!--T:8-->
 +
*: This function will be called to create the field itself and must return an HTML string containing it. This is also where most of the processing usually happens. In our phone book City field example, this function will have to retrieve a list of available cities and return an HTML ''<select>'' with the cities inserted as ''<option>''s.</translate>
 +
* ''public function getValue()''
 +
<translate><!--T:9-->
 +
*: This function will be called to get the field value. The value is obtained from the function LoadFormData in the Model</translate>
 +
<translate>
 +
<!--T:10-->
 +
Inside your code, you will have to process the attributes set by the field's user in the XML form definition. Some of those attributes are accessible via protected member variables of JFormField. For example, the ''name'' attribute is available in your code as ''$this->name''. Similarly, ''label'', ''description'', ''default'', ''multiple'' and ''class'' are also available as properties of ''$this''. Other parameters you might have defined can be accessed through the ''$this->element'' array: the attribute ''size'' will be in ''$this->element['size']''.
 +
</translate>
 +
 
 +
<translate>
 +
== Which Class to Subclass? == <!--T:65-->
 +
</translate>
 +
 
 +
<translate>
 +
<!--T:11-->
 +
For a form field type to be usable in JForm, it needs to be a subclass of JFormField. However, it does not have to be a direct child of that class. You can also subclass an existing (standard or custom) form field type and thereby inherit useful code.
 +
</translate>
 +
 
 +
<translate>
 +
<!--T:12-->
 +
If your form field type is similar to an existing type, you '''should''' subclass that type. Especially if your form field type is a ''list'', please subclass [https://api.joomla.org/cms-3/classes/JFormFieldList.html JFormFieldList]. You only have to override ''getOptions()'' method to return the options to be shown. The ''getInput()'' method will convert those options to HTML.
 +
</translate>
 +
 
 +
<translate><!--T:13-->
 +
To subclass an existing type, for example JFormFieldList, load it by adding the following after ''jimport('joomla.form.formfield');'':</translate>
 +
 
 +
<syntaxhighlight lang="php">
 +
jimport('joomla.form.helper');
 +
JFormHelper::loadFieldClass('list');
 +
</syntaxhighlight>
 +
 
 +
<translate><!--T:14-->
 +
If your form field type is unlike any existing type, subclass ''JFormField'' directly.</translate>
 +
 
 +
<translate>
 +
== Location of Files == <!--T:15-->
 +
</translate>
 +
 
 +
<translate><!--T:16-->
 +
* The standard form field types are located in ''libraries/joomla/form/fields/''. You should not store custom fields there, nor should you have to use this path in your own code. The standard types are usually good examples.</translate>
 +
<translate><!--T:17-->
 +
* The custom field types that belong to your component are usually located in ''administrator/components/<name of your component>/models/fields''. You can specify this or another path in your code:</translate>
 +
<syntaxhighlight lang="php">JForm::addFieldPath(JPATH_COMPONENT . '/models/fields');</syntaxhighlight>
 +
<translate><!--T:18-->
 +
* The XML files that define forms are usually located in ''administrator/components/<name of your component>/models/forms''. Use something like the following snippet to specify a path to your forms:</translate>
 +
<syntaxhighlight lang="php">JForm::addFormPath(JPATH_COMPONENT . '/models/forms');</syntaxhighlight>
 +
 
 +
<translate>
 +
== Naming Conventions and Skeleton == <!--T:19-->
 +
</translate>
 +
 
 +
<translate><!--T:20-->
 +
In this section, <ComponentName> represents the camel-cased name of your component and <FieldName> represents the camel-cased name of your form field type. The field's class should be placed in ''administrator/components/<name of your component>/models/fields/<name of your field>.php'', and look like this:</translate>
 +
 
 +
<syntaxhighlight lang="php"><?php
 
// Check to ensure this file is included in Joomla!
 
// Check to ensure this file is included in Joomla!
 
defined('_JEXEC') or die('Restricted access');
 
defined('_JEXEC') or die('Restricted access');
  
abstract class JFormFieldMyForm extends JFormField {
+
jimport('joomla.form.formfield');
+
 
protected $type = 'MyForm';  
+
// 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
 +
}
 
}
 
}
</source>
+
</syntaxhighlight>
  
Important lines:
+
<translate>
* abstract class JFormField'''MyForm''' extends JFormField
+
=== Grouping Custom Field Types === <!--T:21-->
The name of the file is myform.php this means the name of the class must be the same
+
</translate>
* protected $type = ''' 'MyForm'''';
 
The $type must also have the same name as your class
 
  
== Step 3: Creating a field override ==
+
<translate><!--T:22-->
 +
'''Warning: this information is partially incorrect and needs to be improved.'''</translate>
  
With the base class in place we can now create our field overrides.  
+
<translate><!--T:23-->
 +
Custom field types can be grouped by using an underscore in the field name. A field class with a name for example like "JFormFieldMy_randomField" must be stored in ''administrator/components/<name of your component>/models/fields/my/randomField.php''. We can prefix our form field names with some group name, then we put an underscore and then a name of a field.</translate>
  
Create a new file called mylist.php and place it in the folder administrator/components/<name of your component>/models/fields. Add the following code to the file:
+
<translate>
<source lang="php"><?php
+
== An Example Custom Field Type == <!--T:24-->
 +
</translate>
 +
 
 +
<translate><!--T:25-->
 +
Suppose you're working on your component named ''com_phonebook'' and you want to define a field that contains cities. Create the file ''administrator/components/com_phonebook/models/fields/city.php'' and write something similar to the following:</translate>
 +
 
 +
<syntaxhighlight lang="php"><?php
 
// Check to ensure this file is included in Joomla!
 
// Check to ensure this file is included in Joomla!
 
defined('_JEXEC') or die('Restricted access');
 
defined('_JEXEC') or die('Restricted access');
  
jimport('joomla.form.helper');
+
jimport('joomla.form.formfield');
JFormHelper::loadFieldClass('MyForm');
+
 
 +
class JFormFieldCity extends JFormField {
 +
 
 +
protected $type = 'City';
  
class JFormFieldMyText extends JFormFieldMyForm {
+
// getLabel() left out
 
protected $type = 'MyText';
 
  
 
public function getInput() {
 
public function getInput() {
$field = JFormHelper::loadFieldType('Text');
+
return '<select id="'.$this->id.'" name="'.$this->name.'">'.
// Workaround
+
      '<option value="1" >New York</option>'.
$field->setForm($this->form);  
+
      '<option value="2" >Chicago</option>'.
$field->setup($this->element, $this->value, $this->group);
+
      '<option value="3" >San Francisco</option>'.
 +
      '</select>';
 +
}
 +
}
 +
</syntaxhighlight>
 +
 
 +
<translate>
 +
<!--T:53-->
 +
A more advanced approach is extending the ''JFormFieldList'' class. Suppose you want to create a drop-down of cities dynamically from database based on a dynamic condition. You can do this in the following way:</translate>
 +
 
 +
<syntaxhighlight lang="php"><?php
 +
// Check to ensure this file is included in Joomla!
 +
defined('_JEXEC') or die('Restricted access');
 +
 
 +
JFormHelper::loadFieldClass('list');
 +
 
 +
class JFormFieldCity extends JFormFieldList {
  
return $field->getInput();
+
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;
 
}
 
}
}  
+
}
</source>
+
</syntaxhighlight>
  
Important lines:
+
<translate><!--T:54-->
* JFormHelper::loadFieldClass('MyForm');
+
The above example shows a simple query which will show a list of cities from a table where city name belongs to its country respectively. You can create a drop-down based on more complex queries.</translate>
Here we load our base class
 
* class JFormField'''MyText''' extends JFormField'''MyForm''' {
 
Here we create our override called MyText and make it extend MyForm, our base class. The class name must always be the same as the filename
 
* protected $type = ''' 'MyText'''';
 
Same as the base class the field class also needs a $type set, this is the same name as your class name
 
* public function getInput() {
 
A field override class must always have the public function getInput()
 
  
Notice: This example contains a workaround until bug 21644 (http://joomlacode.org/gf/project/joomla/tracker/?action=TrackerItemEdit&tracker_item_id=21644) has been fixed.
+
<translate>
 +
== Setting the Values of a List Option and Using JSON or an API Instead of a Database Call == <!--T:66-->
 +
</translate>
 +
<translate>
 +
<!--T:67-->
 +
If you want to use an API call instead of a database call to build a custom list item, use the following code.
  
=== Grouping custom field types ===
+
<!--T:68-->
 +
This field was created in a module. To get it to work it needs to be saved to
 +
''mod_modulename/models/fields/stackexchangesites.php''.
 +
The naming convention is important as it is used within our function name.</translate>
 +
<syntaxhighlight lang="php">
 +
<?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';
  
Custom field types can be grouped by using an underscore in the field name. A field class with a name for example like "JFormFieldMy_randomField" must be stored in "administrator/components/<name of your component>/models/fields/my/randomField.php". We can prefix our form field names with some group name, then we put an underscore and then a name of a field.
+
    // 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);
  
== Step 4: Using the override field ==
+
        // 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);
 +
        }
  
To use the override field MyText, we need to update the XML file that contains the form fields. Open your XML file located in administrator/components/<name of your component>/models/forms and find a field that is of the type text. This example is from article.xml:
+
        // Merge any additional options in the XML definition.
<source lang="xml"><field name="title" type="text" label="JGLOBAL_TITLE"
+
        $options = array_merge(parent::getOptions(), $stackExchangesSitesOptions);
description="JFIELD_TITLE_DESC" class="inputbox" size="30"
+
        return $options;
required="true" /></source>
+
    }
 +
}</syntaxhighlight>
 +
<translate>
 +
<!--T:69-->
 +
On the Frontend simply calling the parameter gets us the value.</translate>
 +
<syntaxhighlight lang="php">
 +
$stackexchangesites = $params->get('stackexchangesites');
 +
</syntaxhighlight>
  
We change this field to look like this:
+
<translate>
<source lang="xml"><field name="title" type="MyText" label="JGLOBAL_TITLE"
+
=== Pitfalls === <!--T:55-->
description="JFIELD_TITLE_DESC" class="inputbox" size="30"
+
</translate>
required="true" /></source>
+
<translate><!--T:56-->
Notice how type="text" changed to type="'''MyText'''". The MyText here refers to the class made in step 3. The type name is cAsE-sEnSiTiVe.
+
Loading a custom field can result in a Fatal Error if a core field exists with the same filename and the custom field extends the core field.</translate>
  
With these changes in place, your override is done and ready to use.
+
<translate><!--T:57-->
 +
Consider a file ''testfields/radio.php'' containing</translate>
 +
<syntaxhighlight lang="php">
 +
<?php
 +
class TestFormFieldRadio extends JFormFieldRadio {}
 +
</syntaxhighlight>
  
== Overriding getLabel() ==
+
<translate><!--T:58-->
 +
Calling ''JFormHelper::loadFieldClass('radio')'' will yield a Fatal error: Class 'JFormFieldRadio' not found.</translate>
  
There are 2 ways of overriding the getLabel() method:
+
<translate><!--T:59-->
# Wrap it in your own tags
+
There are two reasons for this.</translate>
# Modify it completely
 
  
=== Wrapping the label ===
+
<translate><!--T:60-->
 +
# JLoader cannot autoload ''JFormFieldRadio'' because the class name (''JFormField*'') does not match the path name (''joomla/form/fields/*'' - notice the plural on fields).</translate>
 +
<translate><!--T:61-->
 +
# JFormHelper cannot load ''JFormFieldRadio'' because custom paths are scanned first and the requested field type('radio') gets resolved before the core classes are reached.</translate>
  
To wrap a form label you can add the following function to your myform.php file:
+
<translate><!--T:62-->
<source lang="php">public function getLabel() {
+
'''Solution'''</translate>
 +
 
 +
<translate><!--T:63-->
 +
Require the core field file directly:</translate>
 +
 
 +
<syntaxhighlight lang="php">
 +
<?php
 +
require_once JPATH_LIBRARIES . '/joomla/form/fields/radio.php';
 +
 
 +
class TestFormFieldRadio extends JFormFieldRadio {}
 +
</syntaxhighlight>
 +
 
 +
<translate><!--T:64-->
 +
and use ''JFormHelper::loadFieldClass'' properly with 'test.radio' instead of 'radio'.</translate>
 +
 
 +
<translate>
 +
=== Using the Custom Field Type === <!--T:26-->
 +
</translate>
 +
<translate>
 +
==== Linked with a Form ==== <!--T:27-->
 +
</translate>
 +
<translate><!--T:28-->
 +
To use the field type City, we need to update the XML file that contains the form fields. Open your XML file located in ''administrator/components/com_phonebook/models/forms'' and add the field in the usual way:</translate>
 +
 
 +
<syntaxhighlight lang="xml"><field name="title" type="City" label="JGLOBAL_TITLE"
 +
description="JFIELD_TITLE_DESC"
 +
required="true" /></syntaxhighlight>
 +
 
 +
<translate><!--T:29-->
 +
The attribute name is cAsE-sEnSiTiVe.</translate>
 +
 
 +
<translate><!--T:30-->
 +
In addition, you may need to add the field path to the parent <fieldset>:</translate>
 +
 
 +
<syntaxhighlight lang="xml">
 +
<fieldset addfieldpath="/administrator/components/<component name>/models/fields">
 +
</syntaxhighlight>
 +
 
 +
<translate>
 +
==== Not Linked with a Form ==== <!--T:31-->
 +
</translate>
 +
<translate><!--T:32-->
 +
For example, when you need the field as a dropdown in a component as admin/site filter.</translate>
 +
 
 +
<syntaxhighlight lang="php">
 +
//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!!
 +
</syntaxhighlight>
 +
 
 +
<translate>
 +
== Overriding ''getLabel()'' == <!--T:33-->
 +
</translate>
 +
 
 +
<translate><!--T:34-->
 +
As mentioned in the section [[#Form field type class requirements|Form field type class requirements]], custom form field types usually do not define their own ''getLabel()''. If you do want to create a custom label, you can still make use of the ''getLabel()'' that every field type class inherits from JFormField by defining it as follows:</translate>
 +
 
 +
<syntaxhighlight lang="php">public function getLabel() {
 
     return '<span style="text-decoration: underline;">' . parent::getLabel() . '</span>';
 
     return '<span style="text-decoration: underline;">' . parent::getLabel() . '</span>';
} </source>
+
} </syntaxhighlight>
This code will underline your form labels.
 
  
=== Writing your own label ===
+
<translate><!--T:35-->
 +
This code will underline your form labels. (Please note that if your goal is to underline form labels, using [[CSS]] is the preferred way.)
  
To write your own label you can add the following function to your myform.php file:
+
<!--T:36-->
<source lang="php">public function getLabel() {
+
If you want to do something completely different, you can of course also override it completely:</translate>
 +
 
 +
<syntaxhighlight lang="php">public function getLabel() {
 
// Initialize variables.
 
// Initialize variables.
 
$label = '';
 
$label = '';
Line 118: Line 346:
 
$class = !empty($this->description) ? 'hasTip' : '';
 
$class = !empty($this->description) ? 'hasTip' : '';
 
$class = $this->required == true ? $class.' required' : $class;
 
$class = $this->required == true ? $class.' required' : $class;
+
 
 
// Add replace checkbox
 
// Add replace checkbox
 
$replace = '<input type="checkbox" name="update['.$this->name.']" value="1" />';
 
$replace = '<input type="checkbox" name="update['.$this->name.']" value="1" />';
+
 
 
// Add the opening label tag and main attributes attributes.
 
// Add the opening label tag and main attributes attributes.
 
$label .= '<label id="'.$this->id.'-lbl" for="'.$this->id.'" class="'.$class.'"';
 
$label .= '<label id="'.$this->id.'-lbl" for="'.$this->id.'" class="'.$class.'"';
Line 133: Line 361:
 
// Add the label text and closing tag.
 
// Add the label text and closing tag.
 
$label .= '>'.$replace.JText::_($text).'</label>';
 
$label .= '>'.$replace.JText::_($text).'</label>';
 
return $label;
 
}</source>
 
  
This example will add a checkbox before the label.
+
return $label;
 +
}</syntaxhighlight>
 +
 
 +
<translate><!--T:37-->
 +
This example will add a checkbox inside the label.</translate>
 +
 
 +
== 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
 +
# 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.
 +
# 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#MVC_and_other_considerations|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'' The manifest file for the component.
 +
<syntaxhighlight lang="xml">
 +
<?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>
 +
</syntaxhighlight>
 +
 
 +
''custom_fields.php'' The main code file which is run when an HTTP GET or POST is directed towards this component.
 +
<syntaxhighlight lang="php">
 +
<?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>
 +
</syntaxhighlight>
 +
 
 +
''form_definition.xml'' File containing the XML for the form definition, basically the two custom fields.
 +
<syntaxhighlight lang="xml">
 +
<?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>
 +
</syntaxhighlight>
 +
 
 +
''City.php'' PHP code for the City custom field.
 +
<syntaxhighlight lang="php">
 +
<?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;
 +
}
 +
}
 +
</syntaxhighlight>
 +
 
 +
''Time.php'' PHP code for the time custom field.
 +
<syntaxhighlight lang="php">
 +
<?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>';
 +
}
 +
}
 +
</syntaxhighlight>
  
<noinclude>[[Category:Development]][[Category:Joomla! 1.6]]</noinclude>
+
<noinclude>
 +
<translate>
 +
<!--T:52-->
 +
[[Category:Development]]
 +
[[Category:Extension development]]
 +
[[Category:Form fields]]
 +
[[Category:Joomla! 2.5]]
 +
[[Category:Joomla! 3.x]]
 +
</translate>
 +
</noinclude>

Latest revision as of 10:03, 9 October 2022

Other languages:
Deutsch • ‎English • ‎Nederlands • ‎español • ‎français
Joomla! 
3.x
series
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, a feature introduced in Joomla! 2.5, lets you easily create HTML forms (<form>). Forms created using JForm consist of form fields, implemented as JFormField. There is a JFormField for each different field type you can find in a form, such as a text field type and a date field type. JForm supports a large selection of standard field types. For a full list, see Standard form field types.

Joomla! makes it possible to extend standard field types or define your own. For example, if your component manages phone book entries, you might want to define a form field type that outputs a select list of cities. There are several advantages to defining a custom form field type:

  • You will be able to mix standard field types with your custom field type in a JForm-based form.
  • You will eventually have a reusable code package that can be used easily throughout your code.
  • Extensions that collaborate with your extension will be able to create form fields without meddling with your database tables and other internals.

Form Field Type Class Requirements[edit]

A form field type is defined in a class that must be a (not necessarily direct) subclass of JFormField. To work correctly, the class must define at least three methods:

  • public function getLabel()
    This function will be called to create the label that belongs to your field and must return an HTML string containing it. Since JFormField defines a ready-to-use getLabel() implementation, custom form field types usually do not define their own getLabel(). If you leave it out, the inherited method of creating labels will be used. It is recommended to leave out the getLabel() method for consistency and speed unless you actually want to modify the label's HTML.
  • public function getInput()
    This function will be called to create the field itself and must return an HTML string containing it. This is also where most of the processing usually happens. In our phone book City field example, this function will have to retrieve a list of available cities and return an HTML <select> with the cities inserted as <option>s.
  • public function getValue()
    This function will be called to get the field value. The value is obtained from the function LoadFormData in the Model

Inside your code, you will have to process the attributes set by the field's user in the XML form definition. Some of those attributes are accessible via protected member variables of JFormField. For example, the name attribute is available in your code as $this->name. Similarly, label, description, default, multiple and class are also available as properties of $this. Other parameters you might have defined can be accessed through the $this->element array: the attribute size will be in $this->element['size'].

Which Class to Subclass?[edit]

For a form field type to be usable in JForm, it needs to be a subclass of JFormField. However, it does not have to be a direct child of that class. You can also subclass an existing (standard or custom) form field type and thereby inherit useful code.

If your form field type is similar to an existing type, you should subclass that type. Especially if your form field type is a list, please subclass JFormFieldList. You only have to override getOptions() method to return the options to be shown. The getInput() method will convert those options to HTML.

To subclass an existing type, for example JFormFieldList, load it by adding the following after jimport('joomla.form.formfield');:

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

If your form field type is unlike any existing type, subclass JFormField directly.

Location of Files[edit]

  • The standard form field types are located in libraries/joomla/form/fields/. You should not store custom fields there, nor should you have to use this path in your own code. The standard types are usually good examples.
  • The custom field types that belong to your component are usually located in administrator/components/<name of your component>/models/fields. You can specify this or another path in your code:
JForm::addFieldPath(JPATH_COMPONENT . '/models/fields');
  • The XML files that define forms are usually located in administrator/components/<name of your component>/models/forms. Use something like the following snippet to specify a path to your forms:
JForm::addFormPath(JPATH_COMPONENT . '/models/forms');

Naming Conventions and Skeleton[edit]

In this section, <ComponentName> represents the camel-cased name of your component and <FieldName> represents the camel-cased name of your form field type. The field's class should be placed in administrator/components/<name of your component>/models/fields/<name of your field>.php, and look like this:

<?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
	}
}

Grouping Custom Field Types[edit]

Warning: this information is partially incorrect and needs to be improved.

Custom field types can be grouped by using an underscore in the field name. A field class with a name for example like "JFormFieldMy_randomField" must be stored in administrator/components/<name of your component>/models/fields/my/randomField.php. We can prefix our form field names with some group name, then we put an underscore and then a name of a field.

An Example Custom Field Type[edit]

Suppose you're working on your component named com_phonebook and you want to define a field that contains cities. Create the file administrator/components/com_phonebook/models/fields/city.php and write something similar to the following:

<?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>';
	}
}

A more advanced approach is extending the JFormFieldList class. Suppose you want to create a drop-down of cities dynamically from database based on a dynamic condition. You can do this in the following way:

<?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;
	}
}

The above example shows a simple query which will show a list of cities from a table where city name belongs to its country respectively. You can create a drop-down based on more complex queries.

Setting the Values of a List Option and Using JSON or an API Instead of a Database Call[edit]

If you want to use an API call instead of a database call to build a custom list item, use the following code.

This field was created in a module. To get it to work it needs to be saved to mod_modulename/models/fields/stackexchangesites.php. The naming convention is important as it is used within our function name.

<?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;
    }
}

On the Frontend simply calling the parameter gets us the value.

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

Pitfalls[edit]

Loading a custom field can result in a Fatal Error if a core field exists with the same filename and the custom field extends the core field.

Consider a file testfields/radio.php containing

<?php
class TestFormFieldRadio extends JFormFieldRadio {}

Calling JFormHelper::loadFieldClass('radio') will yield a Fatal error: Class 'JFormFieldRadio' not found.

There are two reasons for this.

  1. JLoader cannot autoload JFormFieldRadio because the class name (JFormField*) does not match the path name (joomla/form/fields/* - notice the plural on fields).
  2. JFormHelper cannot load JFormFieldRadio because custom paths are scanned first and the requested field type('radio') gets resolved before the core classes are reached.

Solution

Require the core field file directly:

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

class TestFormFieldRadio extends JFormFieldRadio {}

and use JFormHelper::loadFieldClass properly with 'test.radio' instead of 'radio'.

Using the Custom Field Type[edit]

Linked with a Form[edit]

To use the field type City, we need to update the XML file that contains the form fields. Open your XML file located in administrator/components/com_phonebook/models/forms and add the field in the usual way:

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

The attribute name is cAsE-sEnSiTiVe.

In addition, you may need to add the field path to the parent <fieldset>:

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

Not Linked with a Form[edit]

For example, when you need the field as a dropdown in a component as admin/site filter.

//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!!

Overriding getLabel()[edit]

As mentioned in the section Form field type class requirements, custom form field types usually do not define their own getLabel(). If you do want to create a custom label, you can still make use of the getLabel() that every field type class inherits from JFormField by defining it as follows:

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

This code will underline your form labels. (Please note that if your goal is to underline form labels, using CSS is the preferred way.)

If you want to do something completely different, you can of course also override it completely:

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;
}

This example will add a checkbox inside the label.

Sample Component Code[edit]

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 The 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>';
	}
}