Work In Progress

This book is currently work in progress. Some sections are not yet written. Thank you for your understanding!

Web Asset Management

A large part of our work as Joomla extension developers is to load static assets, CSS and JavaScript files, in the user-facing HTML output of our extensions.

In older Joomla versions we did that through the HTML document and using HTMLHelper static methods to load dependencies. For example:

$doc = \Joomla\CMS\Factory::getApplication()->getDocument();
Joomla\CMS\HTML\HTMLHelper::_('bootstrap.tooltip', '.hasTooltip');
Joomla\CMS\HTML\HTMLHelper::_('script', 'com_example/something.js', [
  'version'       => 'auto',
  'relative'      => true,
  'detectDebug'   => false,
  'framework'     => false,
  'pathOnly'      => false,
  'detectBrowser' => false,
], [
  'defer' => true,
  'async' => false,
]);        

This has a few shortcomings, as we have all discovered to our despair.

First of all, we need to remember to load all dependencies in the correct order before our own CSS or JavaScript file. In the above example, the something.js file depends on Bootstrap's Tooltip helper. If we forget the first call to HTMLHelper our JavaScript file will be broken.

However, our extension is not the only thing running on the page, right? Now, see the second parameter in that first HTMLHelper call? It tells Joomla's Bootstrap HTML helper to initialise the Tooltip helper so that anything with the class hasTooltip will have a Bootstrap tooltip. Since this is a component we are confident that this will always be the case. Oh, really? If a plugin ran before us and it also loaded its own JavaScript which also depends on Bootstrap's tooltip BUT had no second argument (or a different second argument) do you care to guess what our code above will do? If you guessed “sod all” you'd be right and a Joomla extensions development veteran! So, yup, a third party extension having a JavaScript file with the same dependency as ours running before us breaks our perfectly working JavaScript. This could even happen within the same extension if the aforementioned code appeared in a layout and your page just happens to load two layout which both call Joomla\CMS\HTML\HTMLHelper::_('bootstrap.tooltip') with a different second parameter. Great!

Beyond that, what happens if our something.js gains another dependency and we are loading this file in six different places… but missed updating one of them? Why, yes, that sixth page will be broken! Even worse, we might only update our code with the additional dependency in one place (let's say a layout), another four places not updated work because they are loading that changed layout and the sixth and final place does not work because it was neither updated nor trying to load something else which loads the dependency for us.

Let's put it this way. If you do manual dependency management you will be in a world of pain, sooner rather than later.

Joomla 4 addresses this problem by introducing the Web Asset Manager (WAM). The WAM is responsible for loading our CSS and JavaScript files and their dependencies. It can figure out simple dependency chains even across multiple extensions and make sure that things are loaded in an order that makes sense and will do what we wanted it to do.

For that to work we need two things:

  1. Using Joomla's media directory the way it was intended ever since its introduction in Joomla 1.5.0, back in 2007.

  2. A joomla.assets.json file which describes our static assets and their dependencies.

On the first point please let me remind you how the media folder works. If you have any static assets which must be web accessible they MUST be placed in a subdirectory of the media folder named after your extension (as Joomla names it internally). Don't put them in your extension's directory, don't put them in cache or tmp (they are NOT web accessible, they CAN be moved around even outside the web root AND their contents can and will be removed anytime).

You have a component named com_foobar? Put your static assets in media/com_foobar. You have a module named mod_example? Put your static assets in media/mod_example. You have a plugin named example in the folder system? Put your static assets in media/plg_system_example. You have a template named beauty? Put your static assets in media/tpl_beauty. Its subdirectories are css for CSS files and js for JavaScript files. It's simple, it's efficient, it's how Joomla is meant to work.

The second point, the joomla.assets.json file, tells Joomla where to find what, what depends on what else and how it all fits together. This file is placed in the extension's media subdirectory.

For example, a component com_example could have a media/com_example/joomla.asset.json file which looks like this:

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "com_example",
  "version": "1.0.0",
  "description": "This file contains details of the assets used by the Example component by Acme, Inc.",
  "license": "GPL-2.0-or-later",
  "assets": [
    {
      "name": "com_example.backend",
      "description": "Backend styling.",
      "type": "style",
      "uri": "com_example/backend.min.css",
      "dependencies": [
        "com_example.typography"
      ]
    },
    {
      "name": "com_example.typography",
      "description": "Fancy typography.",
      "type": "style",
      "uri": "com_example/typography.min.css",
      "dependencies": [
        "fontawesome"
      ]
    },
    {
      "name": "com_example.backend.items",
      "description": "JavaScript for the backend Items page.",
      "type": "script",
      "uri": "com_example/backend_items.js",
      "attributes" : {
        "defer": true
      },
      "dependencies": [
        "core"
      ]
    },
    {
      "name": "com_example.backend.items",
      "type": "preset",
      "dependencies": [
        "com_example.backend#style",
        "com_example.backend.items#script"
      ]
    }
  ]
}        

