Basic form guide

From Joomla! Documentation

Introduction

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.

The Joomla Form class and API enables you to easily develop HTML forms and handle the user's input.

This page describes how you interact in general with the Joomla Form class and provides the code of a simple component which you can install to demonstrate use of this API. A companion guide Advanced Form Guide deals with more advanced aspects of the Joomla Form class.

Joomla itself also has a specific way of designing form functionality, and this page includes tutorial material to help you understand and develop your component to align with these principles.

Overall Description

The diagram below shows the basic use of the Joomla Form class.

Using Joomla Forms

Firstly, you need to define your form in XML using Joomla Standard form field types. See the component code below for an example. In basic terms, each field element in your XML file maps to an HTML (mostly "input") element in the form, with the XML field attributes mapping to the (input) element attributes. Many of the possible field attributes are listed in Text form field type.

Step 1 Loading the Form

The user clicks on the URL of your form, and Joomla routes the HTTP GET request through to your component code. You need to call Form::getInstance() passing the name of your XML file with your form definition. The Joomla Form code creates a Form instance, and then (in Form::loadFile()) reads the file into memory (as a PHP SimpleXMLElement) and parses the XML to ensure it's valid. The main parameters passed to getInstance() are:

  • name – a string specifying the identity to give to this form – it just needs to be unique, so that it doesn't clash with any other Joomla forms on the same webpage
  • data – the name of the XML file which has your form definition
  • options – an array of options, with the "control" element specifying the name of the array which the POST parameters will be held in. For example, the sample code has array("control"=>"myform") which means that the HTML input elements will have name attributes set to "myform[message]", "myform[email]", etc,. They will be passed in POST parameters that way, and it's then very easy to get these into a PHP array.

Step 2 Providing Pre-fill Data

You provide values for any form element you wish. For example, if this form is being used to edit a record in the database, then you would pre-populate it with existing field values from the database. You can provide values by setting up an associative array $data and passing it to Form::bind($data) method, and Joomla Form then stores this data locally within the Form instance.

Step 3 Outputting the Form in HTML

You call renderField(fieldName) on the Form instance and you get returned the HTML which you can echo to the output. Joomla processes its XML representation of your form to obtain the section relating to the passed-in fieldName, generates the HTML for this HTML element and includes the "value" attribute based on the pre-populated data which you passed in step 2. When outputting the form you also need to surround the input elements in a <form> element and add a submit button.

Step 4 User Submitting the Form

The user enters data into your HTML form and clicks on the submit button. The browser generates an HTTP POST request to the URL specified in the <form> element and passes to the server in POST parameters the values entered by the user; each parameter keyed by the "name" attribute of the HTML input element.

Joomla routes this POST through to your component. As this is a new HTTP request the previous Form instance no longer exists, so you have to again call Form::getInstance(), passing the filename of your form XML, and Joomla (as before) creates a Form instance and reads this file into memory.

Step 5 Handling the HTTP POST Data

