20 minutes reading time (4093 words)

Creating a custom Form field type in Joomla 5 using the Modal Select example

2024-01-Creating-custom-form

Using the ModalSelect Form field type in Joomla 5 to simplify finding the right product, from thousands, by filtering in a modal window using category, manufacturer and search when developing an extension.

Intro

In the process of working with clients, there are tasks of various levels: someone needs a simple website of 5-6 pages. Someone needs a large catalog of goods or an online store with integrations with third-party systems using the REST API. Somebody else needs non-standard functionality for which there are no popular solutions available.

Joomla is good for development and allows you to create easily maintained code. If the requirement follows the CMS core then it has an answer for all these cases.

To complete a large project, we need to split it into smaller tasks, and  I want to talk about solving one of these tasks in this article.

Initial data

Customers have already created a product catalog on one of the popular components of the online store for Joomla (JoomShopping). They can select the parameters of the product, put it in the cart and make a purchase. Everything is as usual. However, now you need to add the ability to create a graphic layout for products. For example, your product is a mug or a T-shirt. And before buying, you can go to the product designer, upload your logo or photo, write a text and this layout is attached to the order in the online store. After payment, the layout goes directly to production, where the image and text are applied to your mug and sent to the address.

Since implementation of this functionality is quite time consuming, it is created as a separate product designer component. And data provider plugins will already be created, allowing you to work with one or another e-commerce component.

One of the small, applied tasks is to create connections between the goods of the online store component and the goods in the product designer component. This should be convenient and intuitive for content managers who will work on the content in the future. Therefore, it is not enough just to make a text field where the id number of the desired product will be indicated. There may be only a few dozen products in an online store, and then choosing a product for communication is not very difficult. If there are thousands of products the functionality of searching and filtering products by parameters is important. If you can filter the list of products by category, manufacturer, or find it by name among hundreds of others, your work will be much faster and easier.

This field is very similar to the work of the editor button plugins (editors-xtd group), where the data for selection is shown in the modal window: a link to the article, a short code for the module, etc.

A little theory

In the Joomla administrator panel, there are different fields that need to be filled with data from other components: specify the article, menu item, contact, product, etc. Usually such fields are designed as a select option drop-down list, they can be designed as input type="text" with a datalist, but there are also convenient fields showing a list of the desired entities, with filtering, search and pagination. Not only sources within the site (various components, plugins) can act as a data source, but also third-party services available via the REST API: CRM, delivery services, external databases, other Joomla sites, and so on.

We have all seen these fields in action when selecting an article in a menu item such as "Articles -Single article", "Contacts - Single contact", or when creating an alias of the menu item - "System links - Menu Item alias". However, let's remind ourselves what they look like.

A modal article selection window.

A modal single contact selection window.

The opportunities of the modal select field in Joomla

Let's take a closer look at these fields - what exactly they allow you to do. Somewhere deep inside, we understand that the main job of this field is to get the id of the selected entity and put this id in a text field. But on the screen we see something else - instead of an id number, we see the title of the article or contact. It's nice and convenient. You do not need to remember the name of the article with id 1452704. Also, the video clearly shows that if the field already has a value, the "clear" button appears. It resets the value of the field and allows you to click on the "select" button again.

In some cases, we have the opportunity to create a selected type of entity - article, contact, etc. right in the process of creating a menu item. This button works taking into account the ACL - separation of access rights in Joomla.

Imagine you are building a website and creating a "Contacts" page. If you don't have a bunch of company branches with a complex structure, then this is just the usual Joomla article in the "Uncategorized" category. And it already has all the contacts in the form of text or of variables. In ancient times, you had to create the article first, and then go to the menu items and create a menu item for it. You don't have to do that now.

Joomla 5 menu item single article select

And if the field already has a value, then in some cases it is possible to edit the selected entity (article, menu item, etc.) right in the process of creating a menu item.

