3.8 KiB
06 — Load order
The rule
The order of the ./components/* side-effect imports in src/index.ts is
load-bearing. A component that listens for a bubbling announcement from
another tag must be imported — and therefore customElements.define()d —
before that other tag.
(Only the ./components/* modules call customElements.define. The element
classes themselves live in src/elements/* and the src/elements/index.ts
barrel; those carry no side effect, so their order is always free.)
Why
customElements.define(name, Class) upgrades every matching element already
in the document immediately and synchronously, running
connectedCallback right then for elements that are already connected.
On a real static-HTML page the markup is fully parsed before the deferred
module script runs, so at define() time the elements already exist. The
first define() for a given tag wins the race, and that ordering is fixed by
the sequence of import './components/leaflet-*.ts' statements in
src/index.ts:
- If a child tag is defined before its listening parent tag, every
instance of that child in the page upgrades and fires its one-shot
connect-time announcement (
leaflet-register,icon-changed,leaflet-line-sync,leaflet-crs-changed) into a parent element that has no listener attached yet — because the parent class isn't defined, so itsconnectedCallbackhasn't run. - That announcement is gone for good:
connectedCallbackdoes not fire again for an element that stays connected.
So parents must be defined first.
The ordering in src/index.ts
Root to leaf (this is the comment at the top of the file, kept in sync):
LeafletMapElement ← nothing can register before the root exists
LeafletControlLayersElement, LeafletLayerGroupElement,
LeafletFeatureGroupElement ← listen for leaflet-register from arbitrary layers
every concrete layer type (Marker, Circle,
Polygon, TileLayer, …)
LeafletLineElement ← child of Polygon/Polyline
LeafletPopupElement, LeafletTooltipElement ← child of any layer
LeafletIconElement, LeafletDivIconElement ← child of Marker
Standalone controls (LeafletControlZoomElement,
LeafletControlAttributionElement, LeafletControlScaleElement) have no such
relationship and can go anywhere.
The core re-exports (register.ts, props.ts, with-props.ts,
shared-props.ts, event-types.ts) and the export * from './elements/index.ts' come first and carry no customElements.define, so
their order is free.
The HTMLElementTagNameMap augmentation
Lives in src/core/globals.ts (not src/index.ts), together with the
HTMLElementEventMap augmentation for the internal bubbling events. That
module is imported only by the npm entry src/index.npm.ts and by
test/setup.ts, and is excluded from the JSR tarball — JSR's "no slow types"
check forbids declare global. Its import type { … } from '../elements/index.ts' is type-only, erased at build, backs no define() —
order doesn't matter. See 07.
The test that guards this
test/load-order.test.ts is structurally unlike every other test file on
purpose: it never statically imports a component module, so nothing is
customElements.defined until it dynamically import()s src/index.ts
partway through — after building a DOM tree out of plain, undefined
elements. That's the only test reproducing a real page's actual order (markup
first, module script second).
Every other test file imports components up front, so components are already
defined before any element is created — they structurally cannot catch an
ordering regression. If you add a component with a "parent listens for a
child's announcement" relationship, extend load-order.test.ts.