Skip to main content
Version: 4.0 preview

Assets (CSS) & JavaScript (AMD)

Version
4.0 preview
Updated
View markdown

How to add your own CSS and JavaScript to the CBX frontend/admin without a build step. Two things make this different from a normal Joomla extension: there is no bundler/build pipeline — JS is served as AMD modules (RequireJS) directly — and CBX has a small set of auto-detected customization entry points plus a per-view/per-property asset-injection engine that also works for XHR-loaded views. This guide maps all of it.

Read com_configbox_customization_overview.md first. For attaching CSS/JS to a specific property type, see com_configbox_custom_properties.md §8 — this guide covers the install-wide entry points and the RequireJS configuration; that one covers per-property asset hooks. All paths are relative to the component root docroot/components/com_configbox/. Source references are point-in-time (component 3.4.1) — verify against the code.


1. The mental model

  • No build step. There is no webpack/rollup/npm bundling for the component. JS files are AMD modules loaded at runtime by RequireJS (assets/main.js). A plain define([...], function(){}) file is all you ship.
  • A server→client config gateway. The server serializes an appConfig object into a data-app-config attribute on the RequireJS script tag; main.js reads it (no extra XHR). This is how your customization URLs/paths reach the loader (assets/main.js:14-23, assembled in helpers/view.php:61-114). Two of the config values are the XHR endpoints: urlEndpointBase (a /…/cb-api/__CONTROLLER__/__TASK__ template) and the urlXhr fallback — server.js posts every request/submitForm/injectHtml/… call to the resolved endpoint. See ../platform/joomla/com_configbox_sef_urls.md §6.
  • The configbox/custom namespace maps to your customization JS dir, so any module under it is loadable by id.
  • Auto-detected entry points. Certain customization files load automatically just by existing — no registration. (custom.css, custom.js, custom_questions.js.)
  • A view-asset engine. Each rendered view declares its stylesheets and JS init-calls; the loader injects them, de-duplicated, on first paint and on later XHR injection. Properties plug into the same engine.

2. The auto-detected entry points (drop a file, done)

The simplest customization: create one of these files and it loads everywhere, no config or registration.

FileLoads asTrigger
data/customization/assets/css/custom.cssa global stylesheet, added last so it can overridefile_exists() check in KenedoView::getStyleSheetUrls() (KenedoView.php:728-730); ordered last (:660-669)
data/customization/assets/javascript/custom.jsthe AMD module configbox/custom/custom, required at app startrequireCustomJs flag set by file_exists() (helpers/view.php:82); loaded in main.js:138-140
data/customization/assets/javascript/custom_questions.jsa second auto entry point for question/configurator JSrequireCustomQuestionJs flag (helpers/view.php:83)

So:

/* data/customization/assets/css/custom.css — site-wide style overrides, loaded last */
#com_configbox .cb-configurator-help { background: #f5f7ff; padding: 1rem; }
// data/customization/assets/javascript/custom.js (module id: configbox/custom/custom)
define(['cbj'], function (cbj) { // cbj is CBX's namespaced jQuery
'use strict';
cbj(document).on('click', '.cb-configurator-help', function () { /* … */ });
// Use document-delegated handlers here: this module loads once at app start.
});

custom.js is your global init module — loaded once per page. Use document-delegated event handlers (cbj(document).on(...)) so they survive XHR view injection. For per-view/per-property init that must run on each injection, use the view-asset engine (§4) or property hooks (custom-properties §8), not here.


3. Loading your own modules — the configbox/custom namespace

main.js registers (:43):

'configbox/custom': appConfig.urlCustomAssets + '/javascript', // → data/customization/assets/javascript

So any AMD module file under data/customization/assets/javascript/ is addressable as configbox/custom/<path-without-.js>:

data/customization/assets/javascript/widgets/gallery.js ⇒ module id "configbox/custom/widgets/gallery"
// data/customization/assets/javascript/widgets/gallery.js
define(['cbj'], function (cbj) {
return {
init: function (view) { /* wire up gallery within `view` */ }
};
});

