(written by Grok and reviewed by Max Milbers)
Audience: Extension and template developers (3rd party) and core contributors
File: components/com_virtuemart/assets/js/vm-init-registry.module.js
Global API object: window.VMInit
ES Module import name: vmInitRegistry
Status: VirtueMart 5
1. Why does this exist?
VirtueMart 5 increasingly loads JavaScript as ES Modules. Modules run asynchronously and in an unpredictable order. Older patterns (jQuery(document).ready(), inline scripts, “hope the timing works”) break as soon as:
- parts of the DOM are loaded or replaced via AJAX,
- libraries (Sortable, AutoComplete, Mustache, …) become available on
windowonly late, - several modules share the same dependencies,
- init code must run more than once (cart update, product refresh, dynupdate, …).
VMInit (the Init Registry) is the central, priority-sorted queue built so third-party developers can ship features without reinventing page load and AJAX glue:
- You register your init function once (
VMInit.add/vmInitRegistry.add). - VirtueMart starts the queue once at the end of the page (
VMInit.run()). - After AJAX, VirtueMart re-runs the handlers that opted in — you do not write your own “listen for every cart/product update” system.
That last point is reInit: the feature that makes AJAX-friendly extensions the default, not a weekend project. See §3 — AJAX made easy with reInit.
The registry also provides:
waitFor/waitForGlobal(wait for DOM orwindow.*),- retry with backoff,
- sync vs. parallel execution,
- debug logging (
VMInit.log/vmlog), - URL guards and clearer errors for broken
fetch()URLs.
2. How is the registry loaded?
In core:
vmJsApi::loadVmInitRegistry();
This loads, among other things:
vm-init-registry.module(ES Module),- and exposes the global object
window.VMInit.
At the end of the page, VirtueMart emits a small final trigger (#vm-final-init) that waits until VMInit exists and, ideally, at least one module has already registered, then calls:
VMInit.run();
As a 3rd-party developer you normally do not call VMInit.run() yourself. You only register your handler. The page starts the queue centrally.
Important: The registry is an ES Module. Load your own scripts with vmJsApi::addvScriptModule(...) so they are written into the HTML header as type="module". That starts the download early, in parallel with the rest of the page.
In addition, you can import the scripts you depend on from inside your module. That works very well against timing problems: the browser only continues your code after the import has finished. If the same module was already requested via the header, the import is usually cheap — the file is already cached or in flight, and the import effectively returns “it is there”. If it was not ready yet, the import waits. So you get both early loading and correct execution order.
VirtueMart therefore often uses this dual pattern: put required scripts into the header with addvScriptModule and load them again via import where you need them. Header for speed, import for guaranteed readiness. (See also the dual-loading pattern for third-party libraries later in this article — header script + globalizer module that imports and exposes window.XXX.)
3. AJAX made easy with reInit
Shop pages are not static. Cart mini-modules refresh, product areas swap via AJAX, filters reload lists, admin tabs inject markup. Without a shared init system, every extension author ends up writing the same fragile stack:
- bind on first load,
- somehow detect “content changed”,
- re-bind without double-binding,
- forget half the edge cases in production.
VMInit already solves this for you.
How it works for developers
| Setting | Behaviour |
|---|---|
reInit: true (default) |
Your JavaScript is registered for the first page run and is automatically run again after VirtueMart AJAX updates that trigger the registry. You do not need your own “after AJAX” solution. |
reInit: false |
Your JavaScript runs only once (first init). Ideal for one-shot setup: session keep-alive, global polyfills, things that must never re-bind. |
// Typical plugin: bind UI, survive cart / product AJAX automatically
VMInit.add('MyPlugin Feature', initMyFeature, 550, {
reInit: true // default — keep it true for DOM / UI work
});
// One-shot only
VMInit.add('MyPlugin Bootstrap', initOnce, 550, {
reInit: false
});
What you gain
- No custom AJAX re-init framework in your plugin — VirtueMart owns the re-run lifecycle.
- Same registration path for first paint and for every later update.
- Marketing pitch, but also engineering fact: if you play by the registry, your feature stays alive when the cart updates, dynupdate replaces HTML, or admin UIs refresh partial content — without you wiring each of those events by hand.
Your only real job
Write idempotent init code: if the same node is still there, skip or re-bind cleanly (dataset flags, WeakMap, early return). The registry will call you again; it will not invent duplicate-listener safety for free.
function initMyFeature() {
document.querySelectorAll('.js-my-feature').forEach((el) => {
if (el.dataset.myFeatureInit) return;
el.dataset.myFeatureInit = '1';
el.addEventListener('click', onClick);
});
}
Core (and well-behaved modules) call VMInit.reInit() after dynamic HTML changes. You usually do not call it yourself on shop pages — you just keep reInit: true and let the platform re-register the run for you.
4. Quick start for 3rd-party developers
4.1 Load your own module from PHP
// Filename without .js; path relative to VM JS assets (or as resolved by setPath)
vmJsApi::addvScriptModule('myplugin-init.module', 'plugins/vmcustom/myplugin/assets/js');
Or via the Joomla Web Asset Manager with type="module".
4.2 Register inside the ES Module
Option A – Import (recommended when your module lives next to VM assets):
import vmInitRegistry from '/components/com_virtuemart/assets/js/vm-init-registry.module.js';
// A relative import is fine if the path from your module is correct:
// import vmInitRegistry from '../../../../components/com_virtuemart/assets/js/vm-init-registry.module.js';
function initMyFeature() {
// DOM binding, event listeners, etc.
document.querySelectorAll('.js-my-feature').forEach((el) => {
// ...
});
}
// Priority >= 550 recommended for plugins (core uses max 500)
vmInitRegistry.add('MyPlugin Feature', initMyFeature, 550, {
retry: 3,
reInit: true, // default: survive AJAX automatically (see §3)
waitFor: '.js-my-feature', // optional: wait for DOM
sync: false // default: parallel with other async handlers
});
Option B – Global VMInit (when the module already runs in the global context):
if (typeof VMInit !== 'undefined' && typeof VMInit.add === 'function') {
VMInit.add('MyPlugin Feature', initMyFeature, 550, {
retry: 3,
reInit: true // stay alive after cart / product AJAX
});
} else {
console.error('[MyPlugin] VMInit not available – wrong load order?');
}
Option C – Plugin shortcut (default priority 800):
VMInit.addPlugin('MyPlugin Feature', initMyFeature, {
// options + optional priority override
priority: 600,
retry: 4,
reInit: true
});
addPlugin defaults to priority: 800 and retry: 8. That is intentionally after core.
5. API reference
5.1 VMInit.add(name, initFn, priority = 100, options = {})
| Parameter | Type | Description |
|---|---|---|
name |
string |
Unique display name (logging, error messages) |
initFn |
function |
Sync or async function; may return a Promise |
priority |
number |
Lower = earlier. Core ≤ 500. Plugins: 550+ |
options |
object |
see below |
Default options:
{
retry: 6, // max attempts on exception
waitFor: null, // CSS selector: wait for element before initFn
sync: false, // true = next entry waits for this one
reInit: true // true = re-run after AJAX (default); false = once only
}
5.2 VMInit.addPlugin(name, initFn, options = {})
Same as add, but with plugin-friendly defaults (priority: 800, retry: 8). Priority can be overridden via options.priority.
5.3 VMInit.run(isReInit = false)
Runs the sorted queue. Called by VirtueMart at the end of the page.
When isReInit === true, entries with reInit: false are skipped.
5.4 VMInit.reInit(ms = 120) — platform hook for AJAX
Schedules another run(true) after ms milliseconds.
For most extension authors this is not something you call — it is something VirtueMart (and core modules) call for you after dynamic HTML changes. Your side is the option flag:
reInit: true→ your code is included in that automatic re-run (default).reInit: false→ your code runs only on the first init.
You only call VMInit.reInit() yourself if you injected or replaced DOM outside the normal VM AJAX paths and need the whole registry to refresh.
// Only if *you* replaced HTML and core did not already reInit:
VMInit.reInit(); // 120 ms delay
VMInit.reInit(250); // custom delay
Full product story: §3 — AJAX made easy with reInit.
5.5 VMInit.waitFor(selector, timeout?) → Promise<Element|null>
Polls document.querySelector(selector) until the element exists or the timeout hits.
- Default timeout:
Virtuemart.recheckTimeout(default 8000 ms) - Poll interval:
Virtuemart.recheckInterval(default 50 ms)
const box = await VMInit.waitFor('#my-widget', 3000);
if (!box) return; // timeout – abort init
Two ways to use waitFor:
1. In the options of add (runs automatically before initFn):
VMInit.add('My Feature', initMyFeature, 550, {
waitFor: '#my-widget'
});
2. Manually inside an async init function (more flexible: multiple selectors, shorter timeouts):
VMInit.add('My Feature', async () => {
const el = await VMInit.waitFor('.js-my-feature', 1200);
if (!el) return;
// ...
}, 550);
waitFor in options accepts a selector string; comma selectors such as '#a, #b' work because querySelector returns the first match.
5.6 VMInit.waitForGlobal(name, callbackOrTimeout?, timeout?) → Promise<any>
Waits until window[name] is defined.
// Promise style
const Sortable = await VMInit.waitForGlobal('Sortable');
if (!Sortable) return;
// Callback style
VMInit.waitForGlobal('autoComplete', (lib) => {
if (!lib) return;
// bind library
});
// Timeout only
await VMInit.waitForGlobal('Mustache', 5000);
Note: You wait for the property name on window ('Sortable', 'autoComplete', 'Mustache'), not for the registry label ('Sortable Ready'). The “Ready” entries in the queue are markers/signals with an empty function; actual availability is checked with waitForGlobal('Sortable') (or similar).
5.7 VMInit.requireUrl(url, varName, context) → string
Validates URLs before fetch(). On undefined / null / "undefined" it throws an error with vmNoRetry = true (no pointless retries).
const url = VMInit.requireUrl(
window.Virtuemart?.jsonLink,
'Virtuemart.jsonLink',
'initMyFeature'
);
const res = await fetch(url);
5.8 Logging
| Method | Visibility | Purpose |
|---|---|---|
VMInit.log(...) / vmlog(...) |
debug only | normal init traces |
VMInit.trace(...) |
debug only | stack traces |
VMInit.warn(...) |
always | timeouts, degraded inits |
VMInit.error(...) |
always | hard failures |
Enable the vmdebug in the vmConfig. So just use the backend of your shop, enter the config of virtuemart and enable the debbugging mode per click:
window.Virtuemart = window.Virtuemart || {};
window.Virtuemart.debug = true;
// or:
window.vmDebug = true;
You then get detailed [VM-Init] logs and grouped runs.
5.9 VMInit.startAlpine()
Starts Alpine.start() exactly once (idempotent). Usually unnecessary for 3rd party — core / admin stack handles this.
6. Sync vs. async execution in the queue
The registry has two execution modes per entry:
6.1 Asynchronous (default: sync: false)
- Handlers are started in priority order but continue in parallel (collected promises +
Promise.allSettled). - Ideal for independent UI bindings (cart, lightbox, browse sort, …).
- A slow handler does not block others indefinitely (unless it is
sync).
VMInit.add('My Async Feature', async () => {
await VMInit.waitFor('.js-my-feature');
// ...
}, 560); // sync: false is implicit
6.2 Synchronous (sync: true)
- The queue awaits this handler before starting the next entry.
- Use for foundations that everything else immediately depends on (e.g. keepAlive priority 5, Alpine UI priority 30 in admin).
VMInit.add('My Foundation', initFoundation, 30, {
sync: true,
retry: 2
});
Rule of thumb for plugins:
- Prefer
sync: false(default). - Use
sync: trueonly if later your own handlers must run after you and you cannot express that with priority +waitFor/waitForGlobal.
7. Priority scheme (community rule)
| Range | Who | Meaning |
|---|---|---|
| 1–500 | VirtueMart core | Core business handlers, steps of 10, gaps for internal inserts |
| 480–520 | Lib signals (core) | “Mustache Ready”, “AutoComplete Ready”, “Sortable Ready” |
| 550–799 | 3rd party / templates | Recommended for extensions |
| 800+ | Plugins via addPlugin |
Default of addPlugin |
Core rule (community guideline):
The core uses a maximum priority of 500.
That always leaves room for extensions after core without colliding with core handlers.
Simplified core order:
| Prio | Handler |
|---|---|
| 5 | keepAlive |
| 10 | vmadmin-core |
| 30 | vmadmin-alpine-ui |
| 40+ | Country/State, AskForm, Tabs, Orders, Prices, Dynupdate, … |
| 340 | Media Handler |
| 360 | ChoicesHandler |
| 370 | GLightbox |
| 390–430 | Cart / Product Handler |
| 480–520 | Mustache / AutoComplete / Sortable Ready signals |
Between the tens there are free slots (e.g. 81–89) if an extension deliberately needs to run before one core piece but after another. That is the exception — the normal case is ≥ 550.
8. Retry and errors
Retry
If initFn throws, it is retried with exponential backoff:
- Delay ≈
250ms * attempt - Count:
options.retry(default 6;addPluginuses 8)
Non-retryable errors
Errors with err.vmNoRetry === true (e.g. from requireUrl) abort immediately — no retry.
reInit (reminder)
AJAX lifecycle and when to use true vs false are covered in §3. Short form:
reInit: true(default) — UI / DOM work that must survive cart and product AJAX.reInit: false— one-shot only (e.g. keep-alive, global bootstrap).
Always keep re-runnable inits idempotent.
9. Dual-loading pattern for third-party libraries
When a library must be available both to legacy code and to ES modules:
PHP
// 1) Start download early (classic script tag / header)
vmJsApi::addJScript('mylib/mylib.min', false, false, true);
// 2) Globalizer module: import + window.XXX + VMInit signal
vmJsApi::addvScriptModule('mylib/mylib-global', 'plugins/.../assets/js');
mylib-global.js (ES Module)
import MyLib from './mylib.min.js';
window.MyLib = MyLib;
if (typeof VMInit !== 'undefined' && typeof VMInit.add === 'function') {
// Core lib signals: 480+; own libs similarly at 550+ or 510+ as needed
VMInit.add('MyLib Ready', () => {}, 550);
}
Dependent module
VMInit.add('Feature using MyLib', async () => {
const MyLib = await VMInit.waitForGlobal('MyLib');
if (!MyLib) return;
// use it
}, 560);
Gold-standard examples in core: sortable-global.js, autoComplete-global.js, mustache-global.js.
10. Complete minimal example (plugin)
PHP (e.g. in a plugin event onAfterDispatch / view display):
// Only if VM and the JS API are present
if (class_exists('vmJsApi')) {
vmJsApi::loadVmInitRegistry(); // no-op if already loaded
vmJsApi::addvScriptModule(
'plg-myfeature.module',
'plugins/system/myfeature/assets/js'
);
}
JS plg-myfeature.module.js:
'use strict';
function initMyFeature() {
const nodes = document.querySelectorAll('.js-my-feature');
if (!nodes.length) return;
nodes.forEach((el) => {
if (el.dataset.myFeatureInit) return;
el.dataset.myFeatureInit = '1';
el.addEventListener('click', () => {
// ...
});
});
if (typeof VMInit !== 'undefined') {
VMInit.log('MyFeature bound on', nodes.length, 'elements');
}
}
if (typeof VMInit !== 'undefined' && typeof VMInit.add === 'function') {
VMInit.add('plg_myfeature', initMyFeature, 550, {
retry: 3,
reInit: true, // survive cart / product AJAX automatically — no custom glue
waitFor: '.js-my-feature'
});
} else {
// Emergency fallback only / very early load without registry
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMyFeature);
} else {
initMyFeature();
}
}
With reInit: true, the same initMyFeature is invoked again after VirtueMart AJAX that triggers the registry. Idempotent binding (dataset.myFeatureInit) keeps listeners from stacking.
11. Do’s and don’ts
Do
- Register your handler with priority ≥ 550.
- Leave
reInit: truefor anything that touches DOM — AJAX re-init is free with VirtueMart. - Keep init functions idempotent (
dataset.*/ WeakMap). - Guard the DOM with
waitFor, libraries withwaitForGlobal. - Use
VMInit.log/vmlogfor debug; useconsole.errorfor real errors. - Load scripts via
vmJsApi::addvScriptModule. - Call
VMInit.reInit()only if you replaced HTML outside normal VM AJAX (core already does this on its own paths).
Don’t
- Do not invent your own “rebind after every AJAX” bus for standard shop/admin updates — that is what
reInit: trueis for. - Do not blindly call
VMInit.run()at the top of the page — duplicate runs are blocked, but timing still suffers. - Do not take over or renumber core priorities 1–500.
- Do not assume
document.readymeans “all modules are finished”. - Do not only use
addJScriptfor libraries that modules must wait for — without a globalizer +waitForGlobalyou get race conditions. - Do not rely on absolute ES Module load order without the registry.
12. Debugging checklist
- Console: does
[VM-Init] 🚀 vm-init-registry module loadedappear? - Do you see
[VM-Init] Running now (queue=N)...and🎉 Init run complete? - With
window.Virtuemart.debug = true, do you seeRegistered → YourName? - Is your script loaded before the final trigger, and did it register before
run()? If too late: after registration callVMInit.reInit(80)once (as the core Alpine module does). - Timeouts on
waitFor/waitForGlobal→ check selector / global name and script path. fetch .../undefined→ missing PHP variables (Virtuemart.jsonLink, token, …); userequireUrl.
13. Configuration globals
| Variable | Default | Meaning |
|---|---|---|
Virtuemart.recheckInterval |
50 |
Poll interval (ms) for waitFor* |
Virtuemart.recheckTimeout |
8000 |
Default timeout (ms) |
Virtuemart.debug / vmDebug |
false |
Verbose logging |
window.VMInit |
Registry | Public API |
window.vmlog |
Alias | VMInit.log |
14. Short FAQ
Do I need to put the file in the head myself?
No. loadVmInitRegistry() / the normal VM page build loads it. Only register your own module with addvScriptModule.
What is the point of reInit?
It makes AJAX easy. With reInit: true (default), VirtueMart re-runs your registered init after its AJAX updates — you do not write your own re-init system. With reInit: false, your code runs only once. See §3.
Do I need to listen for cart / dynupdate events myself?
Not for normal VM AJAX flows. Keep reInit: true and write idempotent init code. Call VMInit.reInit() only if you replaced the DOM outside those flows.
Can I still use jQuery.ready in parallel?
Technically yes, but new features should go through VMInit — otherwise you get double inits and worse AJAX compatibility.
Sync or async?
Default async. Sync only for real foundations.
When use waitFor in options vs. in code?
One fixed DOM anchor → option waitFor. Multiple steps, libraries, conditional logic → manually in the async function.
Priority 550 or 800?
550–700 for “soon after core, but out of the way”. addPlugin (800) when order does not matter and late is fine.
15. File paths
| Role | Path |
|---|---|
| Source (readable) | components/com_virtuemart/assets/js/vm-init-registry.module.js |
| Minified | components/com_virtuemart/assets/js/vm-init-registry.module.min.js |
| PHP loader | administrator/components/com_virtuemart/helpers/vmjsapi.php → loadVmInitRegistry(), final trigger in the JS output |
This page documents the central JavaScript initialization system in VirtueMart 5. Related topics: vmJsApi (loading scripts/CSS), dual-loading of libraries, Alpine-first admin UI (vmadmin).