J3.x

Developing an MVC Component/Adding a front-end form

From Joomla! Documentation

< J3.x:Developing an MVC Component
This page contains changes which are not marked for translation.
Other languages:
English • ‎español • ‎français • ‎italiano • ‎中文(台灣)‎
Joomla! 
3.x
Tutorial
Developing an MVC Component



This is a multiple-article series of tutorials on how to develop a Model-View-Controller Component for Joomla! VersionJoomla 3.x.

Begin with the Introduction, and navigate the articles in this series by using the navigation button at the bottom or the box to the right (the Articles in this series).



This tutorial is part of the Developing a MVC Component for Joomla! 3.x tutorial. You are encouraged to read the previous parts of the tutorial before reading this.

Introduction[edit]

In this step we add a form to the site Frontend to allow users to add a new helloworld record. You could adapt the code in this step to provide a range of Frontend data entry functionality, from e.g.

  • a registration form to allow visitors to the site to express interest, to
  • a facility to allow suppliers to maintain the data in certain tables, but without giving them access to the Backend Administrator functionality.

There are two videos associated with this step, covering the overall approach and other topics raised here.

  • Overall approach

  • Other topics

Functionality[edit]

In this step we will build the following functionality:

  • a Frontend form that allows someone to enter a new helloworld record, together with Save and Cancel buttons.
  • additional fields in the helloworld database table to capture the user who entered the record, and the creation date/time
  • validation of the data entered into the fields, including removing dangerous items such as HTML tags
  • access control on the form, controlling who can add a new record
  • an email to an Administrator, to indicate that someone has entered a new record (and we'll include a field on the form where the user can include some text that just goes into the email)
  • a CAPTCHA on the form
  • a Frontend menu item that points to the form (i.e. how users get access to the form)
  • update to the Administrator helloworlds view to include the author and creation date in the display.

Approach[edit]

Providing a form on the site Frontend is pretty much the same as on the Administrator Backend, so much of what is developed in this step reflects what was done in the step Adding Backend actions, and in particular the Edit form for helloworld messages. For this form we'll use (all in the site part)

  • the same main helloworld controller (controller.php)
  • a new view – that we'll call "form"
  • a new layout, associated with the form view, which we'll call "edit" (like the Administrator layout file)
  • a new model – that we'll call "form" also, to align with the view
  • an XML file for the form, which we'll put in add-form.xml in the model

In addition we'll need to handle the HTTP POST requests arising from the user pressing the Save and Cancel buttons, and we'll do this in:

  • a new helloworld controller in the controllers directory (handling Save and Cancel)
  • the same form.php model as above (handling Save)

Upon completion of Save/Cancel we'll redirect back to the same form. As for the Administrator case, we'll consider our classes extending some of the controller and model classes that are richer in functionality than the simple legacy ones.

  • the model will extend JModelAdmin (which supports forms, as well as saving records to the database) - even though it's called JModelAdmin it's still available within the site part
  • the controller handling the POST will extend JControllerForm (which includes the cancel() and save() methods).

Some of the other aspects in this step will involve using the techniques covered in earlier steps, namely Adding a menu type to the site part, Adding verifications, Adding configuration and Adding ACL.

Updating the Database[edit]

We need to add the two new fields to our install and update scripts. To record who added the new helloworld greeting we'll store the user's userid, and this will be zero if the user is not logged on.

admin/sql/install.mysql.utf8.sql

DROP TABLE IF EXISTS `#__helloworld`;

CREATE TABLE `#__helloworld` (
	`id`       INT(11)     NOT NULL AUTO_INCREMENT,
	`asset_id` INT(10)     NOT NULL DEFAULT '0',
	`created`  DATETIME    NOT NULL DEFAULT '0000-00-00 00:00:00',
	`created_by`  INT(10) UNSIGNED NOT NULL DEFAULT '0',
	`greeting` VARCHAR(25) NOT NULL,
	`published` tinyint(4) NOT NULL DEFAULT '1',
	`catid`	    int(11)    NOT NULL DEFAULT '0',
	`params`   VARCHAR(1024) NOT NULL DEFAULT '',
	PRIMARY KEY (`id`)
)
	ENGINE =MyISAM
	AUTO_INCREMENT =0
	DEFAULT CHARSET =utf8;

INSERT INTO `#__helloworld` (`greeting`) VALUES
('Hello World!'),
('Goodbye World!');

/admin/sql/updates/mysql/0.0.16.sql

ALTER TABLE`#__helloworld` ADD COLUMN `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00' AFTER `asset_id`;
ALTER TABLE`#__helloworld` ADD COLUMN `created_by` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `created`;

Adding a Menu Item[edit]

To enable a menu item to point to the form, we need to put an XML file into the associated layout directory. Extend your directory structure (remembering to include the index.html file in each new subdirectory that you create) and put the following into

site/views/form/tmpl/edit.xml

<?xml version="1.0" encoding="utf-8"?>
<metadata>
	<layout title="COM_HELLOWORLD_ADD_VIEW_TITLE">
		<message>COM_HELLOWORLD_ADD_VIEW_DESC</message>
	</layout>
</metadata>

Once you have the new code installed you'll have to go into the Administrator Backend and create a menu item to point to the new Frontend form.

Displaying the New Form View[edit]

Create a new forms subdirectory under the site models directory, and add the definition of the form into

site/models/forms/add-form.xml

<?xml version="1.0" encoding="utf-8"?>
<form
    addrulepath="/administrator/components/com_helloworld/models/rules"
    >
    <fieldset
				name="details"
				label="COM_HELLOWORLD_HELLOWORLD_DETAILS"
	>
		<field
				name="id"
				type="hidden"
				/>
		<field
				name="greeting"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_GREETING_DESC"
				size="40"
				class="inputbox"
				validate="greeting"
				required="true"
                hint="COM_HELLOWORLD_HELLOWORLD_GREETING_HINT"
				default=""
				/>
        <field
				name="catid"
				type="category"
				extension="com_helloworld"
				class="inputbox"
				default=""
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC"
				required="true"
                >
			<option value="0">JOPTION_SELECT_CATEGORY</option>
		</field>
        <field
                name="message"
                type="textarea"
                rows="5"
                cols="80"
				label="COM_HELLOWORLD_HELLOWORLD_MESSAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_MESSAGE_DESC"
                hint="COM_HELLOWORLD_HELLOWORLD_MESSAGE_HINT"
				required="true"
                >
        </field>
        <field
				name="captcha"
				type="captcha"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC"
                validate="captcha"
                >
		</field>
        <fields name="params">
            <field
                    name="show_category"
                    type="list"
                    label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
                    description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
                    default=""
                    useglobal="true"
            >
                <option value="0">JHIDE</option>
                <option value="1">JSHOW</option>
            </field>
        </fields>
    </fieldset>
</form>

The above form includes a CAPTCHA. To get this working you need to install a CAPTCHA plugin from the selection available for Joomla. You can also use the CAPTCHA - reCAPTCHA plugin which comes with Joomla, but before using this you have to get reCAPTCHA keys for your domain (e.g. localhost) from Google, and then configure the plugin with the keys which Google supplies (i.e. Extensions / Plugins, and then click on the plugin to edit its options - if you edit the plugin it tells you exactly what you have to do). Once the plugins are live, you can select in the Global Configuration the CAPTCHA which you want to be the system default, and also in the helloworld Configuration select which CAPTCHA to use for this helloworld form.

The existing controller (in controller.php) will not need to be changed, but we'll need to define a new view, layout and model for the new form.

site/views/form/view.html.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorld View
 * This is the site view presenting the user with the ability to add a new Helloworld record
 *
 */
class HelloWorldViewForm extends JViewLegacy
{

	protected $form = null;
	protected $canDo;

	/**
	 * Display the Hello World view
	 *
	 * @param   string  $tpl  The name of the layout file to parse.
	 *
	 * @return  void
	 */
	public function display($tpl = null)
	{
		// Get the form to display
		$this->form = $this->get('Form');
		// Get the javascript script file for client-side validation
		$this->script = $this->get('Script');

		// Check that the user has permissions to create a new helloworld record
		$this->canDo = JHelperContent::getActions('com_helloworld');
		if (!($this->canDo->get('core.create')))
		{
			$app = JFactory::getApplication();
			$app->enqueueMessage(JText::_('JERROR_ALERTNOAUTHOR'), 'error');
			$app->setHeader('status', 403, true);
			return;
		}

		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			throw new Exception(implode("\n", $errors), 500);
		}

		// Call the parent display to display the layout file
		parent::display($tpl);

		// Set properties of the html document
		$this->setDocument();
	}

	/**
	 * Method to set up the html document properties
	 *
	 * @return void
	 */
	protected function setDocument()
	{
		$document = JFactory::getDocument();
		$document->setTitle(JText::_('COM_HELLOWORLD_HELLOWORLD_CREATING'));
		$document->addScript(JURI::root() . $this->script);
		$document->addScript(JURI::root() . "/administrator/components/com_helloworld"
		                                  . "/views/helloworld/submitbutton.js");
		JText::script('COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE');
	}
}

