16 minutes reading time (3187 words)

Custom Fields - Episode 7 Part 1: one Custom Field to rule them all

OctoberCustomFields

As you probably have noticed, the title of this Episode takes inspiration from 2 famous films / books: 

  1. "One Custom Field to rule them all" is a reference to The Lord of the Rings by JRR Tolkien. This is simply because we talk here about the new Type of Custom Field introduced in Joomla 4: "the Subform"... which is the most powerful and can literally rule all the other ones :)
  2. "Episode 7 Part 1" is a reference to the famous story of JK Rowling: just like for Harry Potter, the seventh and last episode was a bit too long to fit into one single article. So the good news is: what should have been the final episode about Custom Fields in the Joomla Community Magazine will be split into two Parts ;)

Custom Fields of Type "Repeatable" in Joomla 3

As you might have noticed, at some point Joomla 3 introduced a new Type of Custom Fields: the Repeatable Field. As its name suggests, it allows to repeat a field or -more precisely- a block of several fields.

Let's take an example. Suppose you are building a website with recipes where you want to use Custom Fields to list ingredients

  • before having the Repeatable Field, the only way was to create a bunch of Custom Fields like
    • Name of ingredient 1
    • Quantity of ingredient 1
    • Name of ingredient 2
    • Quantity of ingredient 2
    • Name of ingredient 3
    • Quantity of ingredient 3
    • etc
      Actually, if you had 1 recipe with 20 ingredients you had to create 20 "Name of ingredient X" and 20 "Quantity of ingredient"... even if all the other recipes had a maximum of 5 ingredients.
      Been there, done that :)
  • thanks to that new Custom Field Type called "Repeatable Field" you only had to create 1 single Custom Field "Ingredient". In its configuration you would then specify that it consists of two fields: "Name" and "Quantity".
    And you were good to go! No matter the (maximum) number of ingredients, you were covered

This Repeatable Custom Field introduced at some point in Joomla 3 had

  • one huge quality: it did open a new field of possibilities
  • one drawback: only the following (native) Types could be used inside it
    • Editor
    • Media
    • Number
    • Text
    • Text Area

So in practice, it was not completely "universal" since you could not have

  • all the other native Custom Field Types like List, Radio, Color etc
  • nor any other Type of Custom Field coming from a third-party
    (see the previous Episode about Custom Fields for very long list of available Custom Fields, free or paid). Typical examples are
    • Videos
    • Maps
    • Related Article(s)
  • nor any custom-made Type of Custom Field designed especially for your use case
    (Custom Fields are indeed tiny plugins: it is very easy to duplicate an existing type of Custom Field to customize it to your needs, even without being an experienced coder)

Repeatable Field

So that is the reason why different people came with the idea of having a "truly repeatable" Custom Field, where you could select a series of literally any other Custom Field available on the website, be it native / third-party / custom-made.

It was a bit too late though to introduce that new feature in Joomla 3 since

  • Joomla 4 was (really) on the horizon at that point
  • and any new feature added in Joomla 3 would have required a new 3.x version and indirectly delayed Joomla 4

So a long story short: Custom Fields of Type "Repeatable"

  • have been dropped in Joomla 4
  • in order to be replaced by its more universal form: the Custom Fields of Type "Subform"

Custom Fields of Type "Subform" in Joomla 4

Use cases in general

What can be the use case for Custom Fields of Type "Subform"?

Actually the answer is just like for Legos©: you can really build anything you can think of.

My own typical use case is for a Short Film Festival. There are different Cultural Venues organising Sessions, each Session consisting of

  • a date/time
  • a series of Short Films where we typically display
    • The Title
    • The Duration
    • A Gallery with different pictures
    • A Video

So as you can guess:

  • there is no predefined number of Films for a given Session (it can be anything between 1 and 15)
  • some of the Fields related to Films can be native Custom Fields (Title and Duration for example) but others are more advanced third-party Custom Fields (Gallery and Video for instance)

The use case for this demo: a Carousel

