Thứ Sáu, 17 tháng 5, 2013

How to add custom filters to components


Overview

This is a how-to on how to add ordering and filtering (search, custom dropdown, state) to a component backend page.
This approach is almost exactly the same for the frontend.
In general, the process of adding a custom field filter breaks down into the following steps:
  • Creating the class that will generate the form dropdown options
  • Adapting the view to display the form dropdown input
  • Adapting the model to retrieve data from DB & to set submission values in the Joomla 'state'. For a better understanding of the code samples, the db table is called 'my_company' and the fields are: id, name, state, company, somefield, someotherfieldtosearchin.

Extend JFormFieldList (models / fields / fieldname.php)

Frequently your filter fields will be very basic; probably simply a dropdown list of one of the columns being displayed on the current page. Regardless, you will need to create your own field element. This really isn't a big deal - this file will probably be less than 70 lines, including comments. The code below will generate the dropdown element to filter by companies. For more information and complex examples on creating this class see Creating_a_custom_form_field_type.
My 'companies' dropdown element:
<?php
 
defined('JPATH_BASE') or die;
 
jimport('joomla.html.html');
jimport('joomla.form.formfield');
jimport('joomla.form.helper');
JFormHelper::loadFieldClass('list');
 
/**
* Custom Field class for the Joomla Framework.
*
* @package Joomla.Administrator
* @subpackage com_my
* @since 1.6
*/

class JFormFieldMyCompany extends JFormFieldList
{
/**
* The form field type.
*
* @var string
* @since 1.6
*/

protected $type = 'MyCompany';
 
/**
* Method to get the field options.
*
* @return array The field option objects.
* @since 1.6
*/

public function getOptions()
{
// Initialize variables.
$options = array();
 
$db = JFactory::getDbo();
$query = $db->getQuery(true);
 
$query->select('id As value, name As text');
$query->from('#__my_companies AS a');
$query->order('a.name');
$query->where('state = 1');
 
// Get the options.
$db->setQuery($query);
 
$options = $db->loadObjectList();
 
// Check for a database error.
if ($db->getErrorNum()) {
JError::raiseWarning(500, $db->getErrorMsg());
}
 
return $options;
}
}
Obviously, there is only one section here that needs to be customized:
                $query->select('id As value, name As text');
$query->from('#__my_companies AS a');
$query->order('a.name');
$query->where('state = 1');
Change the database name and adjust the sql as necessary. This will produce the text and values for the select element options. I highly recommend not changing the value and text names. Although I did not test this theory, it seems likely that ancestor classes are expecting these names as keys in the resulting array.

Modify Model (models / zzz.php)

In your model you have to include these two functions:
//Add this handy array with database fields to search in
protected $searchInFields = array('text','a.name','someotherfieldtosearchin');
 
//Override construct to allow filtering and ordering on our fields
public function __construct($config = array()) {
$config['filter_fields']=array_merge($this->searchInFields,array('a.company'));
parent::__construct($config);
}
 
protected function getListQuery(){
$db = JFactory::getDBO();
$query = $db->getQuery(true);
 
//CHANGE THIS QUERY AS YOU NEED...
$query->select('id As value, name As text, somefield')
->from('#__my_companies AS a')
->order( $db->escape($this->getState('list.ordering', 'pa.id')) . ' ' .
$db->escape($this->getState('list.direction', 'desc')));
 
 
 
// Filter search // Extra: Search more than one fields and for multiple words
$regex = str_replace(' ', '|', $this->getState('filter.search'));
if (!empty($regex)) {
$regex=' REGEXP '.$db->quote($regex);
$query->where('('.implode($regex.' OR ',$this->searchInFields).$regex.')');
}
 
// Filter company
$company= $db->escape($this->getState('filter.company'));
if (!empty($company)) {
$query->where('(a.company='.$company.')');
}
 
// Filter by state (published, trashed, etc.)
$state = $db->escape($this->getState('filter.state'));
if (is_numeric($state)) {
$query->where('a.state = ' . (int) $state);
}
elseif ($state === '') {
$query->where('(a.state = 0 OR a.state = 1)');
}
 
//echo $db->replacePrefix( (string) $query );//debug
return $query;
}
 
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @since 1.6
*/