And into the layout file:

site/views/form/tmpl/edit.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 *
 * This layout file is for displaying the front end form for capturing a new helloworld message
 *
 */

// No direct access
defined('_JEXEC') or die('Restricted access');
JHtml::_('behavior.formvalidator');

?>
<form action="<?php echo JRoute::_('index.php?option=com_helloworld&view=form&layout=edit'); ?>"
    method="post" name="adminForm" id="adminForm" class="form-validate">

	<div class="form-horizontal">
		<fieldset class="adminform">
			<legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_DETAILS') ?></legend>
			<div class="row-fluid">
				<div class="span6">
					<?php echo $this->form->renderFieldset('details');  ?>
				</div>
			</div>
		</fieldset>
	</div>

	<div class="btn-toolbar">
		<div class="btn-group">
			<button type="button" class="btn btn-primary" onclick="Joomla.submitbutton('helloworld.save')">
				<span class="icon-ok"></span><?php echo JText::_('JSAVE') ?>
			</button>
		</div>
		<div class="btn-group">
			<button type="button" class="btn" onclick="Joomla.submitbutton('helloworld.cancel')">
				<span class="icon-cancel"></span><?php echo JText::_('JCANCEL') ?>
			</button>
		</div>
	</div>

	<input type="hidden" name="task" />
	<?php echo JHtml::_('form.token'); ?>