For the sake of this presentation, we will build a Carousel.

There are multiple reasons for this choice:

  • for this example we only need native Custom Fields. This means that you can immediately reproduce the demo
  • it does not require any third-party library which is not already available with Joomla 4
  • last but not least, it will also be the opportunity to show how to do certain things properly with Joomla 4, like
    • adding CSS in an override / alternate layout
    • enabling only the necessary parts of the Boostrap 5 Javascript which ships by default with Joomla 4

Of course, it might be that you hate Carousels or that you never have the need for a Carousel. I respect that. Still the Carousel is a good example for a demo :)

If you have other use cases, please be constructive and post a comment below to explain what you achieve (if possible with a screenshot). This could give even more ideas to the readers.

Also it could be that the code below is not perfect. I am open to all suggestions for improvement of course ;) But after all this is also the point of the current article: even a non-coder like me can easily make an override and build powerful things thanks to Joomla's features and flexibility.

Step-by-step procedure

Step 1 - creating the basic Custom Fields

The first step will be to create the Custom Fields needed for our Carousel.

For each Slide of the Carousel we want:

  • a Slide Title => a CF (Custom Field) of Type Text
  • a Slide Duration => a CF of Type Integer
  • a Slide Image => a CF of Type Media

Since I am creating these on a newly created Joomla 4 website, these Custom Fields have respectively the IDs 1, 2 and 3. If you had already created Custom Fields before they will have other IDs. This is not a problem of course. All you will need to do is adapt the respective IDs in the code below.

As usual, for each Custom Field you can specify

  • Field Group (if you feel like creating one to keep your site organised)
  • Access (Public, Guest, Super Users, …)
  • Language

But on the "General" Tab note the new switch called "Only Use In Subform". In this example, I want to enable that switch because those three CF will only be used in the context of the CF of Type Subform. Note that enabling this switch does hide the Category assignment field, which is quite logical: anyway you don't want to display these CF directly on the Article Editing Form since all you want to display is the "parent" CF of Type Subform.

Native Custom Field

Step 2 - creating the Custom Field of Type Subform

Now the exciting part: let's create the Custom Field of Type Subform. In the area named "Fields" at the bottom of the screen, add the different basic CF created above.

According to your preference, you could assign this CF to all Categories or only to a selection of Categories. 

Custom Field of Type Subform

