Single form validation set

From Joomla! Documentation

Other languages:
English • ‎español • ‎français • ‎Nederlands
Joomla! 
≥ 3.0
Tutorial
Single form validation set

This page explains how to use the same form validation rules for both client-side and server-side validation. By default Joomla! uses PHP form validation rules for the server-side validation and JavaScript rules for the client-side validation. This means we need to duplicate all the rules we want to check in both PHP and JavaScript. Time to do things different.

One more advantage of using one set of validation rules, you can use the same text for all error messages.

The backend side does not need any special code as it always uses the PHP validation rules, so this works by default.

We are going to create/modify a few files:

  • enquiry.xml (form definition)
  • validate.js (transport)
  • validate.json.php (controller)
  • validate.php (model)
  • edit.php (form)
  • en-GB.com_helloworld.ini (language)

The process

The way this works is by following this process:

  1. User loads a page with an edit form
  2. After filling out the form, the user clicks on the submit button
  3. The function Joomla.submitbutton = function(task) catches the submit action
  4. This function will call the validateForm() function in the validate.js file
  5. The validateForm() will get the needed data from the form and pass it on to the sendForm() function
  6. Now we are ready for sending, the sendForm() will send the data to the server to have the data validated
  7. In case the answer from the server contains a problem with the validation, the message will be shown to the user and the form is not submitted
  8. In case the answer from the server contains no problems, the form will be submitted.

The files

enquiry.xml

Let's start with the XML file, enquiry.xml, for the form, this will have 5 fields:

<?xml version="1.0" encoding="utf-8"?>
<form>
	<fieldset>
		<field name="enquiry_id"
		       type="hidden" />

		<field name="title"
		       type="text"
		       label="COM_HELLOWORLD_ENQUIRY_TITLE_LABEL"
		       maxlength="50"
		       required="true" />

		<field name="publish_up"
		       type="text"
		       label="COM_HELLOWORLD_ENQUIRY_PUBLISH_UP_LABEL"
		       hint="dd-mm-jjjj"
		       required="true"
		       validate="hello.date"
		       message="COM_HELLOWORLD_FORM_NO_VALID_DATE_MESSAGE"/>

		<field name="publish_down"
		       type="text"
		       label="COM_HELLOWORLD_ENQUIRY_PUBLISH_DOWN_LABEL"
		       hint="dd-mm-jjjj"
		       field="publish_up"
		       validate="hello.dategreater"
		       required="true"
		       message="COM_HELLOWORLD_FORM_NO_VALID_GREATERDATE_MESSAGE"/>

		<field name="phone"
		       type="tel"
		       label="COM_HELLOWORLD_PHONE"
		       validate="tel"
		       required="false"
		       message="COM_HELLOWORLD_FORM_NO_VALID_PHONE_MESSAGE"/>
	</fieldset>
</form>

Here we have 5 fields: enquiry_id, title, publish_up, publish_down and phone.

  • title: This field has no validation rule.
  • publish_up: This field has a validation rule of hello.date.
  • publish_down: This field has a validation rule of hello.greaterdate.
  • phone: This field has a validation rule of phone.

validate.js

This file contains the code that sends the validation request to the server.

function validateForm(form, xml, group, token) {
	// Load all the form fields
	var filter = '[name^=jform';

	if (group) {
		filter += '\\[' + group + '\\]';
	}

	filter += ']';

	var fields = jQuery('#' + form + ' ' + filter);

	// Collect the fields and their values
	var data = [];

	fields.each(function (key, field) {
		data.push(field.name + '=' + encodeURIComponent(field.value));
	});

	// Send the form with the field info
	return sendForm(data, xml, group, token);
}

/**
 * Validate the fields on the server
 *
 * @param   array   data   An array with the data to validate.
 * @param   string  xml    The name of the XML file to validate against.
 * @param   string  group  The name of the group the fields belong to.
 * @param   string  token  The security token.
 */