</form>

All the above is similar to the Administrator functionality, except there are certain fields we don't want to allow the Frontend user to see (e.g. the permissions) and instead of the standard Joomla Administrator buttons (and toolbar) we use a Save button and a Cancel button styled using the Bootstrap CSS, and which result in the task parameter being set to helloworld.save or helloworld.cancel.

Our new model is almost exactly the same as the Administrator equivalent; we just have to point to our new add-form instead.

site/models/form.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelForm extends JModelAdmin
{

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

    /**
	 * Method to get the record form.
	 *
	 * @param   array    $data      Data for the form.
	 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
	 *
	 * @return  mixed    A JForm object on success, false on failure
	 *
	 * @since   1.6
	 */
	public function getForm($data = array(), $loadData = true)
	{
		// Get the form.
		$form = $this->loadForm(
			'com_helloworld.form',
			'add-form',
			array(
				'control' => 'jform',
				'load_data' => $loadData
			)
		);

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

		return $form;
	}

	/**
	 * Method to get the data that should be injected in the form.
	 * As this form is for add, we're not prefilling the form with an existing record
	 * But if the user has previously hit submit and the validation has found an error,
	 * then we inject what was previously entered.
	 *
	 * @return  mixed  The data for the form.
	 *
	 * @since   1.6
	 */
	protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$data = JFactory::getApplication()->getUserState(
			'com_helloworld.edit.helloworld.data',
			array()
		);

		return $data;
	}

	/**
	 * Method to get the script that have to be included on the form
	 * This returns the script associated with helloworld field greeting validation
	 *
	 * @return string	Script files
	 */
	public function getScript()
	{
		return 'administrator/components/com_helloworld/models/forms/helloworld.js';
	}
}

Handling the Form HTTP POST[edit]

As in the Administrator case, we'll need to create a new controller in the /controllers subdirectory with save() and cancel() methods, and we'll need a model with a save() method. In fact, because the above model class (in site/models/form.php) extends JModelAdmin, we'll be able to reuse it without adding any more code. (For just displaying the form our model class could have extended JModelForm, but JModelAdmin extends JModelForm and provides additional functions such as the save() one that we need).

Note that we don't need any new view file or layout file, because we're just going to redirect the user back to the same form.

So the only new file we need is a new controller in a new controllers subdirectory:

site/controllers/helloworld.php

<?php
/**
 * @package     Joomla.Site
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorld Controller
 *
 * @package     Joomla.Site
 * @subpackage  com_helloworld
 *
 * Used to handle the http POST from the Frontend form which allows
 * users to enter a new helloworld message
 *
 */
class HelloWorldControllerHelloWorld extends JControllerForm
{
    public function cancel($key = null)
    {
        parent::cancel($key);

        // set up the redirect back to the same form
        $this->setRedirect(
            (string)JUri::getInstance(),
            JText::_('COM_HELLOWORLD_ADD_CANCELLED')
		);
    }

    /*
     * Function handing the save for adding a new helloworld record
     * Based on the save() function in the JControllerForm class
     */
    public function save($key = null, $urlVar = null)
    {
		// Check for request forgeries.
		JSession::checkToken() or jexit(JText::_('JINVALID_TOKEN'));

		$app = JFactory::getApplication();
		$input = $app->input;
		$model = $this->getModel('form');

		// Get the current URI to set in redirects. As we're handling a POST,
		// this URI comes from the <form action="..."> attribute in the layout file above
		$currentUri = (string)JUri::getInstance();

		// Check that this user is allowed to add a new record
		if (!JFactory::getUser()->authorise( "core.create", "com_helloworld"))
		{
			$app->enqueueMessage(JText::_('JERROR_ALERTNOAUTHOR'), 'error');
			$app->setHeader('status', 403, true);

			return;
		}

		// get the data from the HTTP POST request
		$data  = $input->get('jform', array(), 'array');

		// set up context for saving form data
		$context = "$this->option.edit.$this->context";

		// Validate the posted data.
		// First we need to set up an instance of the form ...
		$form = $model->getForm($data, false);

		if (!$form)
		{
			$app->enqueueMessage($model->getError(), 'error');
			return false;
		}

		// ... and then we validate the data against it
		// The validate function called below results in the running of the validate="..." routines
		// specified against the fields in the form xml file, and also filters the data
		// according to the filter="..." specified in the same place (removing html tags by default in strings)
		$validData = $model->validate($form, $data);

		// Handle the case where there are validation errors
		if ($validData === false)
		{
			// Get the validation messages.
			$errors = $model->getErrors();

			// Display up to three validation messages to the user.
			for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++)
			{
				if ($errors[$i] instanceof Exception)
				{
					$app->enqueueMessage($errors[$i]->getMessage(), 'warning');
				}
				else
				{
					$app->enqueueMessage($errors[$i], 'warning');
				}
			}

			// Save the form data in the session.
			$app->setUserState($context . '.data', $data);

			// Redirect back to the same screen.
			$this->setRedirect($currentUri);

			return false;
		}