So eventually you will end up with such a list of Custom Fields (note: in Part 1 we don't make use of the "Slide Free Text (editor)" Custom Field. So please ignore it until Part 2):

All Custom Fields for a Carousel

Step 3 - create a few articles

Create a new Category and there create a few Articles. For each of them fill in at least

  • a Title (and an Alias)
  • the CF of Type Subform with several values for Title / Duration / Image

Article Creation with Custom Field of type Subform

Step 4 - create a menu item

In order to display our Articles in the front-end, let's create a Menu Item of Type Blog pointing to the wished Category.

Step 5 - create an Alternate Layout for the Custom Field

There are two ways to achieve this

  1. either manually: see the section "Create an Alternate Layout for the Custom Field" in the following Joomla Community Magazine article published in May 2021: Explore the Core! Play with Custom Fields to enrich your content or your design
  2. either the usual way via Joomla's interface
    1. go to System > Site Templates > Cassiopeia Details and Files > Create Overrides > Layouts > com_fields
    2. click on "field"
    3. you get the following confirmation message: "Override created in /templates/cassiopeia/html/layouts/com_fields/field"

Whatever the way you choose there are then two methods to rename / edit that newly created file named by default "render.php":

  1. either by staying in that Joomla's interface > Tab Editor > html > layouts > com_fields > field
  2. either via your (s)FTP client or IDE (which is my preferred method because it is easier to do and undo changes)

If you don't rename that "render.php" file then any change you make to it will apply to all Custom Fields in all circumstances. This is called an "Override".

What we want is similar but slightly different: we want our changes to apply not by default but only when we want it. With other words we want to be able to assign our file in certain cases. This is called an "Alternate Layout" (or "Alternative Layout").

This is a two-step process:

  1. first rename that "render.php" file into something explicit. Examples: marc.php, carousel.php or in this case rawvalue.php. Note that dashes are allowed in the filename (but not underscores afaik)
  2. then edit the chosen Custom Field and go to Tab Options > Render Options > Layout. There you find a dropdown which lists all available Alternate Layouts. Select the newly created & renamed file

See the corresponding screenshots hereafter:

Alternate Layout

Assigning an Alternate Layout to the Custom Field

Step 6 - Alternate Layout - version 1 - the rawvalue

It is now time to edit our Alternate Layout as follows:

<?php
/**
 * @package     Joomla.Site
 * @subpackage  com_fields
 *
 * @copyright   (C) 2016 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
defined('_JEXEC') or die;

use Joomla\CMS\Language\Text;

if (!array_key_exists('field', $displayData))
{
	return;
}

$field = $displayData['field'];
$label = Text::_($field->label);
$value = $field->value;
$showLabel = $field->params->get('showlabel');
$prefix = Text::plural($field->params->get('prefix'), $value);
$suffix = Text::plural($field->params->get('suffix'), $value);
$labelClass = $field->params->get('label_render_class');
$renderClass = $field->params->get('render_class');

if ($value == '')
{
	return;
}

?>
<?php

// the block ABOVE comes from the original render.php file. The block BELOW is custom-made and replaces the rest of the original render.php file.

// Get the rawvalue and json_decode it
$rawvalue = $field->rawvalue;
$items = json_decode($rawvalue, true);

// this would display the value of the Custom Field taking into account the potential render class of the Custom Field
echo '<h2>Displaying the <strong>value</strong> of the Custom Field</h2>';
echo '<span class="field-value ' . $renderClass . '">' . $value . '</span>';

// this would display the rawvalue (json) of the Custom Field
echo '<h2>Displaying the <strong>rawvalue (json)</strong> of the Custom Field</h2>';
echo '<small><pre>'.print_r($items, true).'</pre></small>';

// this would display the rawvalue (json) as unordered list
echo '<h2>Displaying the <strong>rawvalue (json)</strong> as unordered list</h2>';
echo '<ul>';
foreach ($items as $item) {
    echo
      '<li>Slide<ul>'.
         '<li>' . ( $item['field1'] ?? '' ) . '</li>' .
         '<li>' . ( $item['field2'] ?? '' ) . '</li>' .
         '<li>' . ( $item['field3']['imagefile'] ?? '' ) . '</li>' .
      '</ul></li>';
}
echo '</ul>';

// this would display all what $displayData contains and that we could potentially use in an override / alternate layout
echo '<h2>Displaying the <strong>$displayData</strong> variable</h2>';
echo '<small><pre>'.print_r($displayData, true).'</pre></small>';
?>

As you will notice, here we have left intact the first block of the original render.php file but we have adapted the rest in order to fetch the "rawvalue" of the CF (ie what is technically written in the database) and not its "value" (which means the "rendered value" in the Joomla context).

Go to the front-end of your site and see the result. It will display respectively

  1. the value of the Custom Field
  2. the rawvalue (json) of the Custom Field
  3. the rawvalue (json) as unordered list
  4. all what $displayData contains and that we could potentially use in an override or alternate layout

See the corresponding screenshots hereafter:

Value of the Custom Field

Rawvalue of the Custom Field

Rawvalue of the Custom Field as unordered list

displaydata variable

Step 7 - Alternate Layout - version 2 - the Carousel on the Article View only

Create a new Alternate Layout following the procedure explained above.

Rename that file my-carousel-article-view-only.php (for example) and copy-paste the following code inside.

<?php

defined('_JEXEC') or die;

if (!array_key_exists('field', $displayData))
{
	return;
}

$field = $displayData['field'];
// Get the rawvalue
$rawvalue = $field->rawvalue ?? '';
$carouselContainerId = 'carousel-field'. $field->id . '-'. md5($rawvalue, false); // giving a unique ID to the carousel container. NB: the ID cannot start with a digit
// getting he article ID by making an override of components/com_fields/layouts/fields/render.php
echo $displayData['itemid'];

if ($rawvalue == '')
{
	return;
}

?>

<?php
use Joomla\CMS\Factory; // necessary bc we use herafter Factory::getApplication() and Factory::getDocument()
$app = Factory::getApplication(); // JFactory is indeed deprecated in J!4
$view = $app->input->getCMD('view', ''); // "view" would output "article" or "category" for example
// $id = $app->input->getCMD('id', ''); // "id" gives the id of the category if blog view, the id of the article if article view. So not useful here
if ('article' !== $view) {
//    return; // comment this line if you want the carousel to also display on the blog view for example
}
?>

<?php // https://docs.joomla.org/J4.x:Using_Bootstrap_Components_in_Joomla_4
\Joomla\CMS\HTML\HTMLHelper::_('bootstrap.carousel', '#' . $carouselContainerId, ['interval' => 3000, 'pause' => 'false']); // selector is necessary for the potentials Options to work ?>

<?php $items = json_decode($rawvalue, true); ?>

<div id="<?php echo $carouselContainerId; ?>" class="carousel slide carousel-fade" data-bs-ride="carousel">
	<div class="carousel-indicators">
		<?php $first=true; $i=0; ?>
			<?php foreach($items as $item): ?>
				<button <?php if ($first) {echo "class=\"active\""; $first=false;} ?> type="button" data-bs-target="#<?php echo $carouselContainerId; ?>" data-bs-slide-to="<?php echo $i; ?>" aria-current="true" aria-label="<?php echo "Slide " . $i+1; ?>"></button>
			<?php $i=$i+1; ?>
		<?php endforeach; ?>
	</div>
	<div class="carousel-inner">
		<?php $first=true; ?>
		<?php foreach($items as $item): ?>
			<?php
				// Note: field1 in $item['field1'] refers to the Custom Field having ID 1 - nothing to do with the Order of fields within the Custom Field of Type SubForm
				// If necessary adapt the next three lines according to the ID of the Custom Fields on *your* site
				$slideTitle = $item['field1'] ?? '';
				$slideDuration = $item['field2'] ?? '1';
				$slideImage = $item['field3']['imagefile'] ?? '';
			?>
			<div class="carousel-item <?php if ($first) {echo "active"; $first=false;} ?>" data-bs-interval="<?php echo 1000*$slideDuration; ?>">
				<img class="d-block w-100" src="/<?php echo $slideImage; ?>" />
				<div class="carousel-caption d-none d-md-block">
					<h5><?php echo $slideTitle; ?></h5>
				</div>
			</div>
		<?php endforeach; ?>
	</div>
	<button class="carousel-control-prev" type="button" data-bs-target="#<?php echo $carouselContainerId; ?>" data-bs-slide="prev">
		<span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="visually-hidden">Previous</span>
	</button>
	<button class="carousel-control-next" type="button" data-bs-target="#<?php echo $carouselContainerId; ?>" data-bs-slide="next">
		<span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="visually-hidden">Next</span>
	</button>
</div>

<?php
$carouselCSS = <<<MYCSS
/* example of CSS for bootstrap.carousel - adding a background to the carousel-caption - see https://ui.glass/generator/ for glassmorphism CSS */
.carousel-caption {
bottom: 40%; /* otherwise with our background too close from Indicators with the default 1.25rem */
left: 25%; /* to make it narrower than with the default 15% */
right: 25%; /* to make it narrower than with the default 15% */
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
background-color: rgba(0,0,0,0.5);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.125);
}
/* by default the custom fields appear in a Unordered List. To make the carousel start on the left and hide the bullet point we use a negative margin as a first approach */
li.field-entry.mycarousel {list-style-type: none; margin-left: -2rem}
MYCSS;