Require it from anywhere (your custom.js, a property's getJsInitCalls*, another module):

cbrequire(['configbox/custom/widgets/gallery'], function (gallery) { gallery.init(cbj(document)); });

Useful built-in module ids you can depend on: cbj (namespaced jQuery 3.7 — always depend on this, not a global $), cbj.ui, bootstrap/cbj.bootstrap, cbj.chosen, cbj.colorbox, plus the app modules (configbox/configurator, configbox/cart, configbox/user, …). See the full paths map in assets/main.js:40-72.

Context: CBX runs RequireJS under the context 'CB' and exposes window.cbrequire for it (main.js:29,135). Use cbrequire, not the global require, to stay in CBX's context and avoid clashes with other software on the page.


4. The view-asset engine (per-view CSS + JS, XHR-safe)

This is how CBX loads assets for a specific view, including views injected by XHR after the initial page load — the mechanism your properties and custom views plug into.

Server side: each rendered .cb-content view wrapper carries data attributes — data-stylesheets, data-init-calls-once, data-init-calls-each — built from the view's getStyleSheetUrls() + getPropertyStyleSheetUrls() and its JS init calls. The stylesheet list is merged, de-duplicated, minified-variant-resolved and cache-busted (KenedoView.php:648-705).

Client side (main.js:159-302): on first paint and on every cbViewInjected event, onViewsInjected() walks each .cb-content:not(.view-init-started) and:

  • marks the view view-init-started immediately, claiming it so the next cbViewInjected does not walk it again;

  • injects its stylesheets if not already in <head> (de-duped against #cb-stylesheets);

  • parses each JS init call of the form "moduleId::method", cbrequires the modules, and calls module.method(view) — passing the view (a jQuery collection) as the argument;

  • runs init-calls-once at most once per page (tracked in doneModuleCalls) and init-calls-each every time the view appears;

  • marks the view view-init-done and fires cbViewInitialized once those calls have actually run.

Knowing when a view is ready

The two markers are a start line and a finish line, and only the finish line means ready. view-init-started is set synchronously, before anything else happens to the view, while the init calls themselves sit inside an asynchronous cbrequire callback. So a view carries view-init-started while its modules are still loading and its handlers do not exist yet. view-init-done is added when they do.

That gap is silent and it bites: CBX's buttons are <a> elements with no href (<a class="… trigger-add-to-cart">), so a click landing in the gap does nothing at all — no navigation, no console error, nothing to see. Code that waited on the start line therefore worked on a warm cache and failed on a cold one.

Renamed from view-processed (2026-08-16). That name read like a finish line and was used as one all over the codebase and the E2E suite; it never was one. The pair view-init-started / view-init-done cannot be misread the same way. If you have a customization keying off view-processed, it is now view-init-started — but check first whether it actually wanted view-init-done, because that is what almost every such use meant.

view-init-done closes it. It is added, and cbViewInitialized fired on document with the view element as its argument, only after every init call for that view has returned:

// In-page: run after ConfigBox has bound the view's handlers.
cbj(document).on('cbViewInitialized', function(event, view) {
if (view.hasClass('view-cart')) { /* the cart's handlers are live */ }
});
// From outside the page (a browser test, a screenshot script): wait for the class.
await page.locator('.kenedo-view.view-cart.view-init-done').waitFor();

A view with no init calls at all gets both immediately — there is nothing to wait for. If an init call throws (a moduleId::method that does not exist), the stamp deliberately never lands, so a waiter fails against that view instead of timing out somewhere unrelated later.

It means the init calls RETURNED, not that everything they started has settled. An init call that itself cbrequires more modules is still working after it returns, and view-init-done is already on. Whatever a handler binds synchronously is guaranteed — that covers the delegated trigger-… click handlers, which is what almost every caller actually needs. Anything bound deeper needs its own marker, which is exactly what .questions-init-done is: initConfiguratorPage returns (and view-init-done lands) two module loads before the question types have bound theirs.

Two narrower markers predate this one and remain, because they answer narrower questions: body.cb-admin-ready (Kenedo's shared admin handlers are bound — set once per page, so it says nothing about a view injected afterwards) and .questions-init-done on the configurator page (every question type has bound its handlers, which happens inside the configurator's own init).

You rarely call this engine directly. You feed it via:

  • Property assetsgetStyleSheetUrls() / getJsInitCallsOnce() / getJsInitCallsEach() on a property (the most common path). Fully covered in com_configbox_custom_properties.md §8.
  • View assets — override getStyleSheetUrls() on a custom view to add view-scoped CSS (it flows through the same pipeline, §2's custom.css is appended via this method too).

…Each must be idempotent. A view can be injected (and thus initialized) more than once. Scope work to the passed view and guard already-initialized nodes with a marker class — see the property guide §8 for the canonical pattern.


5. Adding RequireJS paths & shims (third-party libraries)

To load a third-party AMD/UMD library by a clean module id (or to shim a non-AMD library), define global functions the server picks up — cbGetCustomRequirePaths() and cbGetCustomRequireShims() (helpers/view.php:106-114). Their return values become appConfig.customPaths / appConfig.customShims, which main.js merges into the RequireJS config (main.js:91-106):

// In a boot/settings file or a system_overrides file (loaded at boot) — see the boot-hooks guide.
function cbGetCustomRequirePaths() {
return array(
// module id => URL (no .js extension). Point at your customization assets.
'mylib' => KenedoPlatform::p()->getUrlCustomizationAssets().'/javascript/vendor/mylib.min',
);
}

function cbGetCustomRequireShims() {
return array(
// For non-AMD libs: declare exports/deps so RequireJS can load them.
'mylegacylib' => array('deps' => array('cbj'), 'exports' => 'MyLegacyLib'),
);
}

Then cbrequire(['mylib'], function (mylib) { … }) works anywhere. Define these functions where boot code lives (the customization settings dir or system_overrides/) — see com_configbox_system_overrides_and_boot_hooks.md. (For a property's JS you usually don't need a custom path — configbox/custom/... already resolves your dir, §3.)


6. Minification & cache busting (what to know)

Both are admin settings (use_minified_js, use_minified_css, use_assets_cache_buster) and are handled for you — but a few behaviors affect how you ship files:

  • .min files are build artifacts, not sources. In the CBX repos they are gitignored and generated by build/minify-js.sh / build/minify-css.sh (inside the component) at packaging time. A release install has them; a development checkout does not — and does not need them:
  • CSS: when use_minified_css is on, the pipeline swaps foo.cssfoo.min.css only if that file exists (for both system and customization assets, KenedoView.php:674-694). So shipping a .min.css alongside your .css is optional; if absent, your plain file is used.
  • JS: when use_minified_js is on, .min is appended to RequireJS paths — except configbox and configbox/custom, which are deliberately excluded (main.js:109-132). So your customization JS modules are never expected to have .min variants — ship plain .js. (The two auto entry points and anything under configbox/custom load unminified by design.)
  • The sentinel: because a missing core .min.js would 404, the loader checks for assets/main.min.js first (helpers/view.php, getAmdLoaderJs()): if the minified build isn't present on the install, CBX serves plain sources regardless of the use_minified_js setting. You never have to toggle the setting on a dev site — it self-corrects.
  • Cache busting appends ?version=… to system + customization asset URLs (main.js:37, KenedoView.php:696-701). After changing a file, a cache-buster bump (or disabling the buster in dev) ensures clients reload it.

7. Deployment checklist

data/customization/assets/
css/
custom.css ← auto-loaded site-wide, last (override layer)
<feature>.css ← view/property-scoped CSS (referenced from getStyleSheetUrls)
javascript/
custom.js ← auto-loaded global init module (configbox/custom/custom)
custom_questions.js ← auto-loaded question/configurator init module
<path>/<module>.js ← any AMD module → id "configbox/custom/<path>/<module>"
vendor/<lib>.js ← third-party libs (register via cbGetCustomRequirePaths)
  1. Site-wide tweak? Drop css/custom.css and/or javascript/custom.js — they auto-load, no config.
  2. A reusable module? Put it under javascript/… and require it as configbox/custom/… (§3). Depend on cbj, not a global $; use cbrequire, not require.
  3. Per-view/per-property assets? Feed the view-asset engine via property hooks (custom-properties §8) or a view's getStyleSheetUrls() (§4). Make …Each init idempotent.
  4. Third-party library? Register a path/shim via cbGetCustomRequirePaths()/cbGetCustomRequireShims() in a boot/system-override file (§5).
  5. Ship plain files — no .min needed for configbox/custom JS (§6); .min.css optional.
  6. Bust the cache after changes (or disable the buster in dev).
  7. Verify manually — confirm assets load on a full page load and after an XHR view injection (admin forms often inject), and that init runs once vs. each as intended. CBX has zero automated tests.

8. Conventions & gotchas

  • AMD, no build. Ship define(...) files; there is no bundler. Load via RequireJS.
  • Use cbj and cbrequire. cbj is the namespaced jQuery; cbrequire keeps you in the 'CB' RequireJS context. Don't assume a global $/require.
  • Auto entry points load by existingcustom.css, custom.js, custom_questions.js. No registration; just create them. custom.css loads last (override-friendly).
  • configbox/custom/... is your namespace for arbitrary modules → data/customization/assets/javascript/.
  • Don't .min your custom JSconfigbox/configbox/custom are excluded from min-path rewriting (§6); a foo.min would 404.
  • …Each JS is idempotent; views inject more than once. Scope to the passed view, guard with a marker class.
  • Scope your CSS to #com_configbox/the view/property wrapper so it can't leak; custom.css is global by design but still scope its selectors.
  • paths/shims need a boot hookcbGetCustomRequirePaths/cbGetCustomRequireShims are read at boot; define them where boot code runs (§5, boot-hooks guide).

See also

  • com_configbox_custom_properties.md §8 — attaching CSS/JS to a specific property type (getStyleSheetUrls, getJsInitCallsOnce/Each) — the per-property side of the same engine.
  • ../technical/com_configbox_configurator_questions.md — what custom_questions.js is for: the configurator question/answer loop, the configurator.registerQuestionType() interface, and the cb* custom events a question module hooks into (§4, §7).
  • com_configbox_system_overrides_and_boot_hooks.md — where to define cbGetCustomRequirePaths/…Shims (boot-time functions).
  • com_configbox_overriding_views_and_templates.md — adding view-scoped CSS via a custom view's getStyleSheetUrls().
  • assets/main.js — the AMD bootstrap: appConfig gateway (:14), configbox/custom path (:43), custom paths/shims (:91-106), min handling (:109-132), the view-asset engine (:159-302).
  • helpers/view.phpgetAmdLoaderJs() (:61): the appConfig assembly, the auto-detected entry-point flags (:82-83), and the cbGetCustomRequirePaths/Shims hooks (:106-114).
  • external/kenedo/classes/KenedoView.php — the stylesheet pipeline (getStyleSheetUrls :713, optimization/ordering :648-705).