		// add the 'created by' and 'created' date fields
		$validData['created_by'] = JFactory::getUser()->get('id', 0);
		$validData['created'] = date('Y-m-d h:i:s');

		// Attempt to save the data.
		if (!$model->save($validData))
		{
		// Handle the case where the save failed

			// Save the data in the session.
			$app->setUserState($context . '.data', $validData);

			// Redirect back to the edit screen.
			$this->setError(JText::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()));
			$this->setMessage($this->getError(), 'error');

			$this->setRedirect($currentUri);

			return false;
		}

		// clear the data in the form
		$app->setUserState($context . '.data', null);

		// notify the administrator that a new helloworld message has been added on the front end

		// get the id of the person to notify from global config
		$params   = $app->getParams();
		$userid_to_email = (int) $params->get('user_to_email');
		$user_to_email = JUser::getInstance($userid_to_email);
		$to_address = $user_to_email->get("email");

		// get the current user (if any)
		$current_user = JFactory::getUser();
		if ($current_user->get("id") > 0)
		{
			$current_username = $current_user->get("username");
		}
		else
		{
			$current_username = "a visitor to the site";
		}

		// get the Mailer object, set up the email to be sent, and send it
		$mailer = JFactory::getMailer();
		$mailer->addRecipient($to_address);
		$mailer->setSubject("New helloworld message added by " . $current_username);
		$mailer->setBody("New greeting is " . $validData['greeting']);
		try
		{
			$mailer->send();
		}
		catch (Exception $e)
		{
			JLog::add('Caught exception: ' . $e->getMessage(), JLog::Error, 'jerror');
		}

		$this->setRedirect(
				$currentUri,
				JText::_('COM_HELLOWORLD_ADD_SUCCESSFUL')
				);

		return true;

    }

}

In the above file we've been able to reuse the cancel() method from the parent JControllerForm class, with the exception that we want to redirect back to the same form. (The parent cancel() method redirects back to the view showing the list of records - as in the Administrator case).

However, our save() method functionality is significantly different from that of the parent class, so it's easier just to write our own. Note that we're recording the user and created date/time for records added via the Frontend only. If we wanted to record this information for all records, then we'd need to override a save()function for the Backend as well to include setting those fields.

The mailing functionality relies on you having set up mail in your Joomla instance. If you're using localhost then one possibility is to configure this using a gmail account, and you can easily find the SMTP settings for this on the Internet. Note that you'll need to allow "less secure apps" on the gmail account.

Configuration Parameters[edit]

We've introduced two new configuration parameters - the CAPTCHA to display on the form, and the user to email with notification of a new helloworld message being added. So update the configuration settings as follows:

admin/config.xml

<?xml version="1.0" encoding="utf-8"?>
<config>
	<fieldset
		name="greetings"
		label="COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_LABEL"
		description="COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC"
	>
		<field
			name="show_category"
			type="radio"
			label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
			description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
			default="0"
		>
			<option value="0">JHIDE</option>
			<option value="1">JSHOW</option>
		</field>
        <field
			name="captcha"
			type="plugins"
            folder="captcha"
			label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL"
			description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC"
			default="0"
			filter="cmd"
		>
            <option value="">JOPTION_USE_DEFAULT</option>
			<option value="0">JOPTION_DO_NOT_USE</option>
		</field>
        <field
			name="user_to_email"
			type="user"
			label="COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL"
			description="COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC"
			default="0"
		>
		</field>
	</fieldset>
    <fieldset
		name="permissions"
		label="JCONFIG_PERMISSIONS_LABEL"
		description="JCONFIG_PERMISSIONS_DESC"
	>
		<field
			name="rules"
			type="rules"
			label="JCONFIG_PERMISSIONS_LABEL"
			class="inputbox"
			validate="rules"
			filter="rules"
			component="com_helloworld"
			section="component"
		/>
	</fieldset>
</config>

Display the New Fields on the Administrator List Form[edit]

We want to update the Administrator helloworld list layout file to include the new Author and Created Date fields. This means that we also need to update our model to include these fields in the SQL query.

In addition, we've changed column ordering mechanism from what it was in Adding decorations to the backend to the more modern way that this is handled.

admin/views/helloworlds/view.html.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorlds View
 *
 * @since  0.0.1
 */