// use Joomla\CMS\Factory; // is already called above so commented here bc can only be put once in the file
$doc = Factory::getDocument();
$doc->addStyleDeclaration($carouselCSS); // CSS will be injected only once even if this layout is called multiple times on a page

// Ununeeded stuff, just needed during testing/development. Uncomment the 'return' to execute the code hereafter
return;

echo 'test';
?>

Edit your CF of Type Subform and assign that new Alternate Layout as explained above.

Go to the front-end of your site and see the result.

  • On the Blog View, the Custom Field "Carousel" does not appear any more.
    This is meant :)
    We will see why in Part 2 of the current Episode 7 about Custom Fields. And of course we will also provide a nice solution for that ;)
  • Open one of the Articles of the Category. There it is: our fully functional Carousel!

Here is an animated gif of the result (the quality of the image is voluntarily poor in order to keep the file as light as possible)

Animated GIF of the Carousel

Explanations about the code

In the order of appearance, here are some comments about the code.

What is this ?? ''

On the following row we see ?? ''

$rawvalue = $field->rawvalue ?? '';

It is a short notation -called Null Coalescing Operator- allowing to assign a value if the variable is NULL. More information on https://www.php.net/manual/en/language.operators.comparison.php 

Factory

use Joomla\CMS\Factory;