So, using the selection field in the modal window, we can:

  • select
  • create
  • edit
  • clear

This is what's in front of my eyes. But in the depths of Joomla there is also a curious urlCheckin parameter that allows you to send the selected value to the url specified in the field. It is worth noting that this functionality in Joomla has been gradually developing for quite a long time. However, a separate universal field type that can be used for your needs appeared only in Joomla 5. It's not even in Joomla 4.

How are the fields of the Form constructor of the Joomla admin panel interface arranged?

Previously, this constructor was called JForm. I will assume that not all my readers have in their hands such a development tool as an IDE - development environment - a la PHP Storm or VS Code, so I will try to give additional guidelines for navigating the code base.

In Joomla, the Logic is separated from the View (the actual HTML output), so we will explore it in several places at the same time.

Logic is the Form class

Logic is the Form class. In Joomla 5, the Form class files are located in libraries/src/Form. We examine these files in order to understand the logic itself, what happens to the data and how to work with it.

Joomla 5 Form (ex JForm) class file structure

In short, the Form constructor receives XML with a description of the fields. reads data (field type, custom field class from the addfieldprefix attribute, if any, etc.), loads the required field class using FormHelper. If the field has some rules for filtering the output data - the FormRule class is used - remember the Joomla fields of the filelist type, where you can specify filtering parameters and select, for example, only php or only css files.

Joomla Form field class files are located in libraries/src/Form/Field. There are, to put it mildly, a lot of them. This is the building material of the admin panel, and sometimes the frontend too.

Class files describe class properties such as $type, $layout, and others necessary for operation. Most fields have methods getInput() - actually returns the HTML output of the field, getLayoutData() - preprocessing data for the field before sending it to the render, getLabel() - working with the field label, etc.

We remember that the field classes inherit the parent FormField class. In the class file libraries/src/Form/FormField.php the possible attributes of the field are described, that can be used in the description in XML. They have a brief description of what it is and why.

joomla 5 form field possible attributes for XML

Child classes (heirs) have the ability to work with the methods of the parent class and, if necessary, override it.

View (HTML output, layout) of a field in Joomla 5

Each field class has a HTML output. In classic MVC, the View works with data output immediately, but in Joomla there is an additional layer - Layout, which allows you to override layouts - one of the most important features of this CMS. The core layouts are expected to be located in the layouts folder in the site root. They pass an array of $displayData with all the data received from the getLayoutData() method. We specify which output layout to use in the $layout class property.

<?php
/**
 * Name of the layout being used to render the field
 *
 * @var    string
 * @since  3.7
 */
protected $layout = 'joomla.form.field.email';

This type of recording is quite common. In Joomla, layout is a dot-separated path to the layout file from the layouts folder at the root of the site. That is, the entry $layout = 'joomla.form.field.email' means that layouts will be used when rendering the field layouts/joomla/form/field/email.php.

<?php 
use Joomla\CMS\Layout\LayoutHelper;

$displayData = [
                'src' => $this->item->image,
                'alt' => $this->item->name,
               ];

echo LayoutHelper::render(
                        'joomla.html.image',
                         $displayData
                    );

Similarly, this example will use layout layouts/joomla/html/image.php. Some layouts can be overridden in the html folder of site templates and admin panel.

Accordingly, if we want to see exactly what data comes to the layout in the end and how it is displayed, go to the layout file and look.

Creating a data selection field in the Modal Select window in Joomla 5

Now let's return to the main task of the article.

Examples are important for us to study (Joomla 5.0.1 at the time of writing this article):

  • the main class of the field is libraries/src/Form/Field/ModalSelectField.php
  • the Joomla article modal select field - administrator/components/com_content/src/Field/Modal/ArticleField.php
  • menu type modal select field - administrator/components/com_menus/src/Field/MenutypeField.php
  • menu item modal select field - administrator/components/com_menus/src/Field/MenutypeField.php
  • output layout - layouts/joomla/form/field/modal-select.php