We declare various named assets. The com_example.backend style asset loads the backend.min.css file. However, that file depends on the com_example.typography asset which loads the typography.min.css file. In its turn, this asset depends on the core fontawesome asset which loads the FontAwesome icon font in any way Joomla figures out is appropriate.

When we tell Joomla to load the com_example.backend style asset it will first load the CSS files for FontAwesome (if our backend template has not already loaded it), then our typography.min.css file and finally our backend.min.css file. This all happens automatically. All we have to do in our extension's template layout code is

$this->document->getWebAssetManager()->useStyle('com_example.backend');

If at a later point we decide that the backend style needs to depends on yet another CSS asset we will add it to its dependencies array in the joomla.asset.json file and we are done. We do not have to touch our view templates. We do not have to think about anything else. Joomla will figure it out. No more hard to track down bugs!

You may have noticed that we also declared a script asset called com_example.backend.items which loads the file backend_items.js deferred. Deferred means that we tell the browser to load it after it has finished initialising the DOM. This means that we do not need to add any special code to execute something after the DOM is initialised which saves us a lot of frustration and bugs. We use the script resource like this:

$this->document->getWebAssetManager()->useScript('com_example.backend.items');

We have told Joomla that our script only depends on core, i.e. the Joomla core JavaScript. This is not mandatory, but something you will see plenty of times because you'll be using Joomla.getOptions in your JavaScript code to retrieve settings passed from the backend to the frontend. This is the recommended method instead of setting arbitrary JavaScript variables in inline JavaScript code. In fact, using inline JavaScript code is discouraged (but not forbidden) in Joomla 4 and later.

If at a later point we modify our JavaScript to also depend on Bootstrap's Modal dialog helper we will just update its dependencies:

    {
      "name": "com_example.backend.items",
      "description": "JavaScript for the backend Items page.",
      "type": "script",
      "uri": "backend_items.js",
      "attributes" : {
        "defer": true
      },
      "dependencies": [
        "core",
        "bootstrap.modal"
      ]
    }

That's it! No more hunting down usages of this JavaScript file and updating our view template code.

You can of course tell Joomla to load both CSS and JavaScript assets. The simplest way is being descriptive in our view template:

$this->document->getWebAssetManager()
  ->useStyle('com_example.backend')
  ->useScript('com_example.backend.items');

(note that useStyle and useScript return the WAM object which means they can be chain-called)

However, this runs the same risk as loading assets the old-fashioned way. What happens if we decide that the Items page needs some extra CSS which does not apply to the rest of our component's backend? We'd have to edit the template layout file. Enter bugs.

Instead of being descriptive we can be prescriptive using another WAM feature called presets. A preset consists entirely of dependencies. We declared it in our JSON file like this:

  {
    "name": "com_example.backend.items",
    "type": "preset",
    "dependencies": [
      "com_example.backend#style",
      "com_example.backend.items#script"
    ]
  }
        

and we can load it in our view template very easily like this:

$this->document->getWebAssetManager()->usePreset('com_example.backend.items');

Note that the preset asset's key is the same as our script asset's key. Script, style and preset assets are separate collections which means we can reuse the same key across them. Joomla will not be confused. We tell it which collection to look into by using a different WAM method: useScript, useStyle or usePreset.

