Globals is a static registry mapping every component's moniker to its class,
exposed on the window at window.LeaseLeads_Theme.components. Internally it lets the
loader answer "is this component already registered?"; externally it is the theme's
supported API for reaching a live instance from a script that isn't part of the theme
build. You interact with it through one call — look a component up by moniker:
const Intro = window.LeaseLeads_Theme.components.get('Intro') // the registered class
Registration itself is automatic: the loader writes the moniker onto each class and adds it here as its chunk resolves. You never call the registry's setter — you only ever read from it.
Why it works this way. Client sites regularly need per-instance tweaks injected
from outside the theme — in practice a <script> dropped onto one page via the HFCM
(Header Footer Code Manager) plugin. Every component registers here precisely so that
script can do components.get('Intro').get(el).open(). The registry is a public
API, not internal bookkeeping. (ADR 0002.)
Name vs moniker
Two identifiers travel with every component, and the difference matters the moment you register or reach for one.
- name — the resolution key in the loader descriptor (
convert({ name: 'Intro' })).AssetLoaderresolves the class viaexports[name], so theexport { Final as Intro }alias must equal it — a mismatch throws at load. - moniker — the stable runtime identity
Globals.setwrites onto the class (Class.moniker). It keys this registry and prefixes every lifecycle and emit event.
Vite munges class names when it minifies the build, so SomeClass.name is unreliable
at runtime. The moniker is human-authored and never munged — which is why it, not the
class name, is the external contract.
A moniker is a breaking change if you rename it. External HFCM scripts and every
<moniker>.initialized event listener depend on the exact string. Treat it like a
published API name.
Hooking in from outside the theme
This is the reason the registry exists. An external script — typically injected via HFCM, and it must go in the footer — wants to reach a component instance and drive it. There are two routes.
The catch: instances don't exist at script-eval time
Components boot asynchronously. Deferred doesn't even start loading modules until
the first user interaction (see Two-phase loading),
and each component then loads its chunk, boots, and inits over several turns of the
event loop. So at the moment your injected <script> runs, the instance you want almost
certainly does not exist yet. Reading the registry synchronously will hand you nothing.
The reliable route is therefore to listen for the lifecycle event and read the
instance off event.detail:
<!-- HFCM: footer -->
<script>
// moniker-scoped: fires only for Intro modules
document.addEventListener('intro.initialized', (event) => {
const intro = event.detail.instance
intro.doSomething()
})
// or generic: fires for every module as it initializes
document.addEventListener('module.initialized', (event) => {
const instance = event.detail.instance
})
</script>
Because the events bubble to document, one listener catches every matching instance —
including ones added after your script ran.
The direct pull (only once you know it's booted)
If you already know a component has initialized — say, inside an initialized handler,
or behind your own user-interaction trigger — you can pull the instance straight from
the registry by moniker and element:
const el = document.querySelector('.intro')
const intro = LeaseLeads_Theme.components.get('Intro').get(el)
intro.open() // or whatever the component exposes
.get('Intro') returns the class; .get(el) returns that element's live instance from
its per-element registry. If instantiation hasn't
happened yet, .get(el) is undefined — which is exactly why the event route is the
one to reach for by default.
Related
- Instantiable — the per-element registry
get(el)reads from. - The lifecycle — the
created/booted/initializedevents you listen for. - Two-phase loading — why instances aren't there at script-eval time.