The single contact modal select field from com_contacts at the time of writing this article has not yet been converted to a universal field type one and just lies in administrator/components/com_contact/src/Field/Modal/ContactField.php. It inherits FormField directly, not ModalSelectField.

The algorithm of actions for adding your own field is as follows:

  • create an XML form with our field in an xml file or programmatically using \SimpleXMLElement.
  • If we work on the fly then, using the plugin for the onContentPrepareForm event, we add XML forms to the desired form (check $form->getName() before that)
  • Creating a field class.
  • If necessary, we create our own HTML output (layout) of the field. We will leave this out of the scope of this article.

And it works.

Field XML

The most important thing in this code is the addfieldprefix attribute, which means the namespace of your field class. The class name is formed from addfieldprefix + "\" + type + "Field". In this case, the field class will be Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field\ProductlistField.

<field
      type="productlist"
      name="product_id"
      addfieldprefix="Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field"
      label="Field label"
      hint="Field placeholder"
      />

HTML output (layout) of the field

In order for everything that happens in PHP to be clear, you need to first look at the layout of the field output. It is in the file layouts/joomla/form/field/modal-select.php. In fact, 2 input fields are output - one visible, the other invisible. The title of the selected article, contact, or product is entered in the visible field in the form of a placeholder - the $valueTitle parameter. And the second is his id - $value. If we have not selected anything yet, there should be a phrase in the field like "select an article" or "select a product". This is a language constant that we put in the hint attribute in the XML field or in the setup() method of the field class.

All parameters available for the output layout (which means those that can be used programmatically or in an XML file):

<?php
extract($displayData);

/**
 * Layout variables
 * -----------------
 * @var   string   $autocomplete    Autocomplete attribute for the field.
 * @var   boolean  $autofocus       Is autofocus enabled?
 * @var   string   $class           Classes for the input.
 * @var   string   $description     Description of the field.
 * @var   boolean  $disabled        Is this field disabled?
 * @var   string   $group           Group the field belongs to. <fields> section in form XML.
 * @var   boolean  $hidden          Is this field hidden in the form?
 * @var   string   $hint            Placeholder for the field.
 * @var   string   $id              DOM id of the field.
 * @var   string   $label           Label of the field.
 * @var   string   $labelclass      Classes to apply to the label.
 * @var   boolean  $multiple        Does this field support multiple values?
 * @var   string   $name            Name of the input field.
 * @var   string   $onchange        Onchange attribute for the field.
 * @var   string   $onclick         Onclick attribute for the field.
 * @var   string   $pattern         Pattern (Reg Ex) of value of the form field.
 * @var   boolean  $readonly        Is this field read only?
 * @var   boolean  $repeat          Allows extensions to duplicate elements.
 * @var   boolean  $required        Is this field required?
 * @var   integer  $size            Size attribute of the input.
 * @var   boolean  $spellcheck      Spellcheck state for the form field.
 * @var   string   $validate        Validation rules to apply.
 * @var   string   $value           Value attribute of the field.
 * @var   string   $dataAttribute   Miscellaneous data attributes preprocessed for HTML output
 * @var   array    $dataAttributes  Miscellaneous data attribute for eg, data-*
 * @var   string   $valueTitle
 * @var   array    $canDo
 * @var   string[] $urls
 * @var   string[] $modalTitles
 * @var   string[] $buttonIcons
 */

PHP field class

The field class, as you may have guessed, is in my plugin. The way to it plugins/wtproductbuilder/providerjoomshopping/src/Field/ProductlistField.php. I took the single modal article select field as a basis and redesigned it to suit my needs - choosing a product from the JoomShopping online store. We extend the parent ModalSelectField class with our own class.

My tasks include only product selection, editing and creation are not, so in the text of the article we are talking only about product selection. The PHP class is small, I will give it in its entirety and comment on it.

<?php

namespace Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field;

