J3.x:Developing an MVC Component/Adding Custom Fields
From Joomla! Documentation
This tutorial is part of the Developing an MVC Component for Joomla! 3.2 tutorial. You are encouraged to read the previous parts of the tutorial before reading this.
In this part, we will cover the various aspects of enabling custom fields to be added to our helloworld component.
Introduction
Joomla 3.7 included the capability to define custom fields associated with some of the core Joomla components, and this feature was designed in a way that could be easily included in custom components. If you're not familiar with custom fields it's worth going through the Adding custom fields tutorial and experimenting using the custom fields available with com_content and com_contact for example.
Functionality
Here is a list of the functional changes for this step.
Admin Field Definition
We provide administrators with a way of defining the custom fields which can be added to our helloworld records. We do this by adding "Fields" and "Field Groups" to the sidebar submenu which was built in the Adding Categories step.
We'll also allow admins to set custom fields against our helloworld categories – so that when they click on Fields in the sidebar submenu there's a context dropdown to the left of the filter fields at the top.
Admin Setting Field Values
Once the fields are defined they are available to be used for our helloworld component. But we have to extend our "edit" form (which enables adding and editing helloworld records) to allow admins to enter values of the custom fields associated with each helloworld record.
We also want to allow admins to set values in custom fields of helloworld categories.
Admin Field Permissions
We define the permissions relating to managing Fields and Field Groups for our component. This then enables suitably authorised admins to define which user groups can edit fields and field groups, or enter data for custom fields.
Front-end Display
We extend the Hello World front end page to display the custom field values associated with that helloworld record.
Front-end Form
We extend the front-end form (which allows a user to create a new helloworld record) so that data for the associated custom fields can be captured as well.
Approach
Much of the development to include custom fields mirrors what we had to do to incorporate categories in Adding Categories. The work is outlined in Implementing Custom Fields in your component but there are some differences between that page and what we have to do.
Admin Field Definition
In the Adding Categories step we added support to allow administrators to set up helloworld categories by creating the sidebar submenu and inserting a Categories link there which pointed to the com_categories functionality. We passed a URL parameter "extension=com_helloworld" to indicate that these were to be categories for our component.
When an administrator clicks on Categories then the helloworld categories are shown and functionality is made available to add / edit / delete etc them. The categories are stored in the #__categories table, which is "partitioned" by component, via the extension column which denotes the component to which the category belongs.
In the same way we add into the sidebar submenu a link to the functionality for managing custom Fields, and one for custom Field Groups. We add a URL parameter "context=com_helloworld.helloworld" to indicate that these records belong to our component, and that they're associated with a "section" called "helloworld". (What we set for this "section" part is important, as will be seen below).
When an administrator clicks on one of these links, then the com_fields functionality is run, which displays the existing Fields or Field Groups and allows the administrator to manage them. The custom fields are stored in #__fields and the custom field groups in #__fields_groups, and each table is "partitioned" via the context field.
However, we have an additional feature available here because we can set custom fields against helloworld categories as well as helloworld records. If in the Joomla admin back-end you look at Content / Fields then to the left of the filter fields at the top there's a dropdown which can be set to Articles or Category, and this defines what the custom fields are being defined for. This com_fields functionality (which is running here) makes a call back into the same helper file as our submenu is defined in, and looks for a static method getContexts() which it calls to find the text and associated context values for this dropdown.
So we will defined 2 contexts "com_helloworld.helloworld" for custom fields for the helloworld records and "com_helloworld.category" for custom fields for the helloworld categories.
Also note that when an administrator defines a custom field (no matter whether it's for an item or a category) he/she can restrict the categories to which it should apply, and these custom field id / category id combinations are stored in the #__fields_categories table.
In summary, we have only 1 file to change, but the explanation for why we need the validateSections() method is in the XXX section below.
admin/helpers/helloworld.php
<?php
/**
* @package Joomla.Administrator
* @subpackage com_helloworld
*
* @copyright Copyright (C) 2005 - 2015 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 component helper.
*
* @param string $submenu The name of the active view.
*
* @return void
*
* @since 1.6
*/
abstract class HelloWorldHelper extends JHelperContent
{
/**
* Configure the Linkbar.
*
* @return Bool
*/
public static function addSubmenu($submenu)
{
JHtmlSidebar::addEntry(
JText::_('COM_HELLOWORLD_SUBMENU_MESSAGES'),
'index.php?option=com_helloworld',
$submenu == 'helloworlds'
);
JHtmlSidebar::addEntry(
JText::_('COM_HELLOWORLD_SUBMENU_CATEGORIES'),
'index.php?option=com_categories&view=categories&extension=com_helloworld',
$submenu == 'categories'
);
// Set some global property
$document = JFactory::getDocument();
$document->addStyleDeclaration('.icon-48-helloworld ' .
'{background-image: url(../media/com_helloworld/images/tux-48x48.png);}');
if ($submenu == 'categories')
{
$document->setTitle(JText::_('COM_HELLOWORLD_ADMINISTRATION_CATEGORIES'));
}
if (JComponentHelper::isEnabled('com_fields'))
{
JHtmlSidebar::addEntry(
JText::_('JGLOBAL_FIELDS'),
'index.php?option=com_fields&context=com_helloworld.helloworld',
$submenu == 'fields.fields'
);
JHtmlSidebar::addEntry(
JText::_('JGLOBAL_FIELD_GROUPS'),
'index.php?option=com_fields&view=groups&context=com_helloworld.helloworld',
$submenu == 'fields.groups'
);
}
}
/**
* Get the actions
*/
public static function getActions($component = '', $section = '', $messageId = 0)
{
$result = new JObject;
if (empty($messageId)) {
$assetName = 'com_helloworld';
}
else {
$assetName = 'com_helloworld.message.'.(int) $messageId;
}
$actions = JAccess::getActions('com_helloworld', 'component');
foreach ($actions as $action) {
$value = JFactory::getUser()->authorise($action->name, $assetName);
$result->set($action->name, $value);
}
return $result;
}
public static function validateSection($section, $item)
{
if (JFactory::getApplication()->isClient('site') && $section == 'form')
{
return 'helloworld';
}
if ($section != 'helloworld' && $section != 'form')
{
return null;
}
return $section;
}
}
Admin Setting Field Values
Once the custom fields are defined, we need to change our helloworld edit screen so that it allows an admin to specify values for the custom fields for that helloworld record. As we have seen with Joomla forms, there are 3 stages
- Setting up the form structure in XML
- Rendering the form fields, ie creating html from the fields in the XML form structure.
- Handling the POST data – performing field validation and saving the data
These same 3 stages apply with our custom fields.
Setting the XML form structure
Our admin edit form is built in our admin helloworld model (in admin/models/helloworld.php) and our model class HelloWorldModelHelloWorld inherits (via AdminModel) from FormModel. Once the form is built in XML in memory the FormModel::preprocessForm() method triggers a Joomla event onContentPrepareForm, which is picked up by the System Fields Plugin. It is this plugin which reads the relevant custom fields for our component and builds them dynamically into the form structure (in a similar way to our changes to preprocessForm() code in Adding Associations).
The key it uses to find the custom fields in the #__fields table is the name of the form, and as our form is called "com_helloworld.helloworld" we must have this as the context for our custom fields. This is why we need to have the section part of the context above set to "helloworld" - to match the name of our admin form. (The form name is set in our HelloWorldModelHelloWorld::getForm() method, where we call $this->loadForm(), passing the form name as the first parameter).
Also (as mentioned above) custom fields can be restricted to certain categories, and the System Fields Plugin code handles that too. If it finds a field in our form XML which is called "catid" then it assumes that this is the id of the category record and it excludes custom fields which are restricted to other categories. It also adds an HTML onChange trigger on the category field, so that if the administrator changes the category, then this triggers an HTML POST to the server with the task set to section . "reload" (hence in our case "helloworld.reload") and the current values of the form input elements. The HTML page is reloaded, but this time the System Fields Plugin uses the category id saved in the form fields, and adds the custom fields associated with the updated category.
So provided we match our form name and custom fields context, and name our category id field as "catid", our custom fields are built into our form XML structure for no additional effort on our part.
Rendering the Custom Fields
The second aspect is to render the fields, and to do this Joomla provides a standard layout file in layouts/joomla/edit/params.php. This will output the custom fields in separate tabs of the edit form, one tab for each Field Group, plus one tab for Fields which are not in a Field Group. The layout will actually render more fields than this, including the parameters for the component, so we have to pass to the layout the tabs which we don't want it to handle. So the only change necessary is to include 2 extra lines in our layout file:
<?php $this->ignore_fieldsets = array('details', 'image-info', 'params', 'item_associations', 'accesscontrol'); ?>
<?php echo JLayoutHelper::render('joomla.edit.params', $this); ?>
We also need to consider editing our helloworld categories, and including the custom fields associated with categories in the category edit form. However, all this is automatically handled for us by Joomla, and there's no additional work to do.
Hence our only code change here is to add the additional lines into our layout file:
admin/views/helloworld/tmpl/edit.php
<?php
/**
* @package Joomla.Administrator
* @subpackage com_helloworld
*
* @copyright Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
// No direct access
defined('_JEXEC') or die('Restricted access');
JHtml::_('behavior.formvalidator');
// The following is to enable setting the permission's Calculated Setting
// when you change the permission's Setting.
// The core javascript code for initiating the Ajax request looks for a field
// with id="jform_title" and sets its value as the 'title' parameter to send in the Ajax request
JFactory::getDocument()->addScriptDeclaration('
jQuery(document).ready(function() {
greeting = jQuery("#jform_greeting").val();
jQuery("#jform_title").val(greeting);
});
');
// Required for proper display of fields generated by com_associations
JHtml::_('formbehavior.chosen', 'select');
// if &tmpl=component used on first invocation, ensure it's on subsequent ones too
$input = JFactory::getApplication()->input;
$tmpl = $input->getCmd('tmpl', '') === 'component' ? '&tmpl=component' : '';
?>
<form action="<?php echo JRoute::_('index.php?option=com_helloworld&layout=edit' . $tmpl . '&id=' . (int) $this->item->id); ?>"
method="post" name="adminForm" id="adminForm" class="form-validate">
<input id="jform_title" type="hidden" name="helloworld-message-title"/>
<div class="form-horizontal">
<?php echo JHtml::_('bootstrap.startTabSet', 'myTab', array('active' => 'details')); ?>
<?php echo JHtml::_('bootstrap.addTab', 'myTab', 'details',
empty($this->item->id) ? JText::_('COM_HELLOWORLD_TAB_NEW_MESSAGE') : JText::_('COM_HELLOWORLD_TAB_EDIT_MESSAGE')); ?>
<fieldset class="adminform">
<legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_DETAILS') ?></legend>
<div class="row-fluid">
<div class="span3">
<?php echo $this->form->renderFieldset('details'); ?>
</div>
<div class="span9">
<?php echo $this->form->getInput('description'); ?>
</div>
</div>
</fieldset>
<?php echo JHtml::_('bootstrap.endTab'); ?>
<?php echo JHtml::_('bootstrap.addTab', 'myTab', 'image', JText::_('COM_HELLOWORLD_TAB_IMAGE')); ?>
<fieldset class="adminform">
<legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_IMAGE') ?></legend>
<div class="row-fluid">
<div class="span6">
<?php echo $this->form->renderFieldset('image-info'); ?>
</div>
</div>
</fieldset>
<?php echo JHtml::_('bootstrap.endTab'); ?>
<?php echo JHtml::_('bootstrap.addTab', 'myTab', 'params', JText::_('COM_HELLOWORLD_TAB_PARAMS')); ?>
<fieldset class="adminform">
<legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_PARAMS') ?></legend>
<div class="row-fluid">
<div class="span6">
<?php echo $this->form->renderFieldset('params'); ?>
</div>
</div>
</fieldset>
<?php echo JHtml::_('bootstrap.endTab'); ?>
<?php if (JLanguageAssociations::isEnabled()) : ?>
<?php echo JHtml::_('bootstrap.addTab', 'myTab', 'associations', JText::_('COM_HELLOWORLD_TAB_ASSOCIATIONS')); ?>
<fieldset class="adminform">
<legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_ASSOCIATIONS') ?></legend>
<div class="row-fluid">
<div class="span12">
<?php echo JLayoutHelper::render('joomla.edit.associations', $this); ?>
</div>
</div>
</fieldset>
<?php echo JHtml::_('bootstrap.endTab'); ?>
<?php endif; ?>
<?php echo JHtml::_('bootstrap.addTab', 'myTab', 'permissions', JText::_('COM_HELLOWORLD_TAB_PERMISSIONS')); ?>
<fieldset class="adminform">
<legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_PERMISSIONS') ?></legend>
<div class="row-fluid">
<div class="span12">
<?php echo $this->form->renderFieldset('accesscontrol'); ?>
</div>
</div>
</fieldset>
<?php echo JHtml::_('bootstrap.endTab'); ?>
<?php $this->ignore_fieldsets = array('details', 'image-info', 'params', 'item_associations', 'accesscontrol'); ?>
<?php echo JLayoutHelper::render('joomla.edit.params', $this); ?>
<?php echo JHtml::_('bootstrap.endTabSet'); ?>
</div>
<input type="hidden" name="task" value="helloworld.edit" />
<?php echo JHtml::_('form.token'); ?>
</form>
Handling the POST data
When the form is submitted the data is sent to the server in an HTTP POST request, with the data for custom fields included with the data for standard fields in the jform[] array. Custom fields are transferred as elements of a jform[com_fields] array. Our server code must validate the data and then save it.
Standard practice within Joomla is to add any validation aspects to the fields when the form structure is created. So when the POST is received this form is once again built, and then the validation routines run using the $form->validate(), and there is no additional work for us.
The saving of data is handled in the AdminController save() method. After the data is stored in the component's table this method emits the event onContentAfterSave, passing the context, the item being edited and the validated data. This event is picked up by the System Fields Plugin, which handles the saving of the custom fields, and once again there is nothing for us to do.
Admin Field Permissions
We have to define the types of permissions which relate to custom fields. In general the ability to create a custom field or fieldgroup associated with a component is defined by whether the user can create an item of that component. So if a user can create an article, then the same user can create a com_content custom field and a com_content custom fieldgroup. We can't change this for our component.
In terms of the Joomla assets (where permissions are stored), the hierarchy is Component > Custom Fieldgroup > Custom Field, so here we must define permissions at the Fieldgroup level (which relate to a fieldgroup and fields within that fieldgroup) and permissions at the Field level (which relate to that field). So for example, the setting of "core.create" for a specific fieldgroup controls the creation of custom fields within that fieldgroup. Because we have now introduced a "core.edit.value" at the fieldgroup and field level (associated with being able to set a value in a custom field or fieldgroup), we must now also have this permission at the higher "component" level.
For our component we use the standard permissions used within Joomla core components, and our access.xml file is now:
admin/access.xml
<?xml version="1.0" encoding="utf-8" ?>
<access component="com_helloworld">
<section name="component">
<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
<action name="core.create" title="JACTION_CREATE" description="JACTION_CREATE_COMPONENT_DESC" />
<action name="core.delete" title="JACTION_DELETE" description="JACTION_DELETE_COMPONENT_DESC" />
<action name="core.edit" title="JACTION_EDIT" description="JACTION_EDIT_COMPONENT_DESC" />
<action name="core.edit.state" title="JACTION_EDITSTATE" description="JACTION_EDITSTATE_COMPONENT_DESC" />
<action name="core.edit.own" title="JACTION_EDITOWN" description="JACTION_EDITOWN_COMPONENT_DESC" />
<action name="core.edit.value" title="JACTION_EDITVALUE" description="JACTION_EDITVALUE_COMPONENT_DESC" />
</section>
<section name="category">
<action name="core.create" title="JACTION_CREATE" description="COM_CATEGORIES_ACCESS_CREATE_DESC" />
<action name="core.delete" title="JACTION_DELETE" description="COM_CATEGORIES_ACCESS_DELETE_DESC" />
<action name="core.edit" title="JACTION_EDIT" description="COM_CATEGORIES_ACCESS_EDIT_DESC" />
<action name="core.edit.state" title="JACTION_EDITSTATE" description="COM_CATEGORIES_ACCESS_EDITSTATE_DESC" />
<action name="core.edit.own" title="JACTION_EDITOWN" description="COM_CATEGORIES_ACCESS_EDITOWN_DESC" />
</section>
<section name="message">
<action name="core.delete" title="JACTION_DELETE" description="COM_HELLOWORLD_ACCESS_DELETE_DESC" />
<action name="core.edit" title="JACTION_EDIT" description="COM_HELLOWORLD_ACCESS_EDIT_DESC" />
</section>
<section name="fieldgroup">
<action name="core.create" title="JACTION_CREATE" description="COM_FIELDS_GROUP_PERMISSION_CREATE_DESC" />
<action name="core.delete" title="JACTION_DELETE" description="COM_FIELDS_GROUP_PERMISSION_DELETE_DESC" />
<action name="core.edit" title="JACTION_EDIT" description="COM_FIELDS_GROUP_PERMISSION_EDIT_DESC" />
<action name="core.edit.state" title="JACTION_EDITSTATE" description="COM_FIELDS_GROUP_PERMISSION_EDITSTATE_DESC" />
<action name="core.edit.own" title="JACTION_EDITOWN" description="COM_FIELDS_GROUP_PERMISSION_EDITOWN_DESC" />
<action name="core.edit.value" title="JACTION_EDITVALUE" description="COM_FIELDS_GROUP_PERMISSION_EDITVALUE_DESC" />
</section>
<section name="field">
<action name="core.delete" title="JACTION_DELETE" description="COM_FIELDS_FIELD_PERMISSION_DELETE_DESC" />
<action name="core.edit" title="JACTION_EDIT" description="COM_FIELDS_FIELD_PERMISSION_EDIT_DESC" />
<action name="core.edit.state" title="JACTION_EDITSTATE" description="COM_FIELDS_FIELD_PERMISSION_EDITSTATE_DESC" />
<action name="core.edit.value" title="JACTION_EDITVALUE" description="COM_FIELDS_FIELD_PERMISSION_EDITVALUE_DESC" />
</section>
</access>