In this step you process the submitted data. This involves:

  1. Getting the POST data using Factory::getApplication->input->get(). Usually the "name" attributes of the input elements are defined so that the POST parameters appear as an array, and you can use the ARRAY filter from Retrieving_request_data_using_JInput to read these directly into a PHP array. However, this means that the individual elements are not filtered at all.
  2. Filtering. This applies filtering on each of the input values. The filter applied to a field is governed by the "filter=..." attribute on that field in your form XML file, or if not present then the default filter will remove HTML tags etc from your data values. The possible filters are in the filterField() method of the Form class (and see https://joomla.stackexchange.com/questions/5764/what-are-possible-filters-in-joomla-form-fields). Note that these form field filters are different from the jinput filters.
  3. Validation. You validate the data which the user has entered by calling validate($data) on your Form instance, passing an associative array of the (filtered) user data. Joomla compares the user-entered data with the validation you defined in your form XML file, and generates errors for fields which fail the validation.

If there are validation errors then you should display those errors to the user, and redisplay the form, pre-populating the fields with the (filtered) data which the user entered previously.

If there are no errors then you can confirm this to the user, and show the next web page.

Sample Form 1

Below is the code for a small component which you can install to demonstrate basic use of Joomla forms. Place the following 3 files into a folder called "com_sample_form1". Then zip up the folder to create com_sample_form1.zip and install this as a component on your Joomla instance.

com_sample_form1.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_form1</name>
	<version>1.0.0</version>
	<description>Sample form 1</description>
	
	<administration>
	</administration>

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

sample_form.xml File containing the XML for the form definition

<?xml version="1.0" encoding="utf-8"?>
<form>
	<field
		name="message"
		type="text"
		label="Enter message"
		size="40"
		class="inputbox"
		required="true" />
	<field name="email" 
		type="email"
		label="Enter email"
		required="true"
		size="40"
		class="inputbox" />
	<field name="telephone" 
		type="tel"
		label="Enter telephone number"
		required="true"
		size="40"
		class="inputbox"
		validate="tel" />
</form>

sample_form1.php Component code.

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

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

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

if ($_SERVER['REQUEST_METHOD'] === 'POST') 
{
	$app   = JFactory::getApplication();
	$data = $app->input->post->get('myform', array(), "array");
	echo "Message was " . $data["message"] . 
		", email was " . $data["email"] . 
		", and telephone was " . $data["telephone"] . "<br>";
	$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);
?>
<form action="<?php echo JRoute::_('index.php?option=com_sample_form1'); ?>"
    method="post" name="sampleForm" id="adminForm" enctype="multipart/form-data">

	<?php echo $form->renderField('message');  ?>
	
	<?php echo $form->renderField('email');  ?>
	
	<?php echo $form->renderField('telephone');  ?>
	
	<button type="submit">Submit</button>
</form>

Once installed navigate to your site and add the following parameter to the URL: &option=com_sample_form1. You should then see the form displayed with 3 required fields:

  • a general text input field for a message
  • an email input field, pre-populated with the string ".@."
  • a telephone number input field.

and using your browser's development tools you can compare the html attributes with the attributes in your XML form definition.

Note that modern browsers will do some validation on the values you enter, specifically they will validate the email address and will force you to enter something into fields with the "required" attribute set, but don't (currently) do validation on telephone number fields.

Once you enter valid data into the fields and press Submit, then the data will be sent to the server and the POST leg of the sample code will be run. This runs the filtering and validation routines. If there are validation errors then the code outputs the error messages, and prefills the form with the data which the user entered before redisplaying it.

MVC and other considerations

The way that the code above is written isn't the best approach if you are developing a genuine Joomla component. Instead you should follow the way that the Joomla core code is designed, in particular splitting your component into controllers, models, views and layouts, and the revised component code in the section after this follows this approach. The remainder of this section describes these and other design decisions, which should make the sample code easier to understand.

Joomla MVC split

In general terms Joomla splits components into separate types of functionality:

  • the controller contains the "business logic" of the application, including deciding what view and model to use
  • the model provides access to the data
  • the view decides what data is necessary for outputting on the web page and obtains this data from the model
  • the layout outputs the HTML, and includes in the output the data which has been collated by the view. The layout runs within the context of the view, and so has direct access to the variables of the view code.

Post/Request/Get pattern

In Joomla all of the HTML output (such as the display of a form) is performed in response to an HTTP GET, following the https://en.wikipedia.org/wiki/Post/Redirect/Get pattern. The sample code above doesn't follow this pattern, but instead outputs the validation errors and re-displays the form in the response to the HTTP POST request.

To follow the Joomla pattern, in the code which handles the POST we should include a HTTP GET redirect to the form URL. As that GET will then be a new HTTP request/response we must store in the user session the data to be shown when the form is redisplayed:

  • the validation error messages are stored and output using the enqueueMessage() method (which stores data in the user session automatically for us)
  • the user-entered data is stored using setUserState() and retrieved using getUserState(), and keyed by a context which should be unique to this form. The code which provides the data for the form bind() operation must first check using getUserState() if there is any prefill data in the session. And if the user enters data which successfully passes validation then setUserState() should be called to clear this prefill data in the session, otherwise it will appear whenever the user next displays the form.

Separate Controllers

In response to a GET or POST, Joomla always runs the same component file, sample_form1.php in the sample component example above and the top-level sample_form2.php in the example below. The sample_form2.php code below follows the example of the core Joomla components, and has code which passes control to different methods in different controllers based on the value of the HTTP parameter task. This parameter is set by Joomla core javascript based on the submit button, eg in the example below:

onclick="Joomla.submitbutton('myform.submit')"

The task parameter is in this example set to "myform.submit". In general the task parameter is of the form "firstpart.secondpart" and for a component called "com_example" Joomla will try to run an instance method called secondpart() of a controller class ExampleControllerFirstpart in a file firstpart.php in the controllers directory.

If the task parameter is not set then Joomla will try to run the display() method of the ExampleController class which it will expect to find in controller.php.

In terms of code:

$controller = JControllerLegacy::getInstance('Sample_form2');

uses the task parameter to identify the appropriate controller file to include, and then creates an instance of that controller class.

$controller->execute($input->getCmd('task'));

executes the appropriate method (secondpart() - or in the case below, submit() - or display()) of that class.

In this way controller functionality handling GETs (generally in the file controller.php) is separated from that handling POSTs (generally in the files in the controllers folder).

Joomla MVC classes

Joomla provides feature-rich controller, view and model classes from which your component controllers, views and models can inherit. The model code below inherits from FormModel which shields somewhat the Joomla Form API. In this case our model calls FormModel::loadForm(), and this then executes a callback to our loadFormData() to provide the data to bind() to the form.

You can also use FormController and AdminController classes, but these assume that your component is using a database table to store the data. The Joomla MVC Component Development tutorial takes this approach in J3.x:Developing an MVC Component/Adding backend actions.

Security Token

Joomla uses a security token on forms to prevent CSRF attacks (see https://en.wikipedia.org/wiki/Cross-site_request_forgery). The token is output in the layout file

<?php echo JHtml::_('form.token'); ?>

and checked within the controller handling the POST:

$this->checkToken();

If the token is found to be invalid then checkToken() outputs a warning and redirects the user back to the previous page.

Client-side Validation

In addition to the server-side validation included through the form XML definition, Joomla also provides a way of including javascript which performs client-side validation on the browser. This is NOT included below, as it's deemed beyond the scope of this tutorial, but you can find details of it in J3.x:Developing an MVC Component/Adding verifications and Client-side form validation.

Sample Form 2

This second sample component incorporates the design decisions described above and structures the code according to the Joomla paradigm. The overall file structure is shown in the diagram below.

Sample Form 2 file structure.

Starting at the bottom of this diagram and going upwards the file contents are:

com_sample_form2/sample_form2.php Entry point for the component. This is the first file which Joomla runs.

<?php
defined('_JEXEC') or die('Restricted access');
// Load the appropriate controller class
$controller = JControllerLegacy::getInstance('Sample_form2');

$input = JFactory::getApplication()->input;
// Run the task method, or display() if no task parameter
$controller->execute($input->getCmd('task'));
 
$controller->redirect();

com_sample_form2/controller.php The controller for handling HTTP GET requests

<?php
defined('_JEXEC') or die('Restricted access');
use Joomla\CMS\MVC\Controller\BaseController;

class Sample_form2Controller extends BaseController
{
	// Joomla will look for this class within the controller.php file
	// It will (usually) call the display() method, and will find this is in the BaseController class
}

com_sample_form2/sample_form2.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_form2</name>
	<version>1.0.0</version>
	<description>Sample form 2</description>
	
	<administration>
	</administration>

	<files folder="site">
		<filename>sample_form2.php</filename>
		<filename>controller.php</filename>
		<folder>controllers</folder>
        <folder>views</folder>
        <folder>models</folder>
	</files>
</extension>

com_sample_form2/views/form/view.html.php View file for displaying the form. The controller display() function will create instances of the model and view, and call the view display() function below.

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

use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\Factory;

class Sample_form2ViewForm extends HtmlView
{
	public function display($tpl = null)
	{
		if (!$this->form = $this->get('form'))
		{
			echo "Can't load form<br>";
			return;
		}
		parent::display($tpl);	// this will include the layout file edit.php
	}
}

com_sample_form2/views/form/tmpl/edit.php Layout file for displaying the form

<?php
defined('_JEXEC') or die('Restricted access');
?>
<form action="<?php echo JRoute::_('index.php?option=com_sample_form2&view=form&layout=edit'); ?>"
    method="post" name="adminForm" id="adminForm" enctype="multipart/form-data">

	<?php echo $this->form->renderField('message');  ?>
	
	<?php echo $this->form->renderField('email');  ?>
	
	<?php echo $this->form->renderField('telephone');  ?>

	<button type="button" class="btn btn-primary" onclick="Joomla.submitbutton('myform.submit')">Submit</button>
	
	<input type="hidden" name="task" />
	<?php echo JHtml::_('form.token'); ?>
</form>

com_sample_form2/models/form.php Model associated with displaying the form. The view get('form') gets mapped to the model getForm() method call.

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

use Joomla\CMS\MVC\Model\FormModel;

class Sample_form2ModelForm extends FormModel
{

	public function getForm($data = array(), $loadData = true)
	{
		$form = $this->loadForm(
			'com_sample_form2.sample',  // just a unique name to identify the form
			'sample_form',				// the filename of the XML form definition
										// Joomla will look in the models/forms folder for this file
			array(
				'control' => 'jform',	// the name of the array for the POST parameters
				'load_data' => $loadData	// will be TRUE
			)
		);

		if (empty($form))
		{
            $errors = $this->getErrors();
			throw new Exception(implode("\n", $errors), 500);
		}

		return $form;
	}

    protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$data = JFactory::getApplication()->getUserState(
			'com_sample_form2.sample',	// a unique name to identify the data in the session
			array("telephone" => "0")	// prefill data if no data found in session
		);

		return $data;
	}
	
}

com_sample_form2/models/forms/sample_form.xml XML file containing the form definition

<?xml version="1.0" encoding="utf-8"?>
<form>
	<field
		name="message"
		type="text"
		label="Enter message"
		size="40"
		class="inputbox"
		required="true" />
	<field name="email" 
		type="email"
		label="Enter email"
		required="true"
		size="40"
		class="inputbox" />
	<field name="telephone" 
		type="tel"
		label="Enter telephone number"
		required="true"
		size="40"
		class="inputbox"
		validate="tel" />
</form>

com_sample_form2/controllers/Myform.php Controller which handles the HTTP POST

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

use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Factory;

class Sample_form2ControllerMyform extends BaseController
{   
	public function submit($key = null, $urlVar = null)
	{
		$this->checkToken();
		
		$app   = JFactory::getApplication();
		$model = $this->getModel('form');
		$form = $model->getForm($data, false);
		if (!$form)
		{
			$app->enqueueMessage($model->getError(), 'error');
			return false;
		}
		
		// name of array 'jform' must match 'control' => 'jform' line in the model code
		$data  = $this->input->post->get('jform', array(), 'array');
		
		// This is validate() from the FormModel class, not the Form class
		// FormModel::validate() calls both Form::filter() and Form::validate() methods
		$validData = $model->validate($form, $data);

		if ($validData === false)
		{
			$errors = $model->getErrors();

			foreach ($errors as $error)
			{
				if ($error instanceof \Exception)
				{
					$app->enqueueMessage($error->getMessage(), 'warning');
				}
				else
				{
					$app->enqueueMessage($error, 'warning');
				}
			}

			// Save the form data in the session, using a unique identifier
			$app->setUserState('com_sample_form2.sample', $data);
		}
		else
		{
			$app->enqueueMessage("Data successfully validated", 'notice');
			// Clear the form data in the session
			$app->setUserState('com_sample_form2.sample', null);
		}
		
		// Redirect back to the form in all cases
		$this->setRedirect(JRoute::_('index.php?option=com_sample_form2&view=form&layout=edit', false));
	}
}

After creating the files as described above, zip up the com_sample_form2 folder to create com_sample_form2.zip and install this component. Then navigate to your Joomla site and add into the URL the parameters &option=com_sample_form2&view=form&layout=edit. This should display the form, and should work in a similar fashion to previous sample form, except that errors etc will appear in the message part of the Joomla display.