use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ModalSelectField;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Supports a modal article picker.
 *
 * @since  1.6
 */
class ProductlistField extends ModalSelectField
{
	/**
	 * The form field type.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $type = 'Productlist';

	/**
	 * Method to attach a Form object to the field.
	 *
	 * @param   \SimpleXMLElement  $element  The SimpleXMLElement object representing the `<field>` tag for the form field object.
	 * @param   mixed              $value    The form field value to validate.
	 * @param   string             $group    The field name group control value.
	 *
	 * @return  boolean  True on success.
	 *
	 * @see     FormField::setup()
	 * @since   5.0.0
	 */
	public function setup(\SimpleXMLElement $element, $value, $group = null)
	{
		
       // Get the field
       $result = parent::setup($element, $value, $group);

		if (!$result)
		{
			return $result;
		}

		$app = Factory::getApplication();

		// We need the Url to get the list of products, 
		// getting an editing form, a creation form
		// entities. We indicate them here.
		// The result of accessing these URLs should return HTML,
		// which will include a small javascript,
		// transmitting the selected values - product id and product name.

      
		$urlSelect = (new Uri())->setPath(Uri::base(true) . '/index.php');
		$urlSelect->setQuery([
			'option'                => 'com_ajax',
			'plugin'                => 'providerjoomshopping',
			'group'                 => 'wtproductbuilder',
			'format'                => 'html',
			'tmpl'                  => 'component',
			Session::getFormToken() => 1,
		]);

		$modalTitle = Text::_('PLG_WTPRODUCTBUILDER_PROVIDERJOOMSHOPPING_MODAL_SELECT_CHOOSE_PRODUCT');
		$this->urls['select'] = (string) $urlSelect;

		// We comment on these lines, they are not needed. In the articles about JavaScript section
		// I’ll tell you why.
		// $wa = $app->getDocument()->getWebAssetManager();
		// $wa->useScript('field.modal-fields')->useScript('core');
		
		// Modal window title
		// To create and edit, respectively, you also need
		// individual headings
		$this->modalTitles['select'] = $modalTitle;

		// hint - the field placeholder in HTML.
		$this->hint = $this->hint ?: Text::_('PLG_WTPRODUCTBUILDER_PROVIDERJOOMSHOPPING_MODAL_SELECT_CHOOSE_PRODUCT');

		return $result;
	}
}

Separately, the getValueTitle() method was introduced, which shows the name of the selected entity (product name, article title, etc.) in cases when they have already been selected and saved. That is, we went to edit the menu item, we do not touch the field, but we want to see the title of the article / product name understandable to people, and not just an id. This method shows the desired title.

<?php 
    /**
	 * The method shows the name of the selected product in the placeholder field.
	 *
	 * @return string
	 *
	 * @since   5.0.0
	 */
	protected function getValueTitle()
	{
		$value = (int) $this->value ?: ''; // This is a product id or article id or ...
		$title = '';

		if ($value)
		{
			try
			{
				// To get the necessary data, it is best to use
				// methods of the Joomla core API and/or components,
				// and not direct requests to the database.
				$lang = \JSFactory::getLang();
				$name             = $lang->get('name');
				$jshop_product = \JSFactory::getTable('product', 'jshop');
				$jshop_product->load($value);
				$title = $jshop_product->$name;
			}
			catch (\Throwable $e)
			{
				Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
			}
		}
		return $title ?: $value;
	}

In some fields where more complex functionality is required - multilingual associations and so on - there are other methods in the field class that override the basic methods of the FormField class:

  • setLayoutData() is a method for preprocessing data before actually rendering the field
  • getRenderer() - additional parameters for rendering

etc.

In our case, there is no such need, so we do not use them.

HTML output of the content of the modal window

