Every component is now two modules sharing a basename:
- src/elements/leaflet-foo.ts -- `export default class LeafletFooElement
extends WithProps(...)`, the class only, no customElements.define, no
side effects. src/elements/index.ts is an order-free barrel.
- src/components/leaflet-foo.ts -- three lines: import the class, define
the tag, re-export. Importing this (or src/index.ts) registers the tag.
Plugin authors can now import a class without triggering the built-in
define, to subclass it or register it under a different tag name. Package
subpath exports `leaflet-components/elements`,
`leaflet-components/elements/leaflet-foo.js`, and
`leaflet-components/components/leaflet-foo.js` map onto dist/; jsr.json
gains an `./elements` entry.
The load-bearing ordering moves from the export statements in src/index.ts
to its `./components/*` side-effect imports (the elements barrel carries no
define, so its order is free). Classes are renamed LeafletFoo ->
LeafletFooElement, including in the HTMLElementTagNameMap augmentation.
Docs (CLAUDE.md, docs/, README.md) updated for the new layout.
Eight docs under docs/ covering the current design: the WithProps mixin
and lifecycle, the PropDef/attribute model, the leaflet-register bubbling
protocol, generic event forwarding and its typing, per-component special
cases, the load-bearing export order in src/index.ts, and the toolchain
(TS 7, oxlint, oxfmt, Vitest, the no-bundle build, dual npm/JSR publish).
Also wire docs/**/*.md into the format script and point at docs/ from
CLAUDE.md and README.md.
Replace prettier with oxfmt (oxc's Prettier-compatible formatter), config
seeded via `oxfmt --migrate=prettier` and kept as `oxfmt.config.ts`
(defineConfig default export) so settings match the old prettier.config.js.
The `format` script now runs `oxfmt`; docs and the local tool allowlist
updated to match.
Two parent-looks-for-child patterns found and removed, per the
child-emits-events-parent-never-reaches-in principle:
- leaflet-control-layers: #childLayers() queried children directly via
querySelectorAll + reading their leafletObject/attributes, in two call
sites. Verified empirically that a parent's connectedCallback always
completes before a freshly-connected child's does (even for an
already-built subtree attached in one shot), which means both call
sites always ran against unpopulated children -- dead code. Deleted;
#onChildRegister (already event-driven) was doing all the real work.
- leaflet-polygon/leaflet-polyline: #coords() queried <leaflet-line>
children via querySelectorAll + read their .latlng property directly,
using a MutationObserver + a payload-less event only as a "something
changed, rescan everyone" signal. Rewritten to be purely event-driven:
<leaflet-line> now fires leaflet-line-sync (connect + every lat/lng
change, carrying its own position) and leaflet-line-remove (disconnect)
on itself; a new shared VertexTracker (src/core/vertex-tracker.ts)
turns that event stream into an ordered coordinate list, using
compareDocumentPosition only to place a newly-registered vertex at its
real document position rather than assuming registration order matches
DOM order. No querySelectorAll, no MutationObserver, no reading a
child's property.
Testing the polygon rewrite in an actual browser (not jsdom) surfaced two
real bugs, both fixed:
1. disconnectedCallback fires *after* a node is already detached from its
parent, so leaflet-line's removal event had nowhere to bubble to.
Fixed by caching parentNode while still connected and dispatching from
that cached reference instead of from the (by-then-detached) node.
2. A load-order hazard affecting every "parent listens for a specific
child tag's announcement" relationship in this codebase:
customElements.define() upgrades every matching element already
parsed into the page immediately and synchronously, so whichever tag
gets defined first wins a race -- a child tag defined before its
listening parent fires its one-shot connect-time announcement into a
parent that doesn't exist yet, and it's lost for good. This broke
polygon/polyline (introduced by this rewrite, since leaflet-line was
exported before leaflet-polygon) and, independently, was already
silently broken for leaflet-control-layers (a base/overlay layer
leaked directly onto the map, bypassing the control entirely) and
latently for leaflet-layer-group/leaflet-feature-group. src/index.ts's
export order now encodes and documents the real dependency hierarchy
(map -> containers that listen for child announcements -> concrete
layer types -> their own children).
Added test/load-order.test.ts, which is structurally different from
every other test file: it never statically imports a component
module, building the DOM with plain undefined elements first and only
dynamically import()ing src/index.ts afterward -- reproducing a real
page's actual load order, which every other test's "define everything
first" pattern cannot catch. Confirmed it fails against the old
ordering and passes against the fix.
README.md:
- Import options: drop CJS/UMD/minified-bundle mentions, none exist since
the build simplified to ESM-only.
- New Events + TypeScript sections documenting the leaflet:<type> event
forwarding mechanism and the HTMLElementTagNameMap / per-component
addEventListener typing added this session.
- leaflet-tile-layer: 6 -> 23 attributes (full tileLayerProps), fixed
z-index default (0 -> 1).
- leaflet-tile-layer-wms: now documents that it inherits all tile-layer
attributes (previously listed none, matching the bug fixed earlier) +
the new crs attribute.
- leaflet-video-overlay: added the 4 attributes added this session.
- leaflet-polyline: fixed a wrong "excludes fill*" claim and added
smooth-factor/no-clip.
- leaflet-geojson: was documenting a stale hand-picked subset of path
attributes (with stroke mis-described as a color); now says all
path-style attributes apply, matching the dedup against pathProps.
- Path style table: added stroke/interactive/bubbling-mouse-events/
class-name/pane, fixed fill's default.
- Added leaflet-icon/leaflet-div-icon sections (existed but were
undocumented).
- Nesting rules and Development section brought in line with reality.
CLAUDE.md: the WithProps mixin description referenced an API from before
this session that no longer exists (WithProps(Base, PROPS), definePropAccessors,
initOptions(), updateLeafletObject()) and claimed LeafletMap extends
HTMLElement directly when it extends WithProps(...) like everything else.
Rewrote to match the real internals, fixed the child-component-pattern
steps, and documented how to type a new component's events.
Also added '*.md' to the format script's glob -- root markdown files
weren't covered, so this formatting could have silently drifted again.
No test coverage existed before this. Adds vitest + jsdom (2 new
devDependencies) and a suite that runs entirely without a browser: real
Leaflet objects work fine under jsdom for everything this library needs to
verify (option/attribute wiring, event forwarding), given a ResizeObserver
stub in test/setup.ts (jsdom's only real gap here).
- test/core/with-props.test.ts: the WithProps mixin itself, against a fake
Leaflet-like class -- attribute<->setter dispatch, get/attribute/default
fallback order, event-driven attribute sync-back, positional/recreate/
attach modes, and the generic fire()-patch event forwarding. Child
registration (popup/tooltip/layer binding) uses real Popup/Tooltip/Marker
instances since #onChildRegister discriminates by instanceof.
- test/core/props.test.ts: the codec functions in isolation.
- test/components/*.test.ts: grouped smoke tests across all components.
- test/integration.test.ts: full tree wiring (map + tile-layer +
feature-group + marker + popup).
Caught and fixed one real bug along the way: urlProp's "live url getter"
from the last refactor was dead code -- neither ImageOverlay nor
VideoOverlay actually expose getUrl(). Removed it and the now-pointless
getUrl?() from the Sourced interface in shared-props.ts.
tsconfig.test.json keeps test/ out of the tsc build (dist/ stays
test-free) while still typechecking it; oxlint.config.ts now covers test/
too, with max-classes-per-file relaxed there since testing a class
factory means many small one-off element classes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finishes the WithProps(PROPS, options) mixin (src/core/with-props.ts): every
component gets reactive attributes, live-value property getters that read
through to the Leaflet object where possible, and every Leaflet event
re-emitted on the element as `leaflet:<type>` via a generic fire() patch (no
per-component or per-event registration needed).
Prop coverage:
- Centralized pathProps in shared-props.ts (stroke, lineCap, lineJoin,
dashArray, dashOffset, fillRule, interactive, className,
bubblingMouseEvents, pane), deduped leaflet-geojson against it.
- Added live getters backed by Leaflet's own accessors: radius (circle,
circle-marker), bounds (rectangle, image/video/svg-overlay), url
(image/video-overlay).
- Filled gaps: smoothFactor/noClip on polyline; a shared tileLayerProps
fragment now used by both tile-layer and tile-layer-wms (WMS previously
exposed none of its base tile options); crs on WMS; zIndex/className/
keepAspectRatio/errorOverlayUrl on video-overlay; fixed tile-layer's
zIndex default and div-icon's className default to match Leaflet.
Tooling:
- Removed dead src/core/events.ts (broken, unused, superseded by the
generic event forwarding above) and src/core/attributes.ts (emptied by
an earlier rename, nothing imported it).
- Swapped ESLint + @typescript-eslint for oxlint: no released
@typescript-eslint version supports the pinned typescript@7, even for
parsing alone. oxlint has its own parser and lints clean.
- Dropped the Rollup CJS/UMD bundle step; dist/ is ESM-only from tsc now.
Updated package.json's main/module/exports/unpkg accordingly.
- Updated CLAUDE.md to match: build/lint commands, ESM-only output, the
event-forwarding mechanism, and layer-group/feature-group now going
through WithProps({}) instead of raw HTMLElement.
Change all import specifiers from ".js" to ".ts" in TypeScript source
files. Add allowImportingTsExtensions and rewriteRelativeImportExtensions
to tsconfig so tsc strips them to ".js" in the dist output — no build
pipeline changes needed. Update CLAUDE.md to reflect the new extension policy.
Remove stale JSR reference, add import-options section (formats, deep
imports, leaflet as external dep), fix build command (esbuild -> Rollup).
Rewrite CLAUDE.md architecture to reflect withProps mixin, register.ts
helpers, feature-group, and the full rollup output.
Wraps Leaflet.js as native Web Components. Each element maps 1:1 to a
Leaflet object with reactive attribute binding and proper lifecycle cleanup.
Key design decisions:
- LeafletElement base class handles attribute→option mapping, the
leaflet-register bubble event for parent/child wiring, and
disconnectedCallback cleanup tracking parent bindings.
- LeafletControl base class for map controls (zoom, attribution, scale).
- leaflet-map uses a PROPS table to drive observedAttributes, getters/setters,
attributeChangedCallback, and map event listeners from a single source of
truth. Property types are derived via a mapped type over the table; the
mixin-base pattern (TypedBase) makes them visible to TypeScript without an
interface merge.
- Boolean map options that default to true use disable-* attributes.
- Leaflet and its CSS are bundled into dist/index.js via esbuild.
- JSR config included for @buddy/leaflet-components.