function sendForm(data, xml, group, token) {
// Send the data to the server for validation
	return jQuery.ajax({
		async: true,
		url: 'index.php',
		dataType: 'json',
		cache: false,
		method: 'post',
		data: 'option=com_helloworld&task=validate.validateform&xml=' + xml + '&group=' + group + "&" + data.join('&') + '&format=json&' + token + '=1',
		success: function (data) {
			if (data) {
				var msg = '';

				jQuery(data).each(function (index, item) {
					if (item.message.length > 0) {
						var msgtype = item.type;

						msg =
						{
							msgtype: [item.message]
						};
					}
				});

				// Render the message
				if (jQuery(msg).size() > 0) {
					Joomla.renderMessages(msg);
				}
			}
		},
		error: function (request, status, error) {
			var msg = {
				error: [request.status + ' ' + request.statusText + '<br />' + request.responseText]
			};

			// Render the message
			Joomla.renderMessages(msg);
		}
	});
}

validate.json.php

This is the controller that receives the AJAX request and passes the data to the model to be validated.

defined('_JEXEC') or die;

/**
 * Validate controller.
 *
 * @package     Helloworld
 * @since       1.0
 */
class HelloworldControllerValidate extends JControllerForm
{
	/**
	 * Add a new lead.
	 *
	 * @return  string  JSON encoded string.
	 *
	 * @since   1.0
	 */
	public function validateForm()
	{
		// Check for request forgeries.
		JSession::checkToken() or jexit(JText::_('JINVALID_TOKEN'));

		$model = $this->getModel();
		$data = $this->input->post->get('jform', array(), 'array');
		$xml = $this->input->post->getCmd('xml', '');
		$group = $this->input->post->getCmd('group', '');
		$return = array(
			'message' => '',
			'type' => '',
			'continue' => true
		);

		// Validate the posted data.
		// Sometimes the form needs some posted data, such as for plugins and modules.
		$form = new JForm($xml);
		$form->addFormPath(JPATH_SITE . '/components/com_helloworld/models/forms');
		$form->loadFile($xml);

		if (!$form)
		{
			$return['message'] = $form->getErrors();
			$return['type'] = 'error';
			$return['continue'] = false;
		}
		else
		{
			// Test whether the data is valid.
			$validData = $model->validate($form, $data, $group);

			// Check for validation errors.
			if ($validData === false)
			{
				// Get the validation messages.
				$errors = $model->getErrors();

				// Push the validation messages out to the user.
				for ($i = 0, $n = count($errors); $i < $n && $i < 1; $i++)
				{
					if ($errors[$i] instanceof Exception)
					{
						$return['message'] = $errors[$i]->getMessage();
						$return['type'] = 'warning';
						$return['continue'] = false;
					}
					else
					{
						$return['message'] = $errors[$i];
						$return['type'] = 'warning';
						$return['continue'] = false;
					}
				}
			}
		}

		echo json_encode($return);

		jexit();
	}
}

@todo: Use JResponseJson to return the results.

validate.php

The model is simply there to proxy the request to Joomla.

defined('_JEXEC') or die;

/**
 * Validate form model.
 *
 * @package  Helloworld
 * @since    1.0
 */
class HelloworldModelValidate extends JModelForm
{
	/**
	 * Method to get the contact form.
	 * The base form is loaded from XML and then an event is fired
	 *
	 * @param   array    $data      An optional array of data for the form to interrogate.
	 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
	 *
	 * @return  JForm  A JForm object on success, false on failure
	 *
	 * @since   1.0
	 */
	public function getForm($data = array(), $loadData = true)
	{
		return false;
	}
}

edit.php

The form that shows the actual form. In our template file we will have the following form:

<form action="<?php echo JRoute::_('index.php?option=com_helloworld&view=enquiry&layout=edit&id=' . $this->item->enquiry_id, false); ?>" method="post" name="item-form" id="item-form">
	<div>
		<?php echo $this->form->getLabel('title'); ?>
		<div class="controls">
			<?php echo $this->form->getInput('title'); ?>
		</div>
	</div>

	<div>
		<?php echo $this->form->getLabel('publish_up'); ?>
		<div class="controls">
			<?php echo $this->form->getInput('publish_up'); ?>
		</div>
	</div>

	<div>
		<?php echo $this->form->getLabel('publish_down'); ?>
		<div class="controls">
			<?php echo $this->form->getInput('publish_down'); ?>
		</div>
	</div>

	<input type="hidden" name="task" value="enquiry.submit">
	<input type="hidden" name="enquiry_id" value="<?php echo $this->item->enquiry_id; ?>">
	<?php echo JHtml::_('form.token'); ?>