class HelloWorldViewHelloWorlds extends JViewLegacy
{
	/**
	 * Display the Hello World view
	 *
	 * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
	 *
	 * @return  void
	 */
	function display($tpl = null)
	{

		// Get application
		$app = JFactory::getApplication();
		$context = "helloworld.list.admin.helloworld";
		// Get data from the model
		$this->items			= $this->get('Items');
		$this->pagination		= $this->get('Pagination');
		$this->state			= $this->get('State');
		// Remove the old ordering mechanism
		//$this->filter_order 	= $app->getUserStateFromRequest($context.'filter_order', 'filter_order', 'greeting', 'cmd');
		//$this->filter_order_Dir = $app->getUserStateFromRequest($context.'filter_order_Dir', 'filter_order_Dir', 'asc', 'cmd');
		$this->filterForm    	= $this->get('FilterForm');
		$this->activeFilters 	= $this->get('ActiveFilters');

		// What Access Permissions does this user have? What can (s)he do?
		$this->canDo = JHelperContent::getActions('com_helloworld');

		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			JError::raiseError(500, implode('<br />', $errors));

			return false;
		}

		// Set the submenu
		HelloWorldHelper::addSubmenu('helloworlds');

		// Set the toolbar and number of found items
		$this->addToolBar();

		// Display the template
		parent::display($tpl);

		// Set the document
		$this->setDocument();
	}

	/**
	 * Add the page title and toolbar.
	 *
	 * @return  void
	 *
	 * @since   1.6
	 */
	protected function addToolBar()
	{
		$title = JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLDS');

		if ($this->pagination->total)
		{
			$title .= "<span style='font-size: 0.5em; vertical-align: middle;'>(" . $this->pagination->total . ")</span>";
		}

		JToolBarHelper::title($title, 'helloworld');
		if ($this->canDo->get('core.create'))
		{
			JToolBarHelper::addNew('helloworld.add', 'JTOOLBAR_NEW');
		}
		if ($this->canDo->get('core.edit'))
		{
			JToolBarHelper::editList('helloworld.edit', 'JTOOLBAR_EDIT');
		}
		if ($this->canDo->get('core.delete'))
		{
			JToolBarHelper::deleteList('', 'helloworlds.delete', 'JTOOLBAR_DELETE');
		}
		if ($this->canDo->get('core.admin'))
		{
			JToolBarHelper::divider();
			JToolBarHelper::preferences('com_helloworld');
		}
	}
	/**
	 * Method to set up the document properties
	 *
	 * @return void
	 */
	protected function setDocument()
	{
		$document = JFactory::getDocument();
		$document->setTitle(JText::_('COM_HELLOWORLD_ADMINISTRATION'));
	}
}

Note that in the layout file below the hidden input fields filter_order and filter_order_Dir have been removed.

admin/views/helloworlds/tmpl/default.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted Access');

JHtml::_('formbehavior.chosen', 'select');

$listOrder     = $this->escape($this->state->get('list.ordering'));
$listDirn      = $this->escape($this->state->get('list.direction'));
?>
<form action="index.php?option=com_helloworld&view=helloworlds" method="post" id="adminForm" name="adminForm">
	<div id="j-sidebar-container" class="span2">
		<?php echo JHtmlSidebar::render(); ?>
	</div>
	<div id="j-main-container" class="span10">
        <div class="row-fluid">
            <div class="span12">
                <?php echo JText::_('COM_HELLOWORLD_HELLOWORLDS_FILTER'); ?>
                <?php
                    echo JLayoutHelper::render(
                        'joomla.searchtools.default',
                        array('view' => $this)
                    );
                ?>
            </div>
        </div>
        <table class="table table-striped table-hover">
            <thead>
            <tr>
                <th width="1%"><?php echo JText::_('COM_HELLOWORLD_NUM'); ?></th>
                <th width="2%">
                    <?php echo JHtml::_('grid.checkall'); ?>
                </th>
                <th width="30%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_HELLOWORLDS_NAME', 'greeting', $listDirn, $listOrder); ?>
                </th>
                <th width="30%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_AUTHOR', 'author', $listDirn, $listOrder); ?>
                </th>
                <th width="30%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_CREATED_DATE', 'created', $listDirn, $listOrder); ?>
                </th>
                <th width="5%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_PUBLISHED', 'published', $listDirn, $listOrder); ?>
                </th>
                <th width="2%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_ID', 'id', $listDirn, $listOrder); ?>
                </th>
            </tr>
            </thead>
            <tfoot>
                <tr>
                    <td colspan="5">
                        <?php echo $this->pagination->getListFooter(); ?>
                    </td>
                </tr>
            </tfoot>
            <tbody>
                <?php if (!empty($this->items)) : ?>
                    <?php foreach ($this->items as $i => $row) :
                        $link = JRoute::_('index.php?option=com_helloworld&task=helloworld.edit&id=' . $row->id);
                    ?>
                        <tr>
                            <td><?php echo $this->pagination->getRowOffset($i); ?></td>
                            <td>
                                <?php echo JHtml::_('grid.id', $i, $row->id); ?>
                            </td>
                            <td>
                                <a href="<?php echo $link; ?>" title="<?php echo JText::_('COM_HELLOWORLD_EDIT_HELLOWORLD'); ?>">
                                    <?php echo $row->greeting; ?>
                                </a>
                                <div class="small">
									<?php echo JText::_('JCATEGORY') . ': ' . $this->escape($row->category_title); ?>
								</div>
                            </td>
                            <td align="center">
                                <?php echo $row->author; ?>
                            </td>
                            <td align="center">
                                <?php echo substr($row->created, 0, 10); ?>
                            </td>
                            <td align="center">
                                <?php echo JHtml::_('jgrid.published', $row->published, $i, 'helloworlds.', true, 'cb'); ?>
                            </td>
                            <td align="center">
                                <?php echo $row->id; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>
        <input type="hidden" name="task" value=""/>
        <input type="hidden" name="boxchecked" value="0"/>
        <?php echo JHtml::_('form.token'); ?>
    </div>
