Joomla Forms and Form Fields
|
This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production. This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages. |
Almost everything an editor types into Joomla goes through a form: the article editor, the module and plugin
parameters, the global configuration, the registration page, the Smart Search filters and every custom component screen.
Joomla describes a form in an XML file, turns the XML into a Form object that holds a list of field objects, and uses
that one description three times: to render the HTML, to filter and validate what the browser posts back, and to let
plugins add or remove fields without touching the component. This page was checked on 2026-10-30 against Joomla 6.2.x
(the Developer Manual for 6.1 and the 6.2-dev source tree) and PHP 8.3 or later.
The reader is mostly a developer of com_tallpine or of a plugin that extends someone else’s form, but a site
builder meets the same vocabulary in the field options of the custom-field screens (see
Joomla custom fields), and a themer meets it when overriding the layouts under
html/layouts/joomla/form/field (see Template overrides and alternative layouts).
The running example is the Tallpine Hiking Club trail report: the form behind the administrator edit screen of the
trail entity of com_tallpine, which has a title, a category, a distance, a difficulty grade, a season, a repeatable
list of waypoints and an optional closure period.
Three facts frame everything below.
-
Joomla ships 80 standard field types (the Developer Manual lists exactly 80 in its 6.1 documentation), plus the field types supplied by
fieldsplugins for custom fields. Date and Datetime are new in 6.0 (the classes carry@since 6.0.0), next to the older Calendar. -
Filtering and validation are two separate, server-side steps, and client-side validation is only a convenience on top of them. The browser can always be bypassed with
curl. -
A form is not one page’s business. Plugins that listen to
onContentPrepareFormcan change any form that passes through the model layer, so a form definition is part of a component’s public extension surface.
Anatomy of a form XML file
A form file has a <form> root, optional <fieldset> containers and <field> elements. By convention a component keeps
its form definitions in administrator/components/com_tallpine/forms/, and site-side forms in
components/com_tallpine/forms/. Here is the form for the Tallpine trail:
<?xml version="1.0" encoding="UTF-8"?>
<form addfieldprefix="Tallpine\Component\Tallpine\Administrator\Field"
addruleprefix="Tallpine\Component\Tallpine\Administrator\Rule"
addfilterprefix="Tallpine\Component\Tallpine\Administrator\Filter">
<fieldset name="details" label="COM_TALLPINE_FIELDSET_DETAILS">
<field
name="id"
type="number"
label="JGLOBAL_FIELD_ID_LABEL"
default="0"
readonly="true"
class="readonly"
/>
<field
name="title"
type="text"
label="JGLOBAL_TITLE"
required="true"
maxlength="255"
hint="COM_TALLPINE_FIELD_TITLE_HINT"
/>
<field
name="alias"
type="text"
label="JFIELD_ALIAS_LABEL"
hint="JFIELD_ALIAS_PLACEHOLDER"
/>
<field
name="catid"
type="category"
label="JCATEGORY"
extension="com_tallpine"
required="true"
/>
<field
name="distance_km"
type="number"
label="COM_TALLPINE_FIELD_DISTANCE_LABEL"
description="COM_TALLPINE_FIELD_DISTANCE_DESC"
min="0.5"
max="120"
step="0.5"
validate="number"
required="true"
/>
<field
name="difficulty"
type="grade"
label="COM_TALLPINE_FIELD_DIFFICULTY_LABEL"
default="easy"
/>
<field
name="published"
type="list"
label="JSTATUS"
default="1"
class="form-select-color-state"
validate="options"
>
<option value="1">JPUBLISHED</option>
<option value="0">JUNPUBLISHED</option>
<option value="2">JARCHIVED</option>
<option value="-2">JTRASHED</option>
</field>
<field
name="closed"
type="radio"
label="COM_TALLPINE_FIELD_CLOSED_LABEL"
layout="joomla.form.field.radio.switcher"
default="0"
filter="integer"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="closed_from"
type="date"
label="COM_TALLPINE_FIELD_CLOSED_FROM_LABEL"
showon="closed:1"
/>
<field
name="closure_reason"
type="textarea"
label="COM_TALLPINE_FIELD_CLOSURE_REASON_LABEL"
rows="3"
filter="safehtml"
showon="closed:1"
/>
</fieldset>
<fields name="params">
<fieldset name="display" label="COM_TALLPINE_FIELDSET_DISPLAY">
<field
name="show_map"
type="radio"
label="COM_TALLPINE_FIELD_SHOW_MAP_LABEL"
layout="joomla.form.field.radio.switcher"
default="1"
>
<option value="0">JHIDE</option>
<option value="1">JSHOW</option>
</field>
</fieldset>
</fields>
</form>
Source: Joomla Manual: How forms work
The structure rewards a close reading.
<form> is the root. The prefix attributes addfieldprefix, addruleprefix, addfilterprefix (and addformprefix)
may sit on the root, on a <fieldset>, on a <fields> group or on a single <field>. They register a PHP namespace
prefix with FormHelper, which then looks for <Prefix>\<Name>Field, <Prefix>\<Name>Rule or
<Prefix>\<Name>Filter when it meets a type, a validate name or a filter name it does not know. The prefix list
starts with the core namespaces (Joomla\CMS\Form\Field, Joomla\CMS\Form\Rule, Joomla\CMS\Form\Filter) and each added
prefix is put in front of them, so a component can shadow a core type by reusing its name. Prefixes are global to the
request: once added they remain for every later form, which is convenient and a source of surprises in long-lived
processes.
<fieldset> groups fields for display. It carries a name and a label (a language key) and, optionally,
description. It does not change the structure of the posted data. Tabs in the standard administrator layouts are
built by iterating getFieldsets() (see LayoutHelper and joomla.edit.params in
Component MVC in the administrator).
<fields name="params"> is a field group. It does change the data shape: every field inside is named
jform[params][show_map] in the HTML and nested as params.show_map in the bound array. This is how Joomla stores a
JSON params column in a single database field. Groups nest, and Joomla joins the nested names with dots when it
filters and validates.
<field> has two mandatory attributes, name and type. The type is resolved case-insensitively to a class, so
type="grade" finds GradeField in the registered prefixes. A field without type becomes a text field.
The <option> children of list, radio, checkboxes and similar types give the choices. Language keys in the
label, description, hint and option text are translated for you (see
Language files and internationalisation); use
translate_label="false" or translate_description="false" to switch it off.
Common field attributes
Many attributes map straight to HTML (size, maxlength, rows, cols, readonly, disabled, autofocus,
onchange, data-*). The Manual explains the less obvious ones, and the base class FormField::__set documents the
general set. The table summarises the attributes a component author uses constantly.
| Attribute | Meaning |
|---|---|
|
The key in the posted and bound data. It must be unique within its field group. In the HTML it becomes
|
|
The field type ( |
|
Language keys. Since Joomla 4 the description is shown as inline help below the field; before that it was a tooltip.
The administrator toolbar button Toggle Inline Help shows or hides it. A component enables the button with
|
|
The initial value. Data bound to the form always overrides it, so a re-displayed invalid form keeps what the user typed (see "Loading and binding"). |
|
The browser blocks an empty submission, and the server-side |
|
|
|
The HTML |
|
The CSS class of the input. Joomla also reads |
|
A regular expression used by the browser for |
|
How the posted value is cleaned (see "Filters"). |
|
The server-side rule name (see "Validation rules"). |
|
Show the field only when another field has a given value (see "Conditional display with showon"). |
|
Overrides the layout that renders the field, for example |
|
Register a namespace for custom classes. |
The life of a form
The Form class (Joomla\CMS\Form\Form) is what you hold in PHP. A model, a controller or a plugin obtains one from the
form factory, loads XML into it, binds data to it, and asks it to render, filter or validate. Understanding the order
of those steps explains most "my field disappeared" and "my value is ignored" reports.
The sequence shows that there are two Form instances in a typical edit-and-save round trip, one per request. The form that renders the page is gone when the POST arrives, so the controller asks the model for a fresh one and the model rebuilds it with the same XML and the same plugin changes. This is why a plugin that adds a field must do it on every request, not just when the edit page is displayed.
Creating a form
Since Joomla 4 the supported way to create a Form is the form factory service, FormFactoryInterface, rather than
Form::getInstance(). A form needs a name that is unique among the forms on the page and an options array whose
most important key is control, the prefix of the input names:
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Site\Service;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormFactoryInterface;
final class ReportFormBuilder
{
public function __construct(private readonly FormFactoryInterface $formFactory)
{
}
public function build(): Form
{
$form = $this->formFactory->createForm('com_tallpine.report', ['control' => 'jform']);
$form->loadFile(JPATH_COMPONENT_SITE . '/forms/report.xml');
return $form;
}
}
Source: Joomla Manual: How forms work
loadFile() reads the XML into a SimpleXMLElement and calls load(); both accept $reset / $replace (when a field
with the same group and name already exists, true replaces it and false keeps the old one) and an $xpath to import
only a part of the document. The com_menus component uses $xpath to add menu-item options from layout XML files. In
code that is not inside a model you can obtain the factory from the container,
Factory::getContainer()->get(FormFactoryInterface::class), but constructor injection (see
Namespaces, autoloading and dependency injection)
is better, and MVCFactory already hands every model a form factory.
Older tutorials and the Joomla 3-era JForm examples write JForm::getInstance('sample', $path, ['control' => 'myform']).
Form::getInstance() still exists and works, and the Manual’s sample components use it, but it keeps instances in a static
cache keyed by name, which makes tests and multiple instances awkward. The J* class aliases are available only through
the Behaviour - Backward Compatibility 6 plugin in 6.x, so new code should import the namespaced class.
Binding, rendering and reading values
bind($data) stores an array or object inside the Form instance. At render time the stored value for a field name
overrides the default attribute. setValue($name, $group, $value) changes a single value after binding and is the right
tool when a plugin must force a value, because changing the default attribute would be overridden by bound data.
$form->bind(['title' => 'Cima Verde loop', 'distance_km' => 12.5]);
// one field: label + input wrapped in the layout that the field type chooses
echo $form->renderField('title');
// a whole fieldset (all fields of 'details')
echo $form->renderFieldset('details');
// pieces, for custom markup
echo $form->getLabel('distance_km');
echo $form->getInput('distance_km');
// values and metadata
$distance = $form->getValue('distance_km');
$groups = $form->getFieldsets();
$params = $form->getGroup('params');
renderField() produces a label, an input and a wrapping <div> using the joomla.form.renderfield layout, which a
template can override. Remember that the output must sit inside a <form> element with method="post" and a submit
control; a form object renders fields, not the form tag.
Filtering, validating and post-processing
Form::process($data) is the one-call version of the server-side pipeline, and it is what FormModel::validate() calls:
-
filter($data)runs each field’sfilteron the value that was posted and builds a new, clean array. Fields missing from the input are not added. A field whosesetup()returnsfalse(the field decides not to render, for example because the user lacks a capability) is dropped from the output. -
validate($data)checks the filtered data field by field: the disabled-field check, therequiredcheck and thevalidaterule. Errors are collected in the form and returned bygetErrors(). -
postProcess($data)gives each field type a last chance to transform the value, for example to turn a list of subform rows into the stored structure.
A direct use, outside a model, looks like this (it is the pattern the Manual’s sample components use):
$data = $app->getInput()->post->get('jform', [], 'array');
$filtered = $form->filter($data);
if (!$form->validate($filtered)) {
foreach ($form->getErrors() as $error) {
$app->enqueueMessage($error->getMessage(), 'error');
}
$form->bind($filtered); // keep what the user typed
return false;
}
Source: Joomla Manual: How forms work
Two details matter. validate() returns true or false and puts \Exception objects into getErrors(); it throws a
RuntimeException for a disabled field that was nevertheless posted, and it adds an error for a value that arrives for a
field that is not part of the rendered form. That second check is a deliberate defence against mass-assignment: the
server refuses data that the form definition would never have produced.
The 80 standard field types
The Developer Manual’s page Standard Form Fields lists every field type that ships in the Joomla\CMS\Form\Field
namespace. In the 6.1 documentation there are exactly 80. The groups below are by purpose; each name is the value of the
type attribute.
| Purpose | Field types |
|---|---|
Text and numbers |
|
Choices |
|
Date and time |
|
Files and media |
|
Content and structure |
|
Users and access |
|
Extensions and layout |
|
System |
|
Layout-only |
|
The grouping above is this page’s, not the Manual’s, which lists the fields alphabetically. The type names are the
Manual’s slugs; in particular the sorting field’s page is ordering (the Manual titles it "Order"), and the radio
field has a plain variant, radiobasic.
A few types deserve extra notes because they are used in nearly every component.
Text, number and telephone fields
text is the base for most input types and takes size, maxlength, hint, pattern, autocomplete, inputmode and
dirname. number and range accept min, max and step; integer generates a list of options from first to last
with a step, which is a select, not a text input. Joomla 6.0 also made note a form field: a read-only block of
formatted text that can carry a heading, a description and the close button, useful for explaining a group of options
inside an administrator screen without writing a layout.
Type email automatically adds validate-email for client-side checking and, with validate="email", the server-side
rule. Type telephone has no built-in server filter; use filter="tel" and validate="tel" explicitly.
Calendar, Date and Datetime
calendar is the long-standing date picker. It has format (a strftime-style pattern), showtime, timeformat,
todaybutton, weeknumbers, filltable, minyear and maxyear, and a filter attribute whose special values
user_utc and server_utc convert a date from the user’s or the server’s time zone to UTC on save. That conversion is
why a re-displayed form after a validation error needs the filtered value of a calendar field: the controller applies the
filter before storing posted data back in the session.
Joomla 6.0 added two more, based on the browser’s native pickers: date (<input type="date">, with min and max in
Y-m-d form) and datetime (<input type="datetime-local">, with min, max and a filter). In the source both classes
are marked @since 6.0.0. They suit simple "closed from" dates such as the Tallpine closure period, while calendar is
still the right choice when you need the Joomla time-zone filters, a custom format or the picker’s today and clear
buttons. Older books written for Joomla 3, 4 or 5, including the Joomla 5 development book Developing Extensions for
Joomla! 5 (Packt, 2023), predate these two types and know only calendar.
Category, tag and user fields
category lists categories of one extension (extension="com_tallpine"), accepts published, language, action
(only categories on which the user has an ACL action such as core.create), show_root and allowAdd for the inline
"create new" option. tag is a fancy-select tag input with mode="ajax" or mode="nested", custom="allow" and
multiple. user renders a modal user picker and usergrouplist and accesslevel render group and level selectors.
Most of these hit the database or ACL; they need the correct component context, which is why the same XML works in the
administrator edit form, a custom field and a module parameter.
Media, file and editor fields
media opens the Media Manager (see Media Manager) with types, preview,
directory and schemes attributes; Joomla 6.1 added audio, video and document variants of the media field, described
in the custom-fields page. accessiblemedia adds the alternative-text and caption inputs. file is a real
<input type="file"> that requires enctype="multipart/form-data" on the <form>, an accept attribute for the
browser and server-side checks with the upload helper; Joomla does not validate the file contents for you.
editor renders the editor selected by the user or by the field’s editor attribute, with buttons, hide and filter.
SQL and sorting fields
sql builds a list from a query. It takes query, key_field and value_field, and Joomla runs the query without
placeholders, so the XML author, not the user, must control it. ordering shows items in a sort order and needs the table
name and the order column. A sql field whose query contains the current user’s input is an injection hole: write a custom
ListField with a bound query instead (see below).
Filters
A filter cleans a value before validation. Every field has the filter attribute; the rules are in the filter()
method of FormField.
-
With no
filterattribute a text-like field passes its value through Joomla’s defaultInputFilter, which removes unsafe tags and attributes. The hard-coded defaults are not configurable in the Global Configuration. This is the default when you do nothing, and it is much safer thanraw. -
A
filterattribute replaces the default completely. A value that is empty and not required returns an empty string without running the filter. -
A filter value containing
::calls a static PHP method,filter="Tallpine\Helper\TrailHelper::normaliseSlug". The method receives the value. Joomla 6 still resolves a legacyJComponentHelper::...call only through the BC plugin and throws an exception otherwise. -
A filter that names a class registered via
addfilterprefixis loaded and itsfilter()method is called. -
Otherwise a plain PHP function name such as
trimis called with the value. -
As the last step the name is handed to
InputFilter::clean($value, $filter), which understands the classic typesinteger,int,uint,float,bool,word,alnum,cmd,base64,string,html,path,username,arrayandraw.
The Manual documents a set of ready-made filter classes, available as filter="..." values:
| Filter | Effect |
|---|---|
|
Applies PHP |
|
No filtering at all. Use only where the stored data is never output unescaped, and only for fields that the current user is trusted to fill with markup. |
|
Filters permission ("rules") fields. |
|
Like the default filter but can be configured to block more tags. |
|
Normalises telephone number formats. |
|
Removes the value and returns |
|
Removes characters that are not valid in a URL. |
A custom filter
A custom filter is a class that implements Joomla\CMS\Form\FormFilterInterface, which has a single method. Tallpine
uses one to normalise the free-text alias of a trail:
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Filter;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormFilterInterface;
use Joomla\CMS\Filter\OutputFilter;
use Joomla\Registry\Registry;
\defined('_JEXEC') or die;
final class TrailAliasFilter implements FormFilterInterface
{
public function filter(
\SimpleXMLElement $element,
$value,
$group = null,
?Registry $input = null,
?Form $form = null
) {
$value = trim((string) $value);
if ($value === '' && $input !== null) {
// fall back to the title when the alias is left empty
$value = (string) $input->get('title', '');
}
return OutputFilter::stringURLSafe($value);
}
}
<field name="alias" type="text" label="JFIELD_ALIAS_LABEL"
filter="TrailAlias"
addfilterprefix="Tallpine\Component\Tallpine\Administrator\Filter" />
The filter value TrailAlias is resolved as <prefix>\TrailAliasFilter, because the loader appends Filter to the name.
The $input argument is the whole posted data set as a Registry, which lets a filter read other fields, as above.
Filters must be pure: they are called again whenever a controller needs the processed data of a failed submission.
Validation rules
A validation rule decides whether a filtered value is acceptable. It is named by the validate attribute and lives in
Joomla\CMS\Form\Rule (the core files are in libraries/src/Form/Rule). The server-side checks the Manual describes are
these.
| Rule | What it checks |
|---|---|
|
|
|
|
|
The answer of the configured CAPTCHA; optional |
|
Three or six hexadecimal characters. |
|
A valid CSS identifier or part of one. |
|
A valid address. Attributes: |
|
Compares with another field named in the |
|
The value exists in a table: |
|
A path that stays inside the Joomla folder; existence is not checked. |
|
The folder exists relative to the Joomla root. |
|
A valid module layout name. |
|
A float between optional |
|
The value is one of the field’s options. |
|
The value meets the site’s password requirements. |
|
Uses |
|
All posted permission actions are valid for this field. |
|
The |
|
Validates each subform field with its own rule. |
|
A telephone number; |
|
A valid time, limited by |
|
A valid URL with XSS checks; attributes |
|
A valid user id. |
|
A username that is unique in |
Two checks run regardless of the rule: a required field must have a value, and a disabled field must not be present in
the request. A failed rule shows the generic "Invalid field" message unless the field has a message attribute (a language
key), or the rule sets its own message.
The rule loader fills in services before it calls test(). A rule that implements DatabaseAwareInterface receives the
database through setDatabase() (the standard DatabaseAwareTrait provides it), and one that implements
CurrentUserInterface receives the user. That makes rules injectable without Factory::getDbo(), which no longer exists
in Joomla 6.
A custom rule with a regular expression
The shortest rule extends Joomla\CMS\Form\FormRule and sets $regex. This one accepts a GPX file name for the Tallpine
gpx-file field:
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Rule;
use Joomla\CMS\Form\FormRule;
\defined('_JEXEC') or die;
final class GpxFileNameRule extends FormRule
{
protected $regex = '^[a-z0-9][a-z0-9_-]{0,63}\.gpx$';
protected $modifiers = 'i';
}
FormRule::test() wraps the expression in non-printable delimiters, adds the u modifier when PCRE has Unicode
properties, and returns true when preg_match succeeds. An empty $regex throws an UnexpectedValueException.
A custom rule with a test method
When a regular expression is not enough, implement test(). The signature, in the 6.2 source, is
test(\SimpleXMLElement $element, $value, $group = null, ?Registry $input = null, ?Form $form = null). Return true for
a valid value and false otherwise. The rule below checks that a trail’s elevation gain is plausible for its distance, using
another field of the form; it also demonstrates reading a custom attribute from the XML element and setting a dynamic message.
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Rule;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormRule;
use Joomla\CMS\Language\Text;
use Joomla\Registry\Registry;
\defined('_JEXEC') or die;
final class ElevationRule extends FormRule
{
public function test(
\SimpleXMLElement $element,
$value,
$group = null,
?Registry $input = null,
?Form $form = null
) {
if ($value === '' || $value === null) {
return true; // 'required' is a separate check
}
if (!is_numeric($value) || (float) $value < 0) {
return false;
}
// custom attribute: <field ... metres_per_km="400" />
$limit = (float) ($element['metres_per_km'] ?? 400);
$distance = $input !== null ? (float) $input->get('distance_km', 0) : 0.0;
if ($distance > 0 && (float) $value > $distance * $limit) {
$message = Text::sprintf('COM_TALLPINE_ERROR_ELEVATION_IMPLAUSIBLE', (int) $value, $distance);
if (isset($element['message'])) {
$element->attributes()->message = $message;
} else {
$element->addAttribute('message', $message);
}
return false;
}
return true;
}
}
<field name="elevation_gain" type="number" label="COM_TALLPINE_FIELD_ELEVATION_LABEL"
min="0" step="10" validate="Elevation" metres_per_km="400"
addruleprefix="Tallpine\Component\Tallpine\Administrator\Rule" />
The rule name is resolved the same way as filters: Elevation becomes <prefix>\ElevationRule. The Manual’s own example
uses validate="custom" and CustomRule; the name is just a convention. The $group argument is the field-name group,
so with a group bar and a field foo the HTML name is bar[foo]. In a form that nests field groups, read other fields
from $input with their dotted path (params.show_map), because $input is a Registry.
A rule that must query the database follows the DI route. Because the form loader injects the database into any rule that
implements DatabaseAwareInterface, the class only has to use the trait:
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Rule;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormRule;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
\defined('_JEXEC') or die;
final class UniqueTrailRule extends FormRule implements DatabaseAwareInterface
{
use DatabaseAwareTrait;
public function test(
\SimpleXMLElement $element,
$value,
$group = null,
?Registry $input = null,
?Form $form = null
) {
$id = (int) ($input?->get('id') ?? 0);
$title = (string) $value;
$db = $this->getDatabase();
$query = $db->createQuery()
->select('COUNT(*)')
->from($db->quoteName('#__tallpine_trails'))
->where($db->quoteName('title') . ' = :title')
->where($db->quoteName('id') . ' <> :id')
->bind(':title', $title)
->bind(':id', $id, ParameterType::INTEGER);
return (int) $db->setQuery($query)->loadResult() === 0;
}
}
Notice the prepared statement: the posted title is never concatenated into the SQL. See Joomla database API for the query builder and Security in extensions for the injection checklist.
Rule and filter interplay
The pipeline order is fixed: filter, then validate. A filter can therefore mask an invalid value (an intarray filter
turns abc into an empty list that required then rejects, but integer turns abc into 0, which may pass).
Validation sees what the filter produced, so choose a filter that does not hide errors you want to report, and let a rule
reject what a filter cannot repair.
Client-side validation
Client-side validation gives instant feedback and nothing more. The Manual says plainly that it is "not sufficient to make your website secure". Three steps enable it:
-
Load the script through the Web Asset Manager (see Web Asset Manager):
$wa->useScript('form.validate'), which deliversmedia/system/js/fields/validate.js. -
Add the class
form-validateto the<form>element. -
Add a
validate-...class, or the matching field type, to each field.
<?php
// administrator/components/com_tallpine/tmpl/trail/edit.php
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
\defined('_JEXEC') or die;
/** @var \Tallpine\Component\Tallpine\Administrator\View\Trail\HtmlView $this */
$wa = $this->getDocument()->getWebAssetManager();
$wa->useScript('keepalive')
->useScript('form.validate');
?>
<form action="<?php echo Route::_('index.php?option=com_tallpine&layout=edit&id=' . (int) $this->item->id); ?>"
method="post" name="adminForm" id="adminForm" class="form-validate" enctype="multipart/form-data">
<?php echo $this->form->renderFieldset('details'); ?>
<input type="hidden" name="task" value="">
<?php echo HTMLHelper::_('form.token'); ?>
</form>
The built-in classes are validate-username, validate-password, validate-numeric and validate-email, whose regular
expressions are in media/system/js/fields/validate.js. A field with type="email" gets validate-email automatically
(but still needs the script and the form class), and required="true" makes the validator check that a value was entered.
A pattern="[^x]+" attribute on a text or telephone field adds a JavaScript regular expression, again only client-side.
Validation runs inside Joomla.submitbutton(task). Toolbar save buttons carry a form-validation class, so the helper calls
submitbutton with validation switched on; cancel buttons call it without. A hand-written submit button must call
Joomla.submitbutton('trail.apply') or an equivalent.
A custom client-side validator
The validator is a function registered with document.formvalidator.setHandler; the class on the field is validate- plus
the handler name. This one rejects Tallpine trail codes that are not of the form TP-123:
// media/com_tallpine/js/trail-code-validator.js
window.addEventListener('load', () => {
document.formvalidator.setHandler('trailCode', (value) => /^TP-\d{1,4}$/.test(value));
});
{
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
"name": "com_tallpine",
"version": "1.0.0",
"assets": [
{
"name": "com_tallpine.validate-trail-code",
"type": "script",
"uri": "com_tallpine/trail-code-validator.js",
"dependencies": ["form.validate"],
"attributes": { "defer": true },
"version": "1.0.0"
}
]
}
<field name="trail_code" type="text" label="COM_TALLPINE_FIELD_CODE_LABEL"
class="validate-trailCode"
data-validation-text="COM_TALLPINE_ERROR_TRAIL_CODE"
validate="TrailCode" />
The view then calls $wa->useScript('com_tallpine.validate-trail-code'). The data-validation-text attribute supplies the
message. Always pair a custom JavaScript validator with a server-side rule that enforces the same constraint (here a regex
rule), because the JavaScript is optional for an attacker.
Conditional display with showon
showon is a form attribute handled by a small piece of JavaScript (the showon script in media/system/js). It hides
a field in the browser until another field has a particular value.
| Syntax | Meaning |
|---|---|
|
Show when the field |
|
Show when |
|
Show when |
|
Show when |
|
Show when both conditions hold. |
|
Show when either condition holds. |
The names refer to field names in the same form and are resolved against the control and group, so inside the
params group, showon="show_map:1" finds jform[params][show_map]. For fields inside a subform, use the field id instead
of the name, as the Joomla User Guide describes for custom fields. showon is purely presentational; there is a showon validation rule that
only checks the syntax. The server still processes the hidden field if it arrives, so a field that must not be saved when
hidden needs server-side logic (for example a filter="unset" that is applied from prepareTable(), or clearing the value
in save()).
Custom field classes
When a standard field is not enough, write a PHP class. The base class is Joomla\CMS\Form\FormField (the file is
libraries/src/Form/FormField.php), and you can also extend any standard field, since they all extend FormField.
The class is found through the prefix registered with addfieldprefix: type grade plus the prefix
Tallpine\Component\Tallpine\Administrator\Field makes Joomla load Tallpine\Component\Tallpine\Administrator\Field\GradeField.
Setting the $type property (protected $type = 'Grade';) is still common and sets the value getName() and the
generated ids use, but the Manual notes that it is not required for namespaced classes.
The rendering protocol is:
-
setup(SimpleXMLElement $element, $value, $group)receives the XML element, the bound value and the group, copies the attributes into properties, and returnsfalseto make the field disappear. -
renderField()builds three parts, a label, an input and a wrapping<div>, by callinggetLabel()andgetInput()and passing them to thejoomla.form.renderfieldlayout. -
getInput()is the only method a minimal custom field has to implement; it returns an HTML string. -
getLabel()returns the label HTML and rarely needs overriding. -
Standard fields pass their local data (
$displayData) to layouts instead of echoing markup, soclass,descriptionand so on reach the template overrides.
A ListField subclass
The simplest and most useful custom field extends ListField and overrides getOptions(). The Tallpine difficulty grades
come from a fixed list, translated, with a "choose a grade" placeholder:
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Field;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die;
final class GradeField extends ListField
{
protected $type = 'Grade';
private const GRADES = [
'easy' => 'COM_TALLPINE_GRADE_EASY',
'moderate' => 'COM_TALLPINE_GRADE_MODERATE',
'hard' => 'COM_TALLPINE_GRADE_HARD',
'alpine' => 'COM_TALLPINE_GRADE_ALPINE',
];
protected function getOptions()
{
// options declared in the XML (<option> children) stay in front
$options = parent::getOptions();
foreach (self::GRADES as $value => $key) {
$options[] = (object) [
'value' => $value,
'text' => Text::_($key),
];
}
return $options;
}
}
The Manual’s Custom3Field follows the same shape with array_merge(parent::getOptions(), $options), and its
Custom4Field extends GroupedlistField and overrides getGroups() in the same way. ListField::getOptions() is
protected in Joomla 6.2 (the Manual’s examples declare it public, which also works because PHP allows widening
visibility). ListField also has addOption($text, $attributes = []) for options added at run time.
Because the options are plain objects, validate="options" on the field checks that the posted value is one of them; always
add it for fields whose options are limited by security, such as a "category the user may use".
A field with a dynamic list: the trail wardens
A field that reads from the database should receive the database from the framework, not from a static factory. FormField
implements DatabaseAwareInterface and uses DatabaseAwareTrait, so getDatabase() is available in every field class:
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Field;
use Joomla\CMS\Form\Field\ListField;
use Joomla\Database\ParameterType;
\defined('_JEXEC') or die;
final class WardenField extends ListField
{
protected $type = 'Warden';
protected function getOptions()
{
$options = parent::getOptions();
$db = $this->getDatabase();
$groupId = (int) $this->getAttribute('group_id', 0);
$query = $db->createQuery()
->select([$db->quoteName('u.id', 'value'), $db->quoteName('u.name', 'text')])
->from($db->quoteName('#__users', 'u'))
->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('m.user_id') . ' = ' . $db->quoteName('u.id'))
->where($db->quoteName('m.group_id') . ' = :gid')
->where($db->quoteName('u.block') . ' = 0')
->order($db->quoteName('u.name'))
->bind(':gid', $groupId, ParameterType::INTEGER);
return array_merge($options, $db->setQuery($query)->loadObjectList());
}
}
The Manual’s overview does not document getDatabase() on fields; it comes from DatabaseAwareTrait in the 6.2 source.
If a test creates a field by hand, call setDatabase() first.
A FormField with its own input
For a field type that is not a list, extend FormField and implement getInput(). This one shows the number of metres and
feet and uses a layout so the markup can be overridden:
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Field;
use Joomla\CMS\Form\FormField;
\defined('_JEXEC') or die;
final class ElevationField extends FormField
{
protected $type = 'Elevation';
protected $layout = 'com_tallpine.form.field.elevation';
protected function getInput()
{
return $this->getRenderer($this->layout)->render($this->getLayoutData());
}
protected function getLayoutData()
{
$data = parent::getLayoutData();
$data['feet'] = is_numeric($this->value) ? (int) round((float) $this->value * 3.28084) : null;
return $data;
}
}
<?php
// layouts/com_tallpine/form/field/elevation.php (in the manifest's <layouts> folder)
\defined('_JEXEC') or die;
extract($displayData);
/**
* @var string $id
* @var string $name
* @var string|null $value
* @var bool $required
* @var int|null $feet
*/
?>
<div class="input-group">
<input type="number" name="<?php echo $name; ?>" id="<?php echo $id; ?>"
value="<?php echo htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); ?>"
class="form-control validate-numeric"<?php echo $required ? ' required' : ''; ?>>
<span class="input-group-text">m</span>
<?php if ($feet !== null) : ?>
<span class="input-group-text"><?php echo (int) $feet; ?> ft</span>
<?php endif; ?>
</div>
The Manual’s Custom2Field follows the same plan with protected $layout = 'custom2field' and a file
layouts/custom2field.php that starts with defined('_JEXEC') or die; and extract($displayData);. Its closing reminder
is important and easy to forget: include the layouts folder in the extension manifest, or the layout file is not copied
by the installer and the field renders nothing (see
Extension types, manifests and packaging).
Always escape values in a custom getInput(); the base class does not do it for you.
Subform fields
subform embeds one form in another. With multiple="true" it becomes a repeatable group of rows, which is how core stores
lists such as the menu item "link types" and the Tallpine waypoints of a trail. Joomla serialises the rows as JSON in a
single column.
| Attribute | Meaning |
|---|---|
|
Required. A form XML path relative to the Joomla root, a form name the factory can resolve, or an inline |
|
Make the rows repeatable. Without it the subform appears once. |
|
Minimum (default 0) and maximum (default 1000) number of rows. |
|
One of the layouts below. |
|
The row buttons: any of |
|
Group the subform fields by their fieldset (repeatable, grid and table layouts). |
|
Set to |
|
A JSON string with the default rows. |
The layouts are joomla.form.field.subform.default (single mode), joomla.form.field.subform.repeatable,
joomla.form.field.subform.repeatable-table and, new in Joomla 6.1, joomla.form.field.subform.repeatable-grid, which
renders each row as a grid with one cell per field.
<field
name="waypoints"
type="subform"
label="COM_TALLPINE_FIELD_WAYPOINTS_LABEL"
multiple="true"
min="0"
max="25"
layout="joomla.form.field.subform.repeatable-grid"
buttons="add,remove,move"
validate="Subform"
>
<form>
<field name="label" type="text" label="COM_TALLPINE_WAYPOINT_LABEL" required="true" />
<field name="altitude" type="number" label="COM_TALLPINE_WAYPOINT_ALTITUDE" min="0" max="5000" />
<field name="has_water" type="radio" label="COM_TALLPINE_WAYPOINT_WATER"
layout="joomla.form.field.radio.switcher" default="0">
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</form>
</field>
The posted data is an array indexed by row keys (waypoints0, waypoints1, …). filter() and validate() visit every
row; the field’s filter() method loads a sub-form for each row and calls its filter(), and a default value given as a
JSON string is decoded first (an invalid JSON string filters to null). When saving, convert the row array into the stored
JSON (the AdminModel does this for you for params-like fields; for your own columns call json_encode in
prepareTable() or save()).
The grid layout and the order event (6.1)
The grid layout and the subform-order-changed event are new in Joomla 6.1; the files are present in the 6.1.0 source
tag and absent from 6.0.x. The event is a DOM CustomEvent dispatched by the joomla-field-subform web component after
the user reorders a row, and it bubbles up from the subform element. Its detail carries row (the affected row element),
fromPosition (the old index) and toPosition (the new index):
document.addEventListener('subform-order-changed', (event) => {
const { row, fromPosition, toPosition } = event.detail;
const subform = event.target;
// renumber the visible step labels in the Tallpine waypoint editor
subform.querySelectorAll('.subform-repeatable-group').forEach((group, index) => {
const badge = group.querySelector('.waypoint-index');
if (badge) {
badge.textContent = String(index + 1);
}
});
console.debug(`waypoint ${row.dataset.group} moved from ${fromPosition} to ${toPosition}`);
});
Load such a script through the Web Asset Manager with a dependency on webcomponent.field-subform. Row positions are
renumbered by the component on save, so a listener is for display only.
Subform pitfalls
-
The name
Subforminvalidate="Subform"matters. Without it, the nestedrequiredandvalidateattributes do nothing on the server. -
A
calendarfield inside a repeatable subform has failed on a second save unless Show Time was set to Yes for it, and duplicate subform fields are removed on save with a warning in the 5.4 series and later; see Joomla custom fields for the details. -
Inside a subform,
showonmust refer to the generated field id rather than the plain field name. -
Files uploaded through a
filefield inside a multiple subform are posted under nested names, so the handling code must walk the$_FILESstructure. -
A very large
maxmakes the page heavy: the web component clones the row template in the browser.
Loading and binding forms in models
In an MVC component the model owns the form. FormModel and AdminModel (both in Joomla\CMS\MVC\Model) share the
FormBehaviorTrait, which provides loadForm(), loadFormData(), preprocessForm() and preprocessData(). A component
overrides only two methods, getForm() and loadFormData().
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Administrator\Model;
use Joomla\CMS\Form\Form;
use Joomla\CMS\MVC\Model\AdminModel;
\defined('_JEXEC') or die;
final class TrailModel extends AdminModel
{
public $typeAlias = 'com_tallpine.trail';
public function getForm($data = [], $loadData = true)
{
$form = $this->loadForm(
'com_tallpine.trail',
'trail',
['control' => 'jform', 'load_data' => $loadData]
);
if (empty($form)) {
return false;
}
// adjust the form for the current user
if (!$this->getCurrentUser()->authorise('core.edit.state', 'com_tallpine')) {
$form->setFieldAttribute('published', 'disabled', 'true');
$form->setFieldAttribute('published', 'filter', 'unset');
}
return $form;
}
protected function loadFormData()
{
$app = $this->getApplication();
$data = $app->getUserState('com_tallpine.edit.trail.data', []);
if (empty($data)) {
$data = $this->getItem();
}
$this->preprocessData('com_tallpine.trail', $data);
return $data;
}
protected function preprocessForm(Form $form, $data, $group = 'content')
{
// the form file is the only place the fields are declared; plugins in the
// 'content' group are still allowed to change it via onContentPrepareForm
parent::preprocessForm($form, $data, $group);
}
}
The steps loadForm() performs, in order, are visible in the 6.2 source:
-
Build a signature hash from the form source and the options (excluding
load_data) and return the cached form if the same one was loaded before in this request, unless$clearis set. -
Register the component form and field paths:
forms,models/forms,models/fields,model/form,model/field. -
Get the form factory (the model’s own, or the container’s) and call
createForm($name, $options). -
Load the XML: if the source starts with
<it is parsed as an XML string, otherwiseloadFile()finds<source>.xmlin the registered paths. -
When the option
load_datais true, callloadFormData()to get the data. -
Call
preprocessForm($form, $data), which dispatchesonContentPrepareFormto the plugins of the model’s event group (thecontentgroup by default). -
Call
$form->bind($data), so the bound values are in place after the plugins changed the form.
The control option decides the input names. By convention the controller reads $this->input->post->get('jform', [], 'array'); if the model’s control and the controller’s key disagree, the form posts values the controller never sees, which
is the most common reason for "the form saves nothing" in new components. FormController::save() obtains the model’s form
with $model->getForm($data, false), normalises the request data, calls $model->validate($form, $data) and, on failure,
stores the unfiltered data in the user state (com_tallpine.edit.trail.data) so loadFormData() can re-fill the form.
The validate method of FormModel
FormModel::validate($form, $data, $group = null) dispatches onContentBeforeValidateData (with a
Model\BeforeValidateDataEvent) so a plugin can adjust the posted data, calls $form->process($data, $group) and returns
the filtered array or false. The older event onUserBeforeDataValidation is still dispatched when a listener exists but
logs a deprecation and is removed in 7.0, so plugins should switch to onContentBeforeValidateData. After a false result
the controller pushes the messages from getErrors() to the user.
Changing a form at run time
The Form class offers a small API to change a loaded form:
use Joomla\CMS\Form\Form;
// add or replace a field
$xml = new \SimpleXMLElement(
'<field name="gpx_url" type="url" label="COM_TALLPINE_FIELD_GPX_URL" filter="url" validate="url" />'
);
$form->setField($xml, null, true, 'details'); // $group, $replace, $fieldset
// change a Joomla attribute (not an HTML attribute)
$form->setFieldAttribute('distance_km', 'max', '80');
$form->setFieldAttribute('title', 'hint', 'Cima Verde loop');
// change the value
$form->setValue('difficulty', null, 'moderate');
// remove
$form->removeField('closure_reason');
$form->removeGroup('params');
// load another XML file or string into the same form
$form->loadFile(JPATH_COMPONENT_ADMINISTRATOR . '/forms/trail_extra.xml', false);
setFieldAttribute() changes the Joomla attribute from the XML, for instance hint; whether that reaches the HTML
depends on the field type. To change the value, use setValue(), because bound data overrides the attribute. setFields()
adds several. The $replace argument decides what happens when a field with the same name and group exists.
Plugins that change forms: onContentPrepareForm
A plugin can add fields to any form whose model dispatches onContentPrepareForm. The system plugin fields that powers
custom fields works this way, and so does every core plugin that adds tabs to the article edit screen. In Joomla 6.2 the
plugin is a SubscriberInterface listener with the concrete event class (see
Plugin development and events):
<?php
declare(strict_types=1);
namespace Tallpine\Plugin\Content\TallpineConditions\Extension;
use Joomla\CMS\Event\Model\PrepareFormEvent;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
\defined('_JEXEC') or die;
final class TallpineConditions extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return ['onContentPrepareForm' => 'onContentPrepareForm'];
}
public function onContentPrepareForm(PrepareFormEvent $event): void
{
$form = $event->getForm();
// only the Tallpine trail form, not every form in the site
if ($form->getName() !== 'com_tallpine.trail') {
return;
}
Form::addFormPath(__DIR__ . '/../../forms');
$form->loadFile('conditions', false);
}
}
Source: Joomla Manual: How plugins work
$event->getForm() and $event->getData() are the getters of the event class. The form name is the first argument that
the model passed to loadForm(), so a plugin identifies "its" form by comparing it. Forget this check and the plugin runs
for every form on the site, including user registration and the login module.
A front-end form
Front-end forms follow the same model pattern but add the pieces that visitors, unlike administrators, need: a captcha, a
CSRF token, a no-cache redirect and the Post/Redirect/Get dance. The Tallpine report a trail condition form is a
FormModel with the form definition in components/com_tallpine/forms/report.xml:
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset name="report">
<field name="trail_id" type="sql" label="COM_TALLPINE_REPORT_TRAIL"
query="SELECT id AS value, title AS text FROM #__tallpine_trails WHERE published = 1 ORDER BY title"
required="true" validate="options" />
<field name="reporter_email" type="email" label="COM_TALLPINE_REPORT_EMAIL"
required="true" validate="email" />
<field name="condition" type="list" label="COM_TALLPINE_REPORT_CONDITION"
required="true" validate="options">
<option value="clear">COM_TALLPINE_CONDITION_CLEAR</option>
<option value="snow">COM_TALLPINE_CONDITION_SNOW</option>
<option value="blocked">COM_TALLPINE_CONDITION_BLOCKED</option>
</field>
<field name="notes" type="textarea" label="COM_TALLPINE_REPORT_NOTES"
rows="5" maxlength="1000" filter="string" />
<field name="captcha" type="captcha" label="COM_TALLPINE_REPORT_CAPTCHA"
validate="captcha" />
</fieldset>
</form>
<?php
declare(strict_types=1);
namespace Tallpine\Component\Tallpine\Site\Controller;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die;
final class ReportController extends BaseController
{
public function submit(): bool
{
$this->checkToken(); // CSRF: the form includes HTMLHelper::_('form.token')
$model = $this->getModel('Report', 'Site');
$form = $model->getForm(null, false);
$data = $this->input->post->get('jform', [], 'array');
$valid = $model->validate($form, $data);
if ($valid === false) {
foreach ($model->getErrors() as $error) {
$this->app->enqueueMessage($error instanceof \Exception ? $error->getMessage() : $error, 'warning');
}
$this->app->setUserState('com_tallpine.report.data', $data);
$this->setRedirect(Route::_('index.php?option=com_tallpine&view=report', false));
return false;
}
$model->save($valid);
$this->app->setUserState('com_tallpine.report.data', null);
$this->setRedirect(Route::_('index.php?option=com_tallpine&view=trails', false), Text::_('COM_TALLPINE_REPORT_THANKS'));
return true;
}
}
The captcha rule and field use the CAPTCHA plugin chosen in Global Configuration or in the field’s plugin attribute.
The core reCAPTCHA plugin was removed in 5.0, and a Proof-of-Work captcha arrived in 6.1; install or enable the plugin you
want before depending on it. The sql field above is acceptable because the query is a constant in the XML and the
options rule checks that the posted value is one of the offered ids. See
Component site views and routing for the view and the router.
Security
Forms are where untrusted data enters Joomla. These rules cover almost every form-related vulnerability.
-
Always validate on the server. The Manual’s own warning is that a request can be crafted without the browser, so
required,pattern,minandmaxin the HTML mean nothing on their own. Use avalidaterule or afilterthat enforces the same constraint. -
Choose the filter on purpose. The default
InputFilteris a safe baseline for text.rawdisables all filtering and should only appear on fields that trusted users fill, and only if the output layer escapes.safehtmlis the choice for rich text from semi-trusted users;unsetis the choice for fields the user must not set. -
Disabled and hidden does not mean protected. Joomla rejects a disabled field that is posted, and ignores a value for a field that is not rendered; but a field hidden with
showonor by CSS is still processed if it arrives, so a restricted field needs an ACL check, for example ingetForm()as shown above. -
CSRF token. Render
HTMLHelper::_('form.token')and call$this->checkToken()in the controller. The core controllers do it; hand-written controllers must. -
Escape on output. Filtering cleans stored data; output still needs
htmlspecialcharsor a layout that escapes. CustomgetInput()implementations are the usual place where this is forgotten. -
File fields. Check size, extension and MIME type, store files outside the web root or in a folder without script execution, and never trust the original file name.
-
Mass assignment. Bind only the filtered result of
validate()to the table. Binding the raw$_POSTarray bypasses the form definition entirely. -
Queries in fields and rules.
sqlfields and custom lists must not include user input in the query text; use bound parameters andwhereIn(). -
Error messages. Messages from a rule are shown to the user, so do not include internal details or other users' data.
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
Client-side validation does nothing |
The script is not loaded ( |
|
The |
Custom field renders as a text box |
The type did not resolve to a class: missing |
Layout file for a custom field is missing after install |
The |
Form saves nothing |
|
Edited value comes back after a failed save |
The failed data is stored in the user state; |
|
The condition names a field that is in another group or a subform; name a field from the same group, or use the field id. |
A removed field reappears |
|
Values appear unfiltered |
|
Subform rules ignored |
|
Disabled field rejected |
A disabled field cannot be posted; use |
Date shifts by the time-zone offset after an error |
The posted calendar value was stored in the session without its UTC filter. Use the filtered value, as the core controller does. |
Two forms on a page interfere |
The form names are not unique, or both use the same |
Version notes and book corrections
| Version | Change |
|---|---|
4.0 |
Namespaced form classes; |
4.2 |
Multi-factor authentication replaced TFA in core. Its forms are ordinary forms (a plugin supplies the fields), which shows how far the form API is meant to be extended. |
5.0 |
Concrete event classes for plugins, so |
6.0 |
|
6.1 |
The |
7.0 (planned) |
|
Where older material differs from the 6.x documentation, the corrections are these.
-
Joomla 3 era books, wiki pages and many blog posts use
JForm,JFormField,JFormRule,JFormHelper,JHtml::_('form.token'),JFactory::getApplication()->inputandJFactory::getDbo(). Use the namespaced classesJoomla\CMS\Form\*, the container, and the Aware traits; the aliases exist only in BC6. -
Older material shows field classes in
models/fields, rules inmodels/rulesandJFormHelper::addFieldPath(). Namespaced extensions register a prefix withaddfieldprefix,addruleprefixandaddfilterprefix, and the lookup path registration inloadForm()remains only for old-style components. -
Developing Extensions for Joomla! 5 (Packt, 2023) covers XML forms, field types and custom fields on the Joomla 5.0 baseline. For 6.x, add the
date,datetimeandnotefields (6.0), the grid layout and order event of subforms (6.1), the BC6 note for theJ*aliases, and replace$dband$appas plugin properties (auto-detection ends in 7.0) withgetDatabase()andgetApplication(). -
Joomla! 4 Masterclass (Packt, 2023) treats custom fields from the user’s side (Joomla 4.x, which reached end of life in October 2025). It does not discuss the form API, filters, rules or custom field classes, which this page covers.
-
Documentation at
docs.joomla.orgforJFormand field types describes Joomla 1.5 to 3.x and marks itself obsolete; the Developer Manual atmanual.joomla.orgis the current reference, although its custom-field pages do not yet document every method (getInput()andgetLabel()details, layout locations), which is why the6.2-devsource tree is cited here. -
The Manual’s text for
validate()calls the filter and validation mechanism by their separate names; the source shows thatForm::process()runs filter, validate and post-process in that order, and that filtering happens first.
Quick reference
# find every form definition and every custom field class in a component
find administrator/components/com_tallpine -name '*.xml' -path '*forms*'
find administrator/components/com_tallpine/src -path '*Field*' -name '*Field.php'
# check the XML is well formed
xmllint --noout administrator/components/com_tallpine/forms/trail.xml
# lint the PHP classes of a component with the house toolchain
vendor/bin/phpcs --standard=PSR12 administrator/components/com_tallpine/src
vendor/bin/phpstan analyse administrator/components/com_tallpine/src
Source: Joomla Manual: Forms
For the PHP language features used throughout (namespaces, constructor promotion, readonly properties), see PHP namespaces and autoloading. For where forms fit into the whole component, see Component MVC in the administrator, Categories, custom fields and workflow integration and ACL in extensions. A module’s parameter form is the same machinery with a different manifest tag (see Module development), and a Web Services API endpoint reuses the model’s form for validation (see Web Services API).