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:
-
Using Joomla's media directory the way it was intended ever since its introduction in Joomla 1.5.0, back in 2007.
-
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/
for front-end templates, or
TEMPLATE_NAME
administrator/templates/
for back-end templates, where TEMPLATE_NAME
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/
for front-end templates, or
TEMPLATE_NAME
media/templates/administrator/
for back-end templates, where TEMPLATE_NAME
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/
for front-end templates, or
TEMPLATE_NAME
administrator/templates/
for back-end templates, where TEMPLATE_NAME
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.