</form>

Instead of the using the hidden fields filter_order and filter_order_Dir to keep track of the current column for ordering and direction, we will use the list["fullordering"] field, and this will now be displayed on the form as well (as is done within com_content for the Articles view). We'll also need to include the new language strings in this field in our Administrator language file.

admin/models/forms/filter_helloworlds.xml

<?xml version="1.0" encoding="utf-8"?>
<form>
	<fields name="filter">
		<field
			name="search"
			type="text"
			label="COM_BANNERS_SEARCH_IN_TITLE"
			hint="JSEARCH_FILTER"
			class="js-stools-search-string"
		/>
		<field
			name="published"
			type="status"
			label="JOPTION_SELECT_PUBLISHED"
			description="JOPTION_SELECT_PUBLISHED_DESC"
			onchange="this.form.submit();"
			>
			<option value="">JOPTION_SELECT_PUBLISHED</option>
		</field>
	</fields>
	<fields name="list">
		<field
			name="fullordering"
			type="list"
			label="COM_HELLOWORLD_LIST_FULL_ORDERING"
			description="COM_HELLOWORLD_LIST_FULL_ORDERING_DESC"
			onchange="this.form.submit();"
			default="greeting ASC"
			>
			<option value="">JGLOBAL_SORT_BY</option>
			<option value="greeting ASC">COM_HELLOWORLD_ORDERING_ASC</option>
			<option value="greeting DESC">COM_HELLOWORLD_ORDERING_DESC</option>
			<option value="id ASC">JGRID_HEADING_ID_ASC</option>
			<option value="id DESC">JGRID_HEADING_ID_DESC</option>
			<option value="published ASC">COM_HELLOWORLD_PUBLISHED_ASC</option>
			<option value="published DESC">COM_HELLOWORLD_PUBLISHED_DESC</option>
			<option value="author ASC">COM_HELLOWORLD_AUTHOR_ASC</option>
			<option value="author DESC">COM_HELLOWORLD_AUTHOR_DESC</option>
			<option value="created ASC">COM_HELLOWORLD_CREATED_ASC</option>
			<option value="created DESC">COM_HELLOWORLD_CREATED_DESC</option>
		</field>
		<field
			name="limit"
			type="limitbox"
			class="input-mini"
			default="25"
			label="COM_CONTENT_LIST_LIMIT"
			description="COM_HELLOWORLD_LIST_LIMIT_DESC"
			onchange="this.form.submit();"
		/>
	</fields>
</form>

The above status type field (second field element, with name="published") displays the status values Trashed and Archived that the helloworld component doesn't support. We could define our own custom helloworldStatus type field which displayed only the publishing options, but as defining such custom fields has already been covered in the step Using the database with our helloworld custom field, we won't bother with that here.

Within the model we need to get the author and created date fields from the database, and we also need to add these to the filter_fields array of fields that relate to sortable columns.

admin/models/helloworlds.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorldList Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorlds extends JModelList
{
	/**
	 * Constructor.
	 *
	 * @param   array  $config  An optional associative array of configuration settings.
	 *
	 * @see     JController
	 * @since   1.6
	 */
	public function __construct($config = array())
	{
		if (empty($config['filter_fields']))
		{
			$config['filter_fields'] = array(
				'id',
				'greeting',
				'author',
				'created',
				'published'
			);
		}

		parent::__construct($config);
	}

	/**
	 * Method to build an SQL query to load the list data.
	 *
	 * @return      string  An SQL query
	 */
	protected function getListQuery()
	{
		// Initialize variables.
		$db    = JFactory::getDbo();
		$query = $db->getQuery(true);

		// Create the base select statement.
		$query->select('a.id as id, a.greeting as greeting, a.published as published, a.created as created')
			  ->from($db->quoteName('#__helloworld', 'a'));

		// Join over the categories.
		$query->select($db->quoteName('c.title', 'category_title'))
			->join('LEFT', $db->quoteName('#__categories', 'c') . ' ON c.id = a.catid');

		// Join with users table to get the username of the author
		$query->select($db->quoteName('u.username', 'author'))
			->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.created_by');

		// Filter: like / search
		$search = $this->getState('filter.search');

		if (!empty($search))
		{
			$like = $db->quote('%' . $search . '%');
			$query->where('greeting LIKE ' . $like);
		}

		// Filter by published state
		$published = $this->getState('filter.published');

		if (is_numeric($published))
		{
			$query->where('a.published = ' . (int) $published);
		}
		elseif ($published === '')
		{
			$query->where('(a.published IN (0, 1))');
		}

		// Add the list ordering clause.
		$orderCol	= $this->state->get('list.ordering', 'greeting');
		$orderDirn 	= $this->state->get('list.direction', 'asc');

		$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));

		return $query;
	}
}

