Advanced form guide

From Joomla! Documentation

Revision as of 08:58, 19 December 2019 by Robbiej (talk | contribs) (fix syntax highlighting error)

Introduction[edit]

This is one of a series of API Guides, which aim to help you understand how to use the Joomla APIs through providing detailed explanations and sample code which you can easily install and run.

This guide describes more advanced features of the Joomla Form API than is covered in Basic form guide, and comprises the following aspects:

  • setting the file Path to enable Joomla to find your definitions of your form, your form fields and your form field validation, in the case where you don't follow the Joomla standards.
  • defining groupings of fields – Joomla provides two types, namely fieldsets and field groups.
  • dynamically changing your form (after it's been loaded from an XML file)
  • reflection-like methods which allow you to extract information from your form structure and data.

The guide concludes with a sample component with examples of the above aspects.

File Paths[edit]

By default Joomla will look in the …/models/forms folder of your component to find the XML definition of your form, within the site files if your form is being displayed on the front end, or within the administrator files if your form is being displayed on the back end. The static function addFormPath() allows you to add a different directory to the list of directories which Joomla will search.

Similarly addFieldPath() allows you to define a different directory for any custom form field definitions (the default being …/models/fields) and addRulePath() allows you to define a different directory for any custom validation rules (the default being …/models/rules).

There are examples of all three in the sample component code at the end of this guide.

Fieldsets[edit]

Fieldsets are associated with the <fieldset name="myfieldset"> element in the XML definition of the form. The advantage of using fieldsets is that in your layout file you can use

$form->renderFieldset("myfieldset");

to render all the fields which have <field> elements inside the <fieldset> opening and closing tags. This is instead of having to call renderField() for each field within the fieldset.

A fieldset can be viewed as a set of fields which should be displayed together in a form, and are thus similar in concept to the HTML <fieldset> element. However, note that renderFieldset() does not output the HTML <fieldset> or related tags.

Field Groups[edit]

Field groups are associated with the <fields name="mygroup"> element in the XML definition of the form. This affects the HTML name attribute which is assigned to HTML input elements of fields which are defined within the <fields> opening and closing tags in the XML form definition, and hence the name of the parameter as sent to the server in the HTTP POST request.

If you specify the option "control" => "myform" when you set up your Form instance then input field values will be sent to the server in the HTTP POST request keyed like

myform[field1]

myform[field2]

myform[field3]

If you enclose these fields in the XML form definition within a <fields name="mygroup"> element then the POST parameters will be sent with names like

myform[mygroup][field1]

myform[mygroup][field2]

myform[mygroup][field3]

If your component has a database table and you store a number of parameters in a json string in one of the columns of the table, then you can group the HTML input elements of those parameters inside a <fields> element. If you name the fields tag to match your column name then you can use the Joomla Table functionality to easily convert the PHP associative array arising from the POST parameters into the json string for storing in the database.

The $group parameter which appears in several Form API methods refers to the name attribute of the <fields> tag in the form definition XML.

Examples of this approach can be seen in the Joomla MVC tutorial Adding an Image step and many of the Joomla core components.

Note that fieldsets and field groups are independent. In your form XML definition you can have <fields> elements within <fieldset> elements, and also <fieldset> elements within <fields> elements.

Dynamically Changing Forms[edit]

If you have defined your form statically in an XML file, then once it's been loaded you can modify it dyamically in your PHP code using the Form APIs

  • adding further fields to your form by loading another form XML definition
  • modifying an existing field or fields,
  • removing a field or group of fields.

Adding Fields via form definition[edit]

To include additional definitions from a file into your form do

$form->loadFile($filename);

passing the $filename of an XML file structured in the same way as your main form definition file.

Alternatively you can create a SimpleXMLElement ($xml say) in your code which contains the same XML and then call

$form->load($xml);

(The Joomla code for loadFile() just reads the data from the file into a SimpleXMLElement variable and then calls load()).

With both these functions you can pass additional parameters:

  • $replace (in the load() method) / $reset (in the loadFile() method) – both of these have the same effect, and relate to the case where a field in the XML being loaded has a name which is the same as one in the form already. If set to true then the new field replaces the old one. If set to false then the new field is ignored.
  • $xpath – if you want only part of the XML structure being loaded to be considered then you can specify an xpath to select the part or parts of the XML you want to include.

In addition to the example in the sample code below, you can find examples of loading additional XML files in the core Joomla administrator com_menu code, related to when an admin is setting up site menu options. The basic options for a site menuitem are specified in the item.xml file in administrator/com_menus/models/forms, but in the com_menu model item.php code this is supplemented with options defined in the XML file which is in the layout directory related to the site page which is going to be displayed.

An example of building and loading an XML structure is in the Joomla MVC development tutorial Adding Associations step, where the Associations fields are added dynamically this way, because the associations to be added depend upon what the language of the record is.

Dynamically Setting Fields[edit]

You can use setField() to add or replace a single field in the Form instance, and setFields() to add or replace several fields. To use these you create the XML relating to a field, then pass this to setField()

$xml = new SimpleXMLElement('<field name="newfield" … />');
setField($xml);

Similarly you can define an array of such XML elements and pass these to setFields(), which is the equivalent of calling setField() on each of the individual elements.

Specify the $group and $fieldset parameters to include the new field within a specific field group and fieldset.

If the $replace parameter is set to true then if an existing field with the same field group and name is found, it will be replaced.

If the $replace parameter is set to false and an existing field with the same field group and name is found, then the new field will be ignored.

Setting Field Attributes and Values[edit]

setFieldAttribute() allows you to set / amend an attribute associated with a field. Note that the attribute refers to the Joomla field attribute, rather than the HTML attribute of the input element. For example, to set the HTML placeholder attribute you have to set the Joomla hint field attribute, and this works only if the Form Field type supports that attribute.

The HTML value attribute is treated somewhat differently from other HTML attributes. As outlined in the Basic form guide, in the Joomla Form instance the XML form structure (defined by the form definition XML file) is held separately from the form pre-fill data (passed in the bind() method). What is output in the value attribute of the HTML input element is primarily the default Joomla form field attribute (if supported for that field type), but this is overridden by any value specified for that field in the bind() call.

You can thus set the default attribute using setFieldAttribute(), but to set the field value directly within the pre-fill data use setValue().

Removing Fields[edit]

You can remove fields from the Form definition by calling removeField() to remove a specific field or removeGroup() to remove all the fields within a specified field group.

Reflection Methods[edit]

There are a number of methods which allow you to access various aspects of the Form instance data. Mostly these are fairly straightforward to understand, and only cases where it may not be totally clear are explained below.

getData() returns as a Joomla Registry object the pre-fill data which has been set using the Form bind() call.

The methods getField(), getFieldset() and getGroup() all return fields as Joomla FormField objects, rather than how they're held internally within the Form instance.

getFieldsets() returns an array of Fieldset objects with properties which reflect the <fieldset> tag in the form definition file. So if you have

<fieldset name="myfieldset" label="myfieldsetLabel" description="myfieldsetDescription">

then you can do

$fieldsets = $form->getFieldsets();
echo $fieldsets['myfieldset']->label;   // outputs "myfieldsetLabel"

getFormControl() returns the string from your $options parameter passed when you created the Form instance. If you used the Joomla standard of "control" => "jform" within this $options array then getFormControl() will return the string "jform".

getInput() and getLabel() return the HTML for the <input> tag and <label> respectively of the field which has been passed as a parameter. However, note that neither of these work if you have a Custom Field. Also note that these Form methods are different from the getInput() and getLabel() FormField methods which you have to provide when you are setting up some types of Custom Fields.

getValue() returns the value of the field you pass as a parameter, reading this from the data passed in the bind() call. It doesn't take account of the default attribute set against the field, which will get converted into the HTML field value attribute if no pre-fill data for that field is provided.

Sample Component Code[edit]

Below are 6 files which constitute a small component which you can install and run to demonstrate a number of the features described above, and which you can adapt to experiment with other features.

Create the first 3 files in a folder "com_sample_form3", and the second 3 files in a subdirectory of this "com_sample_form3/extra". Then zip up the "com_sample_form3" folder to create com_sample_form3.zip and install this as a component on your Joomla instance. Once installed navigate on your browser to your Joomla site and add the URL parameter &option=com_sample_form3, which should display the form and allow you to enter data and submit the form. The comments in the code should make it clear what's going on.

As described in the associated Basic form guide, this isn't the best way to design a Joomla MVC component, but it's written this way to make the use of the Form APIs as clear as possible.

com_sample_form3.xml Manifest file for the component

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

	<name>com_sample_form3</name>
	<version>1.0.0</version>
	<description>Sample form 3</description>
	
	<administration>
	</administration>

	<files folder="site">
		<filename>sample_form3.php</filename>
		<filename>sample_form.xml</filename>
		<folder>extra</folder>
	</files>
</extension>

sample_form.xml File containing the XML for the form definition. The 3 fields are enclosed within a fieldset "mainFieldset", and the email and telephone number fields are within a field group "details". The message field has a custom validation "noasterisk".

<?xml version="1.0" encoding="utf-8"?>
<form name="myFormName">
	<fieldset name="mainFieldset" label="mainFieldsetLabel" description="mainFieldsetDescription">
		<field
			name="message"
			type="text"
			label="Message"
			size="40"
			validate="noasterisk"
			class="inputbox"
			required="true" />
		<fields name="details">
			<fieldset name="detailsFieldset">
				<field name="email" 
					type="email"
					label="Email"
					required="true"
					size="40"
					class="inputbox" />
			</fieldset>
			<field name="telephone" 
				type="tel"
				label="Telephone number"
				required="true"
				size="40"
				class="inputbox"
				validate="tel" />
		</fields>
	</fieldset>
</form>

sample_form3.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;
use Joomla\CMS\Factory;

// base form definition
$form = Form::getInstance("sample", __DIR__ . "/sample_form.xml", array("control" => "myform"));

// because email and telephone are within the "details" group, prefill data needs to reflect that
$prefillData = array("details" => array("email" => ".@.", "telephone" => "0"), "time" => "12:34");

// add form and field paths (rule path added later, before validation in HTTP POST handling)
Form::addFieldPath(__DIR__ . "/extra");
Form::addFormPath(__DIR__ . "/extra");

// using xpath parameter include just the "time" field from extra_form.xml, not the "ignored" field
$extraForm = $form->loadFile("extra_form", true, "//field[@name='time']");

// add a new element to the form, into "details" group and "mainFieldset" fieldset
$xml = new SimpleXMLElement('<field name="upload" type="file" label="Photo" accept="image/*" />');
$form->setField($xml, 'details', true, "mainFieldset");

// Change label on message field to say "Description" and set hint (html placeholder) to be "No asterisks!"
$form->setFieldAttribute("message", "label", "Description");
$form->setFieldAttribute("message", "hint", "No asterisks!");

// Remove telephone field - uncomment line below to activate
// $form->removeField("telephone", "details");

// Some reflection methods
echo "<h2>Reflection methods output</h2>";

$formName = $form->getAttribute("name");
echo "Form name is: $formName<br>";

$formControl = $form->getFormControl();
echo "Form control is: $formControl<br>";

$detailsGroup = $form->getGroup('details');
echo "Fields in details group: <br>";
foreach ($detailsGroup as $key => $value)
{
	echo "Key is: $key, PHP class of field is " . get_class($value) . ", and name attribute is " . $value->getAttribute("name") . "<br>";
}

$fieldsets = $form->getFieldsets();
echo "Fieldset label for mainFieldset is " . $fieldsets['mainFieldset']->label . "<br>";

if ($_SERVER['REQUEST_METHOD'] === 'POST') 
{
	$app   = Factory::getApplication();
	$data = $app->input->post->get('myform', array(), "array");
	echo "<h2>POST data</h2>";
	echo "Message was " . $data["message"] . 
		", email was " . $data["details"]["email"] . 
		", and time was " . $data["time"] . "<br>";
	// add rule path for validation 
	Form::addRulePath(__DIR__ . "/extra");
	$filteredData = $form->filter($data);
	$result = $form->validate($filteredData);
	if ($result)
	{
		echo "Validation passed ok<br>";
	}
	else
	{
		echo "Validation failed<br>";
		$errors = $form->getErrors();
		foreach ($errors as $error)
		{
			echo $error->getMessage() . "<br>";
		}
		// in the redisplayed form show what the user entered (after data is filtered)
		$prefillData = $filteredData;
	}
}

$form->bind($prefillData);
$data = $form->getData();
echo "<br>Form data is: $data<br>";

?>
<form action="<?php echo JRoute::_('index.php?option=com_sample_form3'); ?>"
    method="post" name="sampleForm" id="adminForm" enctype="multipart/form-data">

	<h2>mainFieldset fieldset</h2>
	<?php echo $form->renderFieldset('mainFieldset');  ?>
	
	<h2>time field</h2>
	<?php echo $form->renderField('time');  ?>
	
	<h2>ignored field - should be blank</h2>
	<?php echo $form->renderField('ignored');  ?>
	
	<button type="submit">Submit</button>
</form>

extra/extra_form.xml The form definition for the additional form fields to be added to the form. The component code includes just the "time" field from this form definition, and this is a custom field of type "mytime".

<?xml version="1.0" encoding="utf-8"?>
<form>
	<field
		name="ignored"
		type="number"
		label="Number"
		step="0.1"
		class="inputbox"
		required="true" />
	<field name="time" 
		type="mytime"
		label="Enter time"
		required="true"
		class="inputbox" />
</form>

extra/Mytime.php This provides the <input> HTML element for the "mytime" custom field. As there is no getLabel() method here, the label will be taken from the form field definition XML.

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

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

class JFormFieldMytime extends FormField
{
	protected $type = 'Mytime';

	protected function getInput()
	{
		// Set attributes - here just the CSS class for the input element, if specified
		$attr = !empty($this->class) ? ' class="' . $this->class . '"' : '';

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

		return $html;
	}
}

extra/noasterisk.php This provides the validation rule for the "noasterisk" custom validation for the message field.

<?php
defined('_JEXEC') or die('Restricted access');
 
class JFormRuleNoasterisk extends JFormRule
{
	// regex to allow anything except an asterisk
	protected $regex = '^[^\*]+$';
}