</form>
<script type="text/javascript">
	Joomla.submitbutton = function(task)
	{
		if (task == 'enquiry.cancel')
		{
			Joomla.submitform(task, document.getElementById('item-form'));
		}
		else
		{
			jQuery.when(validateForm('item-form', 'enquiry', '', '<?php echo JSession::getFormToken(); ?>')).done(function(result)
			{
				if (result.continue)
				{
					Joomla.submitform(task, document.getElementById('item-form'));
				}
			});
		}
	}
</script>

The validation rules

All custom validation rules are stored in the administrator/components/com_helloworld/models/rules folder. So we have one place where the custom rules live. The default rules, Joomla! knows where to find them, the custom rules Joomla! does not know so the following line of code should be added to your component entry file:

JForm::addRulePath('administrator/components/com_helloworld/models/rules');

hello.date

defined('_JEXEC') or die;

/**
 * Form Rule class that checks for a valid date value.
 *
 * @package     Helloworld
 * @since       1.0
 */
class HelloFormRuleDate extends JFormRule
{
	/**
	 * Method to test the field is not empty.
	 *
	 * @param   SimpleXMLElement  $element  The SimpleXMLElement object representing the <field /> tag for the form field object.
	 * @param   mixed             $value    The form field value to validate.
	 * @param   string            $group    The field name group control value. This acts as as an array container for the field.
	 *                                      For example if the field has name="foo" and the group value is set to "bar" then the
	 *                                      full field name would end up being "bar[foo]".
	 * @param   JRegistry         $input    An optional JRegistry object with the entire data set to validate against the entire form.
	 * @param   JForm             $form     The form object for which the field is being tested.
	 *
	 * @return  boolean  True if the value is valid, false otherwise.
	 *
	 * @since   11.1
	 */
	public function test(SimpleXMLElement $element, $value, $group = null, JRegistry $input = null, JForm $form = null)
	{
		try
		{
			JDate::getInstance($value);
		}
		catch (Exception $e)
		{
			return false;
		}

		return true;
	}
}

hello.greaterdate

defined('_JEXEC') or die;

/**
 * Form Rule class that checks for a date is later than another date.
 *
 * @package     Helloworld
 * @since       1.0
 */
class HelloFormRuleDategreater extends JFormRule
{
	/**
	 * Method to test the field is not empty.
	 *
	 * @param   SimpleXMLElement  $element  The SimpleXMLElement object representing the <field /> tag for the form field object.
	 * @param   mixed             $value    The form field value to validate.
	 * @param   string            $group    The field name group control value. This acts as as an array container for the field.
	 *                                      For example if the field has name="foo" and the group value is set to "bar" then the
	 *                                      full field name would end up being "bar[foo]".
	 * @param   JRegistry         $input    An optional JRegistry object with the entire data set to validate against the entire form.
	 * @param   JForm             $form     The form object for which the field is being tested.
	 *
	 * @return  boolean  True if the value is valid, false otherwise.
	 *
	 * @since   11.1
	 */
	public function test(SimpleXMLElement $element, $value, $group = null, JRegistry $input = null, JForm $form = null)
	{
		try
		{
			// Check for a valid date
			$date1 = JDate::getInstance($value);

			// Check if the second date is after the first date
			$field = (string) $element['field'];
			$fvalue = $input->get($group . '.' . $field, false);

			if ($value)
			{
				try
				{
					$date2 = JDate::getInstance($fvalue);

					if ($date1 < $date2)
					{
						return false;
					}
				}
				catch (Exception $e)
				{
					return false;
				}
			}
		}
		catch (Exception $e)
		{
			return false;
		}

		return true;
	}
}

Language file

In the previous code snippets a number of language strings are used, do not forget them to add them to the appropriate language files. You need to add the language strings to the frontend language file but also to the backend language file in case you want to use the same validation rules in the backend. The other option is to use only the frontend language file but load this language file in the backend as well.