Updated Language Strings[edit]

site/language/en-GB/en-GB.com_helloworld.ini

; add new message form
COM_HELLOWORLD_LEGEND_DETAILS="New Helloworld Message Details"
COM_HELLOWORLD_HELLOWORLD_CREATING="Add message"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Sorry, you have an error"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Message details"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Please specify the greeting to add"
COM_HELLOWORLD_HELLOWORLD_GREETING_HINT="Letters only!"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="Please select the associated category"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_LABEL="Reason"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_DESC="Please say why you're adding this greeting"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_HINT="No HTML tags!"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Spam protection"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Prove you're a real person!"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Display category or not?"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="Select if you want the category displayed too"
; save and cancel confirmation messages
COM_HELLOWORLD_ADD_SUCCESSFUL="New greeting successfully saved"
COM_HELLOWORLD_ADD_CANCELLED="New greeting cancelled ok"

admin/language/en-GB/en-GB.com_helloworld.sys.ini

; Joomla! Project
; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved.
; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
; Note : All ini files need to be saved as UTF-8

COM_HELLOWORLD="Hello World!"
COM_HELLOWORLD_DESCRIPTION="This is the Hello World description"
COM_HELLOWORLD_HELLOWORLD_VIEW_DEFAULT_TITLE="Hello World"
COM_HELLOWORLD_HELLOWORLD_VIEW_DEFAULT_DESC="This view displays a selected message"
COM_HELLOWORLD_ADD_VIEW_TITLE="Hello World form"
COM_HELLOWORLD_ADD_VIEW_DESC="This displays a form to allow the user to enter a new message"
COM_HELLOWORLD_INSTALL_TEXT="HelloWorld Install script"
COM_HELLOWORLD_MENU="Hello World!"
COM_HELLOWORLD_POSTFLIGHT_DISCOVER_INSTALL_TEXT="HelloWorld postflight discover install script"
COM_HELLOWORLD_POSTFLIGHT_INSTALL_TEXT="HelloWorld postflight install script"
COM_HELLOWORLD_POSTFLIGHT_UNINSTALL_TEXT="HelloWorld postflight uninstall script"
COM_HELLOWORLD_POSTFLIGHT_UPDATE_TEXT="HelloWorld postflight update script"
COM_HELLOWORLD_PREFLIGHT_DISCOVER_INSTALL_TEXT="HelloWorld preflight discover install script"
COM_HELLOWORLD_PREFLIGHT_INSTALL_TEXT="HelloWorld preflight install script"
COM_HELLOWORLD_PREFLIGHT_UNINSTALL_TEXT="HelloWorld preflight uninstall script"
COM_HELLOWORLD_PREFLIGHT_UPDATE_TEXT="HelloWorld preflight update script"
COM_HELLOWORLD_UNINSTALL_TEXT="HelloWorld Uninstall script"
COM_HELLOWORLD_UPDATE_TEXT="HelloWorld Update script. HelloWorld now updated to version %s."

admin/language/en-GB/en-GB.com_helloworld.ini

; Joomla! Project
; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved.
; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
; Note : All ini files need to be saved as UTF-8

COM_HELLOWORLD_ADMINISTRATION="HelloWorld - Administration"
COM_HELLOWORLD_ADMINISTRATION_CATEGORIES="HelloWorld - Categories"
COM_HELLOWORLD_NUM="#"
COM_HELLOWORLD_HELLOWORLDS_FILTER="Filters"
COM_HELLOWORLD_AUTHOR="Author"
COM_HELLOWORLD_CREATED_DATE="Created"
COM_HELLOWORLD_PUBLISHED="Published"
COM_HELLOWORLD_HELLOWORLDS_NAME="Name"
COM_HELLOWORLD_ID="Id"