This line is necessary because later in the code we use Factory::getApplication() and Factory::getDocument()

JFactory

JFactory is deprecated in J!4. The correct code is now the following:

$app = Factory::getApplication();

getCMD('view', '')

In this first example we don't want to show the CF on the Blog View.
So how to know whether we are in the case of an "article" or a "category" View for example?

Thanks to this variable:

$view = $app->input->getCMD('view', ''); 

Display only on an Article View (and not on a Blog View)

Thanks to the following line, the rest of the code will be ignored.

Note the "Yoda Condition" which is a good practice to avoid errors or unexpected results in your code if you make typos: https://en.wikipedia.org/wiki/Yoda_conditions 

if ('article' !== $view) { return;}

Calling only the necessary javascript from Boostrap 5 shipping with J!4

Joomla 4 ships with Bootstrap 5. But to make your websites a.o. more performant, BS5 javascript is not loaded by default. Instead you can decide yourself in your overrides / alternate layouts to enable only what you need and when & where you need it.

It is exactly what the following line does, calling what is needed for a carousel to work:

\Joomla\CMS\HTML\HTMLHelper::_('bootstrap.carousel', '#' . $carouselContainerId, ['interval' => 3000, 'pause' => 'false']);

For more information see the Official Documentation: https://docs.joomla.org/J4.x:Using_Bootstrap_Components_in_Joomla_4 

The Carousel HTML code

For the Carousel code, I simply took an example from the official BS website and replaced of course in their HTML code all the given Titles / Images / etc by the respective rawvalues fetched from our CF of Type Subform.

See different examples of Bootstrap Carousels on https://getbootstrap.com/docs/5.1/components/carousel/.

Note: field1 in $item['field1'] refers to the Custom Field having ID 1. So nothing to do with the Order of fields within the Custom Field of Type SubForm. If necessary adapt the code according to the ID of the Custom Fields on *your* site.

Adding custom CSS

The following two lines allow to inject our CSS in the Head of of the Page:

$doc = Factory::getDocument();
$doc->addStyleDeclaration($carouselCSS); 

Note: CSS will be injected only once even if this layout is called multiple times on a page

Using a variable for the CSS

Writing CSS on multiple lines can sometimes be cumbersome, depending on the notation you are using. Here we have opted for the use of a variable $carouselCSS with the "heredoc syntax". Very handy!

More information on https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc.

Return

A bit before the end of the file, there is a return command. This can be handy in the sense that any code below will be ignored. So when you care making tests, you can simply comment this line in order to excecute any test code which is below.

return;
1
Leadership Interviews: Nicola Galgano
Help4Joomla needs you!
 

Comments

Already Registered? Login Here
No comments made yet. Be the first to submit a comment

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