When you click on the "select" button, a modal Bootstrap window opens, in which a list of products opens in the <iframe>. By clicking on the product name or Javascript image, get the product id and name for our field. But to get the HTML of this list itself, we have to implement it somewhere. That is, such a conclusion and functionality must either support the component and then we will access the component URL. Either through com_ajax we access the plugin and get the HTML from there. That's what we'll do.

Modal window iframe content

In my plugin, the onAjaxProviderjoomshopping() method returns the HTML output of the list of products. There we loop through the array with them, take the picture, the name and output. The code is generally voluminous, so I will publish the most important fragments.

Simplified loop code example:

<table class="table table-striped table-bordered">
   <thead>
   <tr>
      <th><?php echo Text::_('JSHOP_PRODUCT'); ?></th>
   </tr>
   </thead>
   <tbody>
   <?php foreach ($products as $product): ?>
      <tr>
         <td>
            <b> <?php
               $link_attribs = [
                  'class' => 'select-link',
                  'data-product-id' => $product->product_id,
                  'data-product-title' => htmlspecialchars($product->name),
               ];
               echo HTMLHelper::link(
                  '#',
                  $product->name,
                  $link_attribs
               );?></b>
            <div><?php echo $product->short_description; ?></div>
         </td>
      </tr>
      <?php endforeach; ?>
   </tbody>
</table>

First, we add our javascript to the <iframe>, which will listen to clicks on pictures and product names. We make it out as a media assett to the plugin and connect it via the Web Asset Manager.

<?php
$app = $this->getApplication();
$doc = $app->getDocument();
$doc->getWebAssetManager()
    ->useScript('core')
    ->registerAndUseScript(
        'wtproductbuilder.providerjoomshopping.modal', 'plg_wtproductbuilder_providerjoomshopping/providerjoomshopping.modal.js'
    );

Second. The link tag code must contain data attributes with the data we need. We saw this fragment in the sample code of the goods output cycle.

<?php
use Joomla\CMS\HTML\HTMLHelper;

// this code is executed internally in foreach($products as $product)

$link_attribs = [
    'class' => 'select-link',
    'data-product-id' => $row->product_id,
    'data-product-title' => htmlspecialchars($row->name),
];
echo HTMLHelper::link(
    '#', // url
    $row->name, // Link text
    $link_attribs // link attributes
);

JavaScript processing. Sending data from the <iframe> to the field in the parent window

Now let's start working with JavaScript. In the process of writing the article, nuances emerged that allow us to talk about the old and new ways of working.

We remember that in the process of working, we connected the following js scripts

  • media/system/js/fields/modal-fields.min.js - this file was connected in the class of the article selection field. However, we can talk now that this is an archaism and outdated method of work. This file is no longer needed. We commented it out in our PHP class.
  • media/plg_wtproductbuilder_providerjoomshopping/js/providerjoomshopping.modal.js - our own js file.

Let's start with our own javascript. Here, using the select-link class, we get all the selectors and hang the listener of the click event on them.

(() => {
    document.addEventListener('DOMContentLoaded', () => {
        // Get the elements

        const product_links = document.querySelectorAll('.select-link');
        // Listen for click event
        product_links.forEach((element) => {
            element.addEventListener('click', event => {
                event.preventDefault();
                const {
                    target
                } = event;

                let data = {
                    'messageType' : 'joomla:content-select',
                    'id' : target.getAttribute('data-product-id'),
                    'title' : target.getAttribute('data-product-title')
                };
                window.parent.postMessage(data);
            });
        });
    });
})();

If everything is intuitive with id and title, then with the data object and postMessage, it may not be obvious to those who are used to working with Joomla.

As it was in Joomla 3 and Joomla 4

Previously, in Joomla 2.5, 3.x and even in 4.x, the following approach was used: in the layout of the field output, we used an inline script to hang a handler function on the window, and from <iframe> we called it as window.parent[functionName]. Take a look at this code

