We have a database where the data about the component is stored. The next step is to display the dynamic content in the frontend. In this part, I'll show you how to output the content for an element via menu item. For this we will create our own form field.
For impatient people: View the changed program code in the Diff View[^codeberg.org/astrid/j4examplecode/compare/t6b...t7] and copy these changes into your development version.
Step by step
New files
administrator/components/com_foos/ src/Field/Modal/FooField.php
First, we create the form field through which it is possible to select or deselect a Foo element. In this case, we cannot access a ready-made field. Basically, we implement the methods getInput
and getLabel
and we set the type to Modal_Foo
. It is not mandatory that the name of the class starts with the word 'Field' and that the class is stored in the directory 'Field'. However, it can be helpful because it is standard in Joomla's own extension.
It is possible to extend the field so that a Foo element is created via a button. I have left this out so far for the sake of simplicity. Sample code is provided by the component
com_contact
in the fileadministrator/components/com_contact/ src/Field/Modal/ContactField.php
.
administrator/components/com_foos/src/Field/Modal/FooField.php
// https://codeberg.org/astrid/j4examplecode/raw/branch/t7/src/administrator/components/com_foos/src/Field/Modal/FooField.php
<?php
/**
* @package Joomla.Administrator
* @subpackage com_foos
*
* @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace FooNamespace\Component\Foos\Administrator\Field\Modal;
\defined('JPATH_BASE') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Session\Session;
/**
* Supports a modal foo picker.
*
* @since __DEPLOY_VERSION__
*/
class FooField extends FormField
{
/**
* The form field type.
*
* @var string
* @since __DEPLOY_VERSION__
*/
protected $type = 'Modal_Foo';
/**
* Method to get the field input markup.
*
* @return string The field input markup.
*
* @since __DEPLOY_VERSION__
*/
protected function getInput()
{
$allowClear = ((string) $this->element['clear'] != 'false');
$allowSelect = ((string) $this->element['select'] != 'false');
// The active foo id field.
$value = (int) $this->value > 0 ? (int) $this->value : '';
// Create the modal id.
$modalId = 'Foo_' . $this->id;
// Add the modal field script to the document head.
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
// Add the modal field script to the document head.
$wa->useScript('field.modal-fields');
// Script to proxy the select modal function to the modal-fields.js file.
if ($allowSelect) {
static $scriptSelect = null;
if (is_null($scriptSelect)) {
$scriptSelect = [];
}
if (!isset($scriptSelect[$this->id])) {
$wa->addInlineScript("
window.jSelectFoo_" . $this->id . " = function (id, title, object) {
window.processModalSelect('Foo', '" . $this->id . "', id, title, '', object);
}",
[],
['type' => 'module']
);
$scriptSelect[$this->id] = true;
}
}
// Setup variables for display.
$linkFoos = 'index.php?option=com_foos&view=foos&layout=modal&tmpl=component&'
. Session::getFormToken() . '=1';
$linkFoo = 'index.php?option=com_foos&view=foo&layout=modal&tmpl=component&'
. Session::getFormToken() . '=1';
$modalTitle = Text::_('COM_FOOS_CHANGE_FOO');
$urlSelect = $linkFoos . '&function=jSelectFoo_' . $this->id;
if ($value) {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('name'))
->from($db->quoteName('#__foos_details'))
->where($db->quoteName('id') . ' = ' . (int) $value);
$db->setQuery($query);
try {
$title = $db->loadResult();
} catch (\RuntimeException $e) {
Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
}
}
$title = empty($title) ? Text::_('COM_FOOS_SELECT_A_FOO') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
// The current foo display field.
$html = '';
if ($allowSelect || $allowNew || $allowEdit || $allowClear) {
$html .= '<span class="input-group">';
}
$html .= '<input class="form-control" id="' . $this->id . '_name" type="text" value="' . $title . '" readonly size="35">';
// Select foo button
if ($allowSelect) {
$html .= '<button'
. ' class="btn btn-primary hasTooltip' . ($value ? ' hidden' : '') . '"'
. ' id="' . $this->id . '_select"'
. ' data-bs-toggle="modal"'
. ' type="button"'
. ' data-bs-target="#ModalSelect' . $modalId . '"'
. ' title="' . HTMLHelper::tooltipText('COM_FOOS_CHANGE_FOO') . '">'
. '<span class="icon-file" aria-hidden="true"></span> ' . Text::_('JSELECT')
. '</button>';
}
// Clear foo button
if ($allowClear) {
$html .= '<button'
. ' class="btn btn-secondary' . ($value ? '' : ' hidden') . '"'
. ' id="' . $this->id . '_clear"'
. ' type="button"'
. ' onclick="window.processModalParent(\'' . $this->id . '\'); return false;">'
. '<span class="icon-remove" aria-hidden="true"></span>' . Text::_('JCLEAR')
. '</button>';
}
if ($allowSelect || $allowNew || $allowEdit || $allowClear) {
$html .= '</span>';
}
// Select foo modal
if ($allowSelect) {
$html .= HTMLHelper::_(
'bootstrap.renderModal',
'ModalSelect' . $modalId,
[
'title' => $modalTitle,
'url' => $urlSelect,
'height' => '400px',
'width' => '800px',
'bodyHeight' => 70,
'modalWidth' => 80,
'footer' => '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">'
. Text::_('JLIB_HTML_BEHAVIOR_CLOSE') . '</button>',
]
);
}
// Note: class='required' for client side validation.
$class = $this->required ? ' class="required modal-value"' : '';
$html .= '<input type="hidden" id="'
. $this->id . '_id"'
. $class . ' data-required="' . (int) $this->required
. '" name="' . $this->name
. '" data-text="'
. htmlspecialchars(Text::_('COM_FOOS_SELECT_A_FOO', true), ENT_COMPAT, 'UTF-8')
. '" value="' . $value . '">';
return $html;
}
/**
* Method to get the field label markup.
*
* @return string The field label markup.
*
* @since __DEPLOY_VERSION__
*/
protected function getLabel()
{
return str_replace($this->id, $this->id . '_name', parent::getLabel());
}
}
The programme code for the form field is adapted to Bootstrap 5[^getbootstrap.com]. This framework was integrated into Joomla in the pull request 32037[^github.com/joomla/joomla-cms/pull/32037].
Make sure that you use the correct names. If nothing happens later during testing when you select a single Foo, it is usually due to a typing error. Background: JavaScript is being executed. You add this script in two places. First, you create the function jSelectFoo_...
using variables.
...
if (!isset($scriptSelect[$this->id])) {
$wa->addInlineScript("
window.jSelectFoo_" . $this->id . " = function (id, title, object) {
window.processModalSelect('Foo', '" . $this->id . "', id, title, '', object);
}",
[],
['type' => 'module']
);
$scriptSelect[$this->id] = true;
}
...
In the source code of the Joomla administration area, the following code is added:
<script nonce="sampleId=">
function jSelectFoos_jform_request_id(id, title, object) { window.processModalSelect('Foo', 'jform_request_id', id, title, '', object);}
</script>
A little later, you call the function.
...
$urlSelect = $linkFoos . '&function=jSelectFoo_' . $this->id;
...
The name of the function must be the same in both places!
In an early sample code version for the field 'FooField' we do not use the Webasset Manager. The necessary changes can be found here[^github.com/joomla/joomla-cms/commit/04f844ad4a6d0432ec4b770bbb2a33243ded16d9].
administrator/components/com_foos/ tmpl/foos/modal.php
We open the selection in a modal window via the FooField. As address we have inserted in the field $linkFoos = 'index.php?option=com_foos&view=foos&layout=modal&tmpl=component&'
. The following code shows you the template for this modal window.
administrator/components/com_foos/tmpl/foos/modal.php
// https://codeberg.org/astrid/j4examplecode/raw/branch/t7/src/administrator/components/com_foos/tmpl/foos/modal.php
<?php
/**
* @package Joomla.Administrator
* @subpackage com_foos
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
\defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$app = Factory::getApplication();
$wa = $this->document->getWebAssetManager();
$wa->useScript('com_foos.admin-foos-modal');
$function = $app->input->getCmd('function', 'jSelectFoos');
$onclick = $this->escape($function);
?>
<div class="container-popup">
<form action="<?php echo Route::_('index.php?option=com_foos&view=foos&layout=modal&tmpl=component&function=' . $function . '&' . Session::getFormToken() . '=1'); ?>" method="post" name="adminForm" id="adminForm" class="form-inline">
<?php if (empty($this->items)) : ?>
<div class="alert alert-warning">
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
</div>
<?php else : ?>
<table class="table table-sm">
<thead>
<tr>
<th scope="col" style="width:10%" class="d-none d-md-table-cell">
</th>
<th scope="col" style="width:1%">
</th>
</tr>
</thead>
<tbody>
<?php
$iconStates = [
-2 => 'icon-trash',
0 => 'icon-unpublish',
1 => 'icon-publish',
2 => 'icon-archive',
];
?>
<?php foreach ($this->items as $i => $item) : ?>
<?php $lang = ''; ?>
<tr class="row<?php echo $i % 2; ?>">
<th scope="row">
<a class="select-link" href="javascript:void(0)" data-function="<?php echo $this->escape($onclick); ?>" data-id="<?php echo $item->id; ?>" data-title="<?php echo $this->escape($item->name); ?>">
<?php echo $this->escape($item->name); ?>
</a>
</th>
<td>
<?php echo (int) $item->id; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<input type="hidden" name="task" value="">
<input type="hidden" name="forcedLanguage" value="<?php echo $app->input->get('forcedLanguage', '', 'CMD'); ?>">
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
A Modal[^en.wikipedia.org/wiki/dialog_box] is an area that opens in the foreground of a web page and changes its state. It is required to actively close it. Modal dialogs lock the rest of the application as long as the dialog is displayed. A modal is also called a dialog or lightbox.
media/com_foos/joomla.asset.json
We use the WebAssetManager. This time we add our own webasset using the file joomla.asset.json
. If you don't include it correctly, you will get the following error when you select a foo item for the menu item: There is no "com_foos.admin-foos-modal" asset of a "script" type in the registry.
. Reason: In the modal, the line $wa->useScript('com_foos.admin-foos-modal');
calls the script com_foos.admin-foos-modal
, which, however, was not registered correctly before. Therefore it is not found.
Because of the newly added file
joomla.asset.json
it is necessary that we reinstall the extension. We have used other files so far without a new installation in Joomla. This does not work with the filejoomla.asset.json
. This file has to be registered once during an installation. Furthermore, changes can be made in it. These are recognised without a new installation.
It is not mandatory to create the file
joomla.asset.json
if you want to use the WebAssetManager[^docs.joomla.org/j4.x:web_assets]. In the documentation you will find possibilities to register webassets in the code afterwards.
media/com_foos/joomla.asset.json
/* https://codeberg.org/astrid/j4examplecode/raw/branch/t7/src/media/com_foos/joomla.asset.json */
{
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
"name": "com_foos",
"version": "1.0.0",
"description": "Joomla CMS",
"license": "GPL-2.0-or-later",
"assets": [
{
"name": "com_foos.admin-foos-modal",
"type": "script",
"uri": "com_foos/admin-foos-modal.js",
"dependencies": ["core"],
"attributes": {
"defer": true
}
}
]
}
Are you wondering that instead of
com_foos/js/admin-foos-modal.js
I writecom_foos/admin-foos-modal.js
asuri
? In my opinion this is a hidden secret in Joomla.js
andcss
file, if the path is not absolute[^github.com/joomla/joomla-cms/blob/ddb844b450ec989f08f6a54c051ca52d57fa0789/ libraries/src/webasset/webassetitem.php#l349], are automatically searched in the subdirectoryjs
, respectivelycss
. This was already the case in Joomla 3.x[^docs.joomla.org/adding_javascript#external_javascript]. In the callJHtml::_('script', 'com_example/example.js', array('relative' => true));
Joomla expects the fileexample.js
to be located atmedia/com_example/example.js
. You should not includejs
in the path in the statement This behavior is implemented by Web Asset Manager for scripts and styles by default. Want to take a closer look? You can find the code for this in the filelibraries/src/WebAsset/WebAssetItem.php
.
For the media version the Web Asset Manager sets the default auto
. This means that JHtml::_('script', 'com_example/example.js', array('version' => 'auto'));
is called by default. What does this mean exactly? The media version is used to control the new loading of CSS and JavaScript files. Specifically, the media version is reset during an update, installation, or uninstall. The reason for this is that browsers cache CSS and JS files, so the following situation can occur:
- A user accesses a Joomla website, and the CSS and JS files are stored in the user's browser.
- Joomla is updated, and in the update process, the contents of several CSS and JS files change. The file names remain the same.
- The user accesses the newly updated site, but the new CSS and JS files are not reloaded because the user's browser uses the cached versions instead.
- if
version' => 'auto
is set, thesrc
attribute of the<script>
tag is different after the update, and the browser loads the new file.
For normal work with a Joomla website this setting is useful. When developing it might happen that you want to reload web asset files more often. I use debug mode when developing, because this way a new media version is forced on every HTTP request.
What does the attribute
"defer": true
mean? Scripts are loaded withasync
asynchronous or parallel to other resources.defer
promises the browser that the web page will not be changed by instructions. More information at Mozilla.org.
The Joomla Web Assets Manager manages all assets in a Joomla installation. It is not mandatory to include script files or stylesheets via this manager. All calls to
HTMLHelper::_('stylesheet or script ...)
work, but these assets are appended after the Web Asset Manager assets. This results in overriding styles that are set in the template. Thus, a user does not have the possibility to manipulate by means of auser.css
. However, it does have more advantages: If dependencies are set correctly, no conflicts occur and necessary files are loaded by Joomla. For example, we have set a dependency in the line"dependencies": ["core"],
.
media/com_foos/js/admin-foos-modal.js
The following is the JavaScript code that causes a foo element to be selectable when a menu item is created. We will assign the class select-link
, which is the main element in the file, to the corresponding button in the field later.
media/com_foos/js/admin-foos-modal.js
/* https://codeberg.org/astrid/j4examplecode/raw/branch/t7/src/media/com_foos/js/admin-foos-modal.js */
;(function () {
'use strict'
document.addEventListener('DOMContentLoaded', function () {
var elements = document.querySelectorAll('.select-link')
for (var i = 0, l = elements.length; l > i; i += 1) {
elements[i].addEventListener('click', function (event) {
event.preventDefault()
var functionName = event.target.getAttribute('data-function')
window.parent[functionName](
event.target.getAttribute('data-id'),
event.target.getAttribute('data-title'),
null,
null,
event.target.getAttribute('data-uri'),
event.target.getAttribute('data-language'),
null
)
if (window.parent.Joomla.Modal) {
window.parent.Joomla.Modal.getCurrent().close()
}
})
}
})
})()
Modified files
administrator/components/com_foos/foos.xml
We have created a new JavaScript file. We place it in the media\js
directory. So that it is copied when the component is installed, we add the js
folder in the section media
of the installation manifest.
administrator/components/com_foos/foos.xml
<folder>src</folder>
<folder>tmpl</folder>
</files>
<media folder="media/com_foos" destination="com_foos">
<folder>js</folder>
<filename>joomla.asset.json</filename>
</media>
<!-- Back-end files -->
<administration>
<!-- Menu entries -->
Read in the preface why you choose the
media
directory ideally for assets like JavaScript files or stylesheets.
components/com_foos/src/Model/FooModel.php
We no longer output static text. An item from the database is displayed. Therefore we rename the getMsg
method to getItem
. We adjust the variable names and create a database query.
Make sure you update the DocBlock here. This sounds nit-picky and unimportant at the beginning. In small extensions, it may still be minor. But later you may want to create documentation automatically based on this information. Then you will be happy if they are correct.
components/com_foos/src/Model/FooModel.php
class FooModel extends BaseDatabaseModel
{
/**
* @var string message
* @var string item
*/
protected $message;
protected $_item = null;
/**
* Get the message
* Gets a foo
*
* @return string The message to be displayed to the user
* @param integer $pk Id for the foo
*
* @return mixed Object or null
*
* @since __BUMP_VERSION__
*/
public function getMsg()
public function getItem($pk = null)
{
$app = Factory::getApplication();
$this->message = $app->input->get('show_text', "Hi");
$pk = $app->input->getInt('id');
if ($this->_item === null)
{
$this->_item = array();
}
if (!isset($this->_item[$pk]))
{
try
{
$db = $this->getDbo();
$query = $db->getQuery(true);
$query->select('*')
->from($db->quoteName('#__foos_details', 'a'))
->where('a.id = ' . (int) $pk);
$db->setQuery($query);
$data = $db->loadObject();
if (empty($data))
{
throw new \Exception(Text::_('COM_FOOS_ERROR_FOO_NOT_FOUND'), 404);
}
$this->_item[$pk] = $data;
}
catch (\Exception $e)
{
$this->setError($e);
$this->_item[$pk] = false;
}
}
return $this->message;
return $this->_item[$pk];
}
}
Joomla supports you in creating the database queries. If you use the available statements[^docs.joomla.org/accessing_the_database_using_jdatabase], Joomla will take care of security or different syntax in PostgreSQL and MySQL.
components/com_foos/src/View/Foo/HtmlView.php
In the view we consequently replace $this->msg = $this->get('Msg');
with $this->item = $this->get('Item');
.
components/com_foos/src/View/Foo/HtmlView.php
class HtmlView extends BaseHtmlView
{
/**
* The item object details
*
* @var \JObject
* @since __BUMP_VERSION__
*/
protected $item;
/**
* Execute and display a template script.
*
*/
public function display($tpl = null)
{
$this->msg = $this->get('Msg');
$this->item = $this->get('Item');
return parent::display($tpl);
}
components/com_foos/ tmpl/foo/default.php
We will customize the display of the name in the template. Here we access the item
element and its name
property. In this way we can flexibly and easily add new properties in the future.
components/com_foos/tmpl/foo/default.php
\defined('_JEXEC') or die;
?>
Hello Foos: <?php echo $this->msg;
<?php
echo $this->item->name;
components/com_foos/ tmpl/foo/default.xml
We create an entry in the default.xml
file for the new form field. This way we enable the selection of a Foo element at the menu item. Worth mentioning are the entries addfieldprefix="FooNamespace\Component\Foos\Administrator\Field"
and type="modal_foo"
:
components/com_foos/tmpl/foo/default.xml
</layout>
<!-- Add fields to the request variables for the layout. -->
<fields name="request">
<fieldset name="request">
<fieldset name="request"
addfieldprefix="FooNamespace\Component\Foos\Administrator\Field"
>
<field
name="show_text"
type="text"
label="COM_FOOS_FIELD_TEXT_SHOW_LABEL"
default="Hi"
name="id"
type="modal_foo"
label="COM_FOOS_SELECT_FOO_LABEL"
required="true"
select="true"
new="true"
edit="true"
clear="true"
/>
</fieldset>
</fields>
Test your Joomla component
-
install your component in Joomla version 4 to test it: Copy the files in the
administrator
folder into theadministrator
folder of your Joomla 4 installation. Copy the files in thecomponents
folder into thecomponents
folder of your Joomla 4 installation. Copy the files in themedia
folder into themedia
folder of your Joomla 4 installation. A new installation is required to register the filejoomla.asset.json
. -
open the menu manager to create a menu item. Click on
Menu
and then onAll Menu Items
.
Then click on the New
button and fill in all the necessary fields. You can find the appropriate Menu Item Type
by clicking the Select
button. Make sure that you see a selection field instead of the text field for entering static text. The selection field also contains a Select
button.
- click on the second
Select
and select an item. Make sure that a selected item can be cleared usingClear
.
-
save the menu item.
-
switch to the frontend and make sure that the menu item is created correctly. You will see the title of the element you selected in the administration area.
Webmentions