COM_HELLOWORLD_HELLOWORLD_CREATING="HelloWorld - Creating"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Details"
COM_HELLOWORLD_HELLOWORLD_EDITING="HelloWorld - Editing"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Some values are unacceptable"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="The category the messages belongs to"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_DESC="This message will be displayed"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_LABEL="Message"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Show category"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="If set to Show, the title of the message&rsquo;s category will show."
COM_HELLOWORLD_HELLOWORLD_HEADING_GREETING="Greeting"
COM_HELLOWORLD_HELLOWORLD_HEADING_ID="Id"
COM_HELLOWORLD_MANAGER_HELLOWORLD_EDIT="HelloWorld manager: Edit Message"
COM_HELLOWORLD_MANAGER_HELLOWORLD_NEW="HelloWorld manager: New Message"
COM_HELLOWORLD_MANAGER_HELLOWORLDS="HelloWorld manager"
COM_HELLOWORLD_EDIT_HELLOWORLD="Edit message"
COM_HELLOWORLD_N_ITEMS_DELETED_1="One message deleted"
COM_HELLOWORLD_N_ITEMS_DELETED_MORE="%d messages deleted"
COM_HELLOWORLD_N_ITEMS_PUBLISHED="%d message(s) published"
COM_HELLOWORLD_N_ITEMS_UNPUBLISHED="%d message(s) unpublished"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Add Hello World Greeting"
COM_HELLOWORLD_SUBMENU_MESSAGES="Messages"
COM_HELLOWORLD_SUBMENU_CATEGORIES="Categories"
COM_HELLOWORLD_CONFIGURATION="HelloWorld Configuration"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_LABEL="Messages settings"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC="Settings that will be applied to all messages by default"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Captcha"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Select Captcha to use on front end form"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL="User to email"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC="Select user to email when a new message is entered on front end"
COM_HELLOWORLD_FIELDSET_RULES="Message Permissions"
COM_HELLOWORLD_FIELD_RULES_LABEL="Permissions"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to edit this message?"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to delete this message?"
COM_HELLOWORLD_TAB_NEW_MESSAGE="New Message"
COM_HELLOWORLD_TAB_EDIT_MESSAGE="Message Details"
COM_HELLOWORLD_TAB_PARAMS="Parameters"
COM_HELLOWORLD_TAB_PERMISSIONS="Permissions"
COM_HELLOWORLD_LEGEND_DETAILS="Message Details"
COM_HELLOWORLD_LEGEND_PARAMS="Message Parameters"
COM_HELLOWORLD_LEGEND_PERMISSIONS="Message Permissions"
; Column ordering in the Helloworlds view
COM_HELLOWORLD_ORDERING_ASC="Greeting ascending"
COM_HELLOWORLD_ORDERING_DESC="Greeting descending"
COM_HELLOWORLD_AUTHOR_ASC="Author ascending"
COM_HELLOWORLD_AUTHOR_DESC="Author descending"
COM_HELLOWORLD_CREATED_ASC="Creation date ascending"
COM_HELLOWORLD_CREATED_DESC="Creation date descending"
COM_HELLOWORLD_PUBLISHED_ASC="Unpublished first"
COM_HELLOWORLD_PUBLISHED_DESC="Published first"


Packaging the Component[edit]

Contents of your code directory. Each file link below takes you to the step in the tutorial that has the latest version of that source code file.

helloworld.xml

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

	<name>COM_HELLOWORLD</name>
	<!-- The following elements are optional and free of formatting constraints -->
	<creationDate>January 2018</creationDate>
	<author>John Doe</author>
	<authorEmail>john.doe@example.org</authorEmail>
	<authorUrl>http://www.example.org</authorUrl>
	<copyright>Copyright Info</copyright>
	<license>License Info</license>
	<!--  The version string is recorded in the components table -->
	<version>0.0.16</version>
	<!-- The description is optional and defaults to the name -->
	<description>COM_HELLOWORLD_DESCRIPTION</description>

	<!-- Runs on install/uninstall/update; New in 2.5 -->
	<scriptfile>script.php</scriptfile>

	<install> <!-- Runs on install -->
		<sql>
			<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
		</sql>
	</install>
	<uninstall> <!-- Runs on uninstall -->
		<sql>
			<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>
		</sql>
	</uninstall>
	<update> <!-- Runs on update; New since J2.5 -->
		<schemas>
			<schemapath type="mysql">sql/updates/mysql</schemapath>
		</schemas>
	</update>

	<!-- Site Main File Copy Section -->
	<!-- Note the folder attribute: This attribute describes the folder
		to copy FROM in the package to install therefore files copied
		in this section are copied from /site/ in the package -->
	<files folder="site">
		<filename>index.html</filename>
		<filename>helloworld.php</filename>
		<filename>controller.php</filename>
		<folder>controllers</folder>
		<folder>views</folder>
		<folder>models</folder>
	</files>

        <languages folder="site/language">
		<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
        </languages>

	<media destination="com_helloworld" folder="media">
		<filename>index.html</filename>
		<folder>images</folder>
	</media>

	<administration>
		<!-- Administration Menu Section -->
		<menu link='index.php?option=com_helloworld' img="../media/com_helloworld/images/tux-16x16.png">COM_HELLOWORLD_MENU</menu>
		<!-- Administration Main File Copy Section -->
		<!-- Note the folder attribute: This attribute describes the folder
			to copy FROM in the package to install therefore files copied
			in this section are copied from /admin/ in the package -->
		<files folder="admin">
			<!-- Admin Main File Copy Section -->
			<filename>index.html</filename>
			<filename>config.xml</filename>
			<filename>helloworld.php</filename>
			<filename>controller.php</filename>
			<filename>access.xml</filename>
			<!-- SQL files section -->
			<folder>sql</folder>
			<!-- tables files section -->
			<folder>tables</folder>
			<!-- models files section -->
			<folder>models</folder>
			<!-- views files section -->
			<folder>views</folder>
			<!-- controllers files section -->
			<folder>controllers</folder>
			<!-- helpers files section -->
			<folder>helpers</folder>
		</files>
		<languages folder="admin/language">
        		<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
                        <language tag="en-GB">en-GB/en-GB.com_helloworld.sys.ini</language>
		</languages>
	</administration>

</extension>

Contributors[edit]