Now let's see why presets are the bee's knees. Let's say we decided that Items also needs some special styling in a separate CSS file called items.min.css. We will just add this asset to our JSON file and update the preset:

    {
      "name": "com_example.backend.items",
      "description": "Backend styling just for the Items page.",
      "type": "style",
      "uri": "items.min.css",
      "dependencies": [
        "com_example.backend"
      ]
    },
    {
      "name": "com_example.backend.items",
      "type": "preset",
      "dependencies": [
        "com_example.backend#style",
        "com_example.backend.items#style",
        "com_example.backend.items#script"
      ]
    }
        

(You may notice that our com_example.backend.items asset depends on com_example.backend. I didn't have to do that, but I like to be explicit about dependencies to avoid any stupid bugs if I remove any intermediate dependencies in a dependency chain.)

We do NOT have to touch our view template file. Since we are telling it to load a preset, changing the preset is enough for Joomla to figure out what it needs to do.

Using the Web Asset Manager correctly can be a massive asset (no pun intended!) in your extensions' public frontend. Your view templates can load your prescriptive presets. If you decide you want to change something you can change the preset. Your clients who have created template overrides will NOT need to update their overrides. This means far fewer “bug” reports and more time for you to work on your code.

The Web Asset Manager has changed the way I write extensions and has solved a lot of my headaches. You can use the WAM on any component running on Joomla 4, regardless of whether you are using the “old” (Joomla 3) MVC or the “new” (Joomla 4) MVC. In fact, since it is a part of the Joomla document object, you can use it in modules, even plugins — however, if you are using it in a plugin you MUST tell the WAM to load your JSON file since Joomla will not do that by default for plugins.

Finally, the WAM is a much less error-prone method to injecting static assets to Joomla. All Joomla document classes have a WAM, even when they are not HTML; it just follows that if it's a non-HTML document adding an asset through WAM does nothing. Compare that with what happens if you try to use the HTMLHelper or the addScript / addStyle document methods when your document is not HTML. Yup, these old ways of adding static assets cause Joomla to error out. Again, WAM is safe, the methods of yesteryear are not. One more reason to migrate your extensions to WAM today.

Caveat: modules don't auto-load the joomla.asset.json file

Modules do not auto-load the joomla.asset.json file. You will need to load it yourself with:

$wa = \Joomla\CMS\Factory::getApplication()
  ->getDocument()
  ->getWebAssetManager();
$wa
  ->getRegistry()
  ->addRegistryFile(JPATH_ROOT . '/media/mod_mymodule/joomla.asset.json');

Alternatively, you can register and load each separate dependency in your view template file, like Joomla's core modules do, even though that's NOT a recommended or sustainable practice:

$wa = \Joomla\CMS\Factory::getApplication()
  ->getDocument()
  ->getWebAssetManager();
$wa
  ->registerAndUseScript(
    'mod_mymodule.something', 
    'mod_mymodule/something.min.js',
    [],
    ['defer' => true],
    ['core']
  );

Caveat: templates have a weird location for the joomla.asset.json file

In Joomla 3 and 4.0 you are supposed to put the template's static files inside your template's subdirectory: templates/TEMPLATE_NAME for front-end templates, or administrator/templates/TEMPLATE_NAME for back-end templates, where TEMPLATE_NAME is the name of your template without the tpl_ prefix.

In Joomla 4.1 and later versions you are supposed to put the template's static files inside a media subdirectory: media/templates/site/TEMPLATE_NAME for front-end templates, or media/templates/administrator/TEMPLATE_NAME for back-end templates, where TEMPLATE_NAME is the name of your template without the tpl_ prefix. As of Joomla 6 this will be the only supported method. It is definitely the only supported method since Joomla 4.1 for templates which have support for child templates.

In both cases you have to put your joomla.asset.json file inside your template's subdirectory: templates/TEMPLATE_NAME for front-end templates, or administrator/templates/TEMPLATE_NAME for back-end templates, where TEMPLATE_NAME is the name of your template without the tpl_ prefix. This may look a bit weird, but it's there for legacy reasons. Templates with the old-style static resources wouldn't work otherwise.