protected function populateState($ordering = null, $direction = null)
{
// Initialise variables.
$app = JFactory::getApplication('administrator');
 
// Load the filter state.
$search = $this->getUserStateFromRequest($this->context.'.filter.search', 'filter_search');
//Omit double (white-)spaces and set state
$this->setState('filter.search', preg_replace('/\s+/',' ', $search));
 
//Filter (dropdown) state
$state = $this->getUserStateFromRequest($this->context.'.filter.state', 'filter_state', '', 'string');
$this->setState('filter.state', $state);
 
//Filter (dropdown) company
$state = $this->getUserStateFromRequest($this->context.'.filter.company', 'filter_company', '', 'string');
$this->setState('filter.company', $state);
 
//Takes care of states: list. limit / start / ordering / direction
parent::populateState('a.name', 'asc');
}
For the frontend: your $app variable looks like this:
  protected function populateState($ordering = null, $direction = null)
{
// Initialise variables.
$app = JFactory::getApplication();

Modify View (views / zzz / view.html.php)

In you view class, you have to set the state, like this:
                $this->items            = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
 
//Following variables used more than once
$this->sortColumn = $this->state->get('list.ordering');
$this->sortDirection = $this->state->get('list.direction');
$this->searchterms = $this->state->get('filter.search');

Modify Template (views / zzz / tmpl / default.php)

Basic

Top of template somewhere:
//Get companie options
JFormHelper::addFieldPath(JPATH_COMPONENT . '/models/fields');
$companies = JFormHelper::loadFieldType('MyCompany', false);
$companyOptions=$companies->getOptions(); // works only if you set your field getOptions on public!!
In your template file, this fieldset reflects filter options:
        <fieldset id="filter-bar">
<div class="filter-search fltlft">
<input type="text" name="filter_search" id="filter_search" value="<?php echo $this->escape($this->searchterms); ?>" title="<?php echo JText::_('Search in company, etc.'); ?>" />
<button type="submit">
<?php echo JText::_('JSEARCH_FILTER_SUBMIT'); ?>
</button>
<button type="button" onclick="document.id('filter_search').value='';this.form.submit();">
<?php echo JText::_('JSEARCH_FILTER_CLEAR'); ?>
</button>
</div>
<div class="filter-select fltrt">
<select name="filter_state" class="inputbox" onchange="this.form.submit()">
<option value="">
<?php echo JText::_('JOPTION_SELECT_PUBLISHED');?>
</option>
<?php echo JHtml::_('select.options', JHtml::_('jgrid.publishedOptions', array('archived'=>false)), 'value', 'text', $this->state->get('filter.state'), true);?>
</select>
 
<select name="filter_type" class="inputbox" onchange="this.form.submit()">
<option value=""> - Select Company - </option>
<?php echo JHtml::_('select.options', $companyOptions, 'value', 'text', $this->state->get('filter.company'));?>
</select>
 
</div>
</fieldset>
Remark: the name of the select tag must be the same as defined in the populateState function. In the example:
    //populateState function
$state = $this->getUserStateFromRequest($this->context.'.filter.state', 'filter_state', '', 'string');
 
//template
<select name="filter_state" class="inputbox" onchange="this.form.submit()">

Extra: highlighting search terms

Here is some extra to visually mark the found search terms in the results page.

Template

Add this somewhere at top somehwere:
$searchterms = $this->state->get('filter.search');
 
//Highlight search terms with js (if we did a search => more performant and otherwise crash)
if (strlen($searchterms)>1) JHtml::_('behavior.highlighter', explode(' ',$searchterms));
And further in your template, enclose the specific fields with the default highlighter finder.
Example:
<span id="highlighter-start"></span>
<table class="adminlist">
...
</table>
<span id="highlighter-end"></span>

CSS

If your default admin template doesn't have required css, you should add the class in your component css.
Example in controller:
JHtml::stylesheet('com_peopleactions/admin.css', array(), true);
In admin.css:
.highlight {   background: none repeat scroll 0 0 #FFFF00; }

Adding view layout configuration parameters


Background

When creating views for a custom component, you can create a series of .xml files that will both describe the view and allow administrators to configure the view as they require. The .xml files are used by the com_menu core component (ilink.php) to create the menu trees that are shown when an administrator creates a new menu item.

View title and description

First give a title and description to your view. This is done by including a metadata.xml file in the view directory:
<?xml version="1.0" encoding="utf-8"?>
<metadata>
<view title="CAST_VIEW">
<message><![CDATA[CAST_VIEW_DESC]]></message>
</view>
</metadata>
The 'title' element is shown as the 1st level entry under your component name when a new menu item is created. The 'message' element is the descriptive tool tip that is shown when the mouse hovers over the 'title' element. The example is using JText placeholders for the title and message. Just add the corresponding tags to your language definition files. You did create them didn't you?

Hiding a view from the menu

If you don't want this view to be selectable for a new menu item, then add the hidden="true" parameter to the view element (e.g. <view title="CAST_VIEW" hidden="true">). You can also hide a view by using an underscore '_' in the name. Be careful with naming your views or you can spend hours trying to figure out why it doesn't appear in the new menu item tree!

Template names, description, parameters

You can also create .xml files for the individual files in the tmpl folders. This is a cool way to have a name other than 'default' show up in the new menu item tree. Make sure that the name of the .xml file is the same as the name of your template file. I.E. if your file name is default.php, then your xml file name is default.xml. Its probably more productive to name your template files something other than default. That way, when you have 10 of them open in your favorite IDE, you won't get totally confused as to which file you are editing. That also avoids the nightmare of uploading the wrong file to the wrong directory!
These will also be hidden by including an underscore '_' in their name. This is a bit of a gotcha. Also ensure the filenames for these and the layotus are in lower case. CamelCase does not work for these.
Note: To use a temp/layout file name other than 'default', pass a configuration array to the __construct function parent in the view.html file. EG:
        function __construct()  {
$config = array();
$config['layout'] = 'bookslayout';
parent::__construct($config);
}
The .xml file for the template can be very simple with just a title and message or very complex with basic and/or advanced parameters:
<?xml version="1.0" encoding="utf-8"?>
<metadata>
<layout title="STANDARD_CATEGORY_LAYOUT">
<message>
<![CDATA[STANDARD_CATEGORY_LAYOUT DESC]]>
</message>
</layout>
<state>
<name>STANDARD_CATEGORY_LAYOUT</name>
<description>STANDARD_CATEGORY_LAYOUT_DESC</description>
<url>
</url>
<params>
</params>
<advanced>
</advanced>
</state>
</metadata>
See components\com_content\views\frontpage\tmpl\default.xml, or components\com_content\views\section\tmpl\default.xml for examples of how to specify both basic and advanced parameters for your views. See also Standard parameter types for information on defining parameters.
You can also add the hidden="true" to the layout element if you don't want this particular template file to be selected as a new menu item.

Final touches

Remember to create JText language definitions for your titles and messages so they can be easily translated. Have fun giving your views cool names and descriptions for that final bit of polish on your custom component!

Creating a toolbar for your component


Note: This applies when using the MVC structure.
By default the Joomla Toolbar uses a Delete, Edit, New, Publish, Unpublish and Parameters buttons. To use them in your component you must write the following code in the view.html.php of your view file (below your display function).
      JToolBarHelper::title( 'your_component_view_page_title', 'generic.png' );
JToolBarHelper::deleteList(); // Will call the task/function "remove" in your controller
JToolBarHelper::editListX(); // Will call the task/function "edit" in your controller
JToolBarHelper::addNewX(); // Will call the task/function "add" in your controller
JToolBarHelper::publishList(); // Will call the task/function "publish" in your controller
JToolBarHelper::unpublishList(); // Will call the task/function "unpublish" in your controller
JToolBarHelper::preferences('your_component_xml_file', 'height');
So (i.e.) your code would look like this:
 class HelloViewHellos extends JView
{
function display($tpl = null) {
global $mainframe, $option;
JToolBarHelper::title( 'Hello Component', 'generic.png' );
JToolBarHelper::deleteList();
JToolBarHelper::editListX();
JToolBarHelper::addNewX();
JToolBarHelper::publishList();
JToolBarHelper::unpublishList();
JToolBarHelper::preferences('com_hello', '500');
An example of creating a custom button in the admin list of records of your component Joomla 3.1 could be:

administrator/views/hellos/view.html.php
        public function display($tpl = null)
{
 
$this->state = $this->get('State');
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
 
// Check for errors.
if (count($errors = $this->get('Errors'))) {
JError::raiseError(500, implode("\n", $errors));
return false;
}
 
$this->addToolbar();
 
$this->sidebar = JHtmlSidebar::render();
parent::display($tpl);
 
}
 
protected function addToolbar()
{
// assuming you have other toolbar buttons ...
 
JToolBarHelper::custom('hellos.extrahello', 'extrahello.png', 'extrahello_f2.png', 'Extra Hello', true);
 
}
administrator/controllers/hellos.php
    public function extrahello()
{
 
// Get the input
$input = JFactory::getApplication()->input;
$pks = $input->post->get('cid', array(), 'array');
 
// Sanitize the input
JArrayHelper::toInteger($pks);
 
// Get the model
$model = $this->getModel();
 
$return = $model->extrahello($pks);
 
// Redirect to the list screen.
$this->setRedirect(JRoute::_('index.php?option=com_hello&view=hellos', false));
 
}
administrator/models/hello.php (note: the singular model)
    public function extrahello($pks)
{
 
// perform whatever you want on each item checked in the list
 
return true;
 
}