# Assets (CSS) & JavaScript (AMD)

> 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 i…

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/customization/assets_and_amd. Last updated 2026-08-16.

---
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.

| File | Loads as | Trigger |
|---|---|---|
| `data/customization/assets/css/custom.css` | a global stylesheet, added **last** so it can override | `file_exists()` check in `KenedoView::getStyleSheetUrls()` (`KenedoView.php:728-730`); ordered last (`:660-669`) |
| `data/customization/assets/javascript/custom.js` | the AMD module **`configbox/custom/custom`**, required at app start | `requireCustomJs` flag set by `file_exists()` (`helpers/view.php:82`); loaded in `main.js:138-140` |
| `data/customization/assets/javascript/custom_questions.js` | a second auto entry point for question/configurator JS | `requireCustomQuestionJs` flag (`helpers/view.php:83`) |

So:

```css
/* data/customization/assets/css/custom.css — site-wide style overrides, loaded last */
#com_configbox .cb-configurator-help { background: #f5f7ff; padding: 1rem; }
```

```js
// 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`):

```js
'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"
```

```js
// 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):

```js
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"`**, `cbrequire`s 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:

```js
// 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 */ }
});
```

```js
// 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 `cbrequire`s 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 assets** — `getStyleSheetUrls()` / `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`):

```php
// 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.css` → `foo.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 existing** — `custom.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 JS** — `configbox`/`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 hook** — `cbGetCustomRequirePaths`/`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.php` — `getAmdLoaderJs()` (`: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`).