element.addEventListener('click', event => {
      event.preventDefault();
  
      const functionName = event.target.getAttribute('data-function');
  
      if (functionName === 'jSelectMenuItem' && window[functionName]) {
        // Used in xtd_contacts
        window[functionName](event.target.getAttribute('data-id'), event.target.getAttribute('data-title'), event.target.getAttribute('data-uri'), null, null, event.target.getAttribute('data-language'));
      } else if (window.parent[functionName]) {
        // Used in com_menus
        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);
      }

In this form, the function name was specified in the data-function attribute of each link in the list of articles / contacts / menu items. And the function itself was placed inline, sometimes unifying its name with an additional id. For example, "jSelectArticle_".$this->id.

The jSelectArticle() function or similar (we would have jSelectProduct()) is a wrapper for the standard processModalSelect() function from the file modal-fields.min.js . It, in turn, calls the processModalParent() function and closes the modal window after execution.

Old joomla processModalSelect method on javascript

This function needed to specify a bunch of parameters to work: the type of entity (article,  contact, etc.), the prefix of the field (which in practice turned out to be the id of the HTML field selector), the actual id and title - the parameters we need, etc.

Old joomla processModalSelect method

In one function, everything was collected for all occasions. That's where the data was placed in our field. 

How it became in Joomla 5

However, now, in Joomla 5, this file is no longer needed. If we use the standard layout of the field output, then the modal-content-select-field asset is connected to it, working in a new way.

modal content select field asset in joomla 5

Now the Joomla 5 frontend is switching to using JavaScript postMessages. Since not all old extensions are yet ready to switch to new rails, the JoomlaExpectingPostMessage flag has been implemented, which allows you to distinguish outdated event calling methods. It has an indirect relation to the described method of work, but it may be useful to someone. This flag will be removed after the full transition to postMessages.

JoomlaExpectingPostMessage flag in javascript in Joomla 5

So, now we don't need extra attributes of links with the name of the called functions. Instead, we use the postMessage mechanism. To do this, in the data object, we need to specify the messageType parameter equal to joomla:content-select. Why? From the point of view of JavaScript, working in Joomla is as follows:

  • clicking on a link and getting link attributes
  • sending a message to the parent window window.parent.postMessage(data)
  • The media/system/js/fields/modal-content-select-field.js file is connected in the parent window where that has a listener for the message event.
  • It checks the message type and if it is joomla:content-select, then the values are placed in the required fields and the modal window is closed

joomla javascript postmessages message type joomla content select

In the process of studying the Joomla core code and searching for a solution, I naturally came across the functions jSelectArticle() and the like. Then I came across postMessage and decided to make my MessageType by giving it a long unique name. To make it work, I wrote my own processing to it, calling the (as it turned out, outdated) processModalSelect() function. And I was faced with the fact that the modal window did not want to close in any way, although the data was inserted correctly into the fields. Further searches led first to the correct type of event, and then to the removal of unnecessary scripts and simplification of the code as a whole.

Summary

Joomla provides the developer with a rich set of tools for working with and obtaining data from third-party sources and using it in your code. Working with JForm fields is important for a developer when creating their own extensions, especially when it is necessary to solve a task that goes beyond typical scope. Of course, such a modal window and the selection of data in it is a rather special case, but in this way you can both override any other JForm fields and create your own types with your own UX logic.

1
Your Guide to the Joomla Year
Elections for Joomla's Leadership coming up!
 

Comments 2

Already Registered? Login Here
Harald Leithner on Wednesday, 07 February 2024 14:33
Looks like a great article

But why is this not on https://manual.joomla.org ?

1
But why is this not on https://manual.joomla.org ?
Sergey Tolkachyov on Saturday, 10 February 2024 12:14
May be

In order to become part of the official documentation, this article, it seems to me, must go through one more stage of adaptation.

0
In order to become part of the official documentation, this article, it seems to me, must go through one more stage of adaptation.

By accepting you will be accessing a service provided by a third-party external to https://magazine.joomla.org/