You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
leaflet-components/docs/06-load-order.md

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 its connectedCallback hasn't run.
  • That announcement is gone for good: connectedCallback does 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.