|
|
# 01 — Architecture
|
|
|
|
|
|
## Element per Leaflet object
|
|
|
|
|
|
Every `leaflet-*` custom element maps 1:1 to a single Leaflet object — a
|
|
|
`Map`, a `Marker`, a `TileLayer`, a `Popup`, a `Control`, an `Icon`. The
|
|
|
element owns that object for its connected lifetime, holds the only reference
|
|
|
to it, and exposes it as `element.leafletObject`.
|
|
|
|
|
|
Nothing about the wrapping is per-object hand-written plumbing. An element
|
|
|
class is small (often 30–60 lines): a table of property descriptors, a
|
|
|
`createLeafletObject()` that calls one Leaflet constructor, and a couple of
|
|
|
`declare` lines for types. Everything else — attributes, option building,
|
|
|
two-way sync, event forwarding, tree membership — comes from the `WithProps`
|
|
|
mixin.
|
|
|
|
|
|
## `elements/` and `components/`
|
|
|
|
|
|
Each component is two files sharing a basename:
|
|
|
|
|
|
- **`src/elements/leaflet-foo.ts`** exports `default class LeafletFooElement
|
|
|
extends WithProps(PROPS)` — the class alone, no side effects. Import it (or
|
|
|
the `src/elements/index.ts` barrel, which re-exports every class by name)
|
|
|
to get the constructor **without** registering a tag; useful for
|
|
|
subclassing or defining it under a different name.
|
|
|
- **`src/components/leaflet-foo.ts`** is three lines: import the class,
|
|
|
`customElements.define('leaflet-foo', LeafletFooElement)`, re-export it.
|
|
|
Importing this module (or `src/index.ts`, which imports all of them in a
|
|
|
load-bearing order — see [06](./06-load-order.md)) is what registers the
|
|
|
tag.
|
|
|
|
|
|
`package.json` maps `leaflet-components/elements`,
|
|
|
`leaflet-components/elements/leaflet-foo.js` and
|
|
|
`leaflet-components/components/leaflet-foo.js` onto the matching `dist/`
|
|
|
files.
|
|
|
|
|
|
## The `WithProps` mixin
|
|
|
|
|
|
`src/core/with-props.ts`. `WithProps(PROPS, options?)` is a mixin **factory**:
|
|
|
it always extends `HTMLElement` internally (there is no base-class parameter)
|
|
|
and returns a constructor. An element class does:
|
|
|
|
|
|
```ts
|
|
|
export default class LeafletMarkerElement extends WithProps(PROPS) {
|
|
|
declare readonly leafletObject?: Marker;
|
|
|
createLeafletObject(options: MarkerOptions): Marker {
|
|
|
return new Marker([this.lat, this.lng], options);
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
`PROPS` is a `const` record mapping property names to `PropDef` descriptors
|
|
|
(see [02](./02-props-and-attributes.md)). From that table alone the mixin
|
|
|
derives everything below.
|
|
|
|
|
|
### What the generated class does
|
|
|
|
|
|
- **`static observedAttributes`** — the kebab-cased attribute name of every
|
|
|
prop in the table (`fillOpacity` → `fill-opacity`; a `PropDef` can override
|
|
|
the derived name, e.g. `disabled()` produces `disable-*`).
|
|
|
|
|
|
- **Property accessors** — one `Object.defineProperty` per prop on the
|
|
|
prototype. The getter reads the _live_ Leaflet value when the `PropDef`
|
|
|
defines a `get` (falling back to the attribute, then the declared default);
|
|
|
the setter encodes the value onto the attribute and lets
|
|
|
`attributeChangedCallback` propagate it.
|
|
|
|
|
|
- **On connect** (`connectedCallback`): `#buildOptions()` assembles a Leaflet
|
|
|
options object from the currently-present attributes plus nothing else
|
|
|
(absent attribute ⇒ absent key ⇒ Leaflet's own default applies), skipping
|
|
|
any prop marked `positional()`. Then `createLeafletObject(options)` runs.
|
|
|
Then, unless `attach: 'none'`, the element registers with its parent
|
|
|
(see [03](./03-component-tree.md)). Then `leafletObjectCreated()` — a no-op
|
|
|
hook components can override.
|
|
|
|
|
|
- **On attribute change** (`attributeChangedCallback`): dispatch to the
|
|
|
prop's own `set(obj, value, el)` if it has one; otherwise call the
|
|
|
matching Leaflet setter by naming convention (`opacity` → `obj.setOpacity`)
|
|
|
if the object has one; otherwise **silently do nothing** — many Leaflet
|
|
|
options are constructor-only and this is expected. If the mixin was created
|
|
|
with `options.recreate` (icons), an attribute change instead throws the
|
|
|
object away and rebuilds it.
|
|
|
|
|
|
- **Two-way sync**: `#watchObject()` subscribes one Leaflet listener per
|
|
|
distinct `event:` named in the table; when it fires, every prop naming that
|
|
|
event has its live value read via `get` and written back to the attribute.
|
|
|
Dragging a marker fires `move`, which writes both `lat` and `lng`. A
|
|
|
`#syncing` flag set during that write makes the resulting
|
|
|
`attributeChangedCallback` a no-op — this is the only place an update cycle
|
|
|
could form, and it's the only guard needed.
|
|
|
|
|
|
- **Event forwarding**: `#forwardEvents()` wraps the object's own `fire()`
|
|
|
method so _every_ Leaflet event becomes a non-bubbling `leaflet:<type>`
|
|
|
`CustomEvent` on the element. Generic and automatic — no per-component or
|
|
|
per-event registration. See [04](./04-events.md).
|
|
|
|
|
|
- **On disconnect**: unbind children, remove the object from its parent, call
|
|
|
`obj.off()` for the sync listeners and `obj.remove()`.
|
|
|
|
|
|
### Why a factory and not a base class
|
|
|
|
|
|
The prop table drives code generation (`observedAttributes`, the accessor
|
|
|
descriptors) that has to exist on the class _before_ any instance. A factory
|
|
|
that closes over the resolved table and defines accessors on
|
|
|
`Class.prototype` is the natural shape for that. `WithProps({})` — an empty
|
|
|
table — is still a useful base: `leaflet-layer-group` and
|
|
|
`leaflet-feature-group` use it to get lifecycle, child registration and event
|
|
|
forwarding with no options of their own.
|
|
|
|
|
|
### Duck typing, deliberately
|
|
|
|
|
|
`Layer`, `Control` and `Icon` share no common Leaflet interface, and which
|
|
|
setters exist varies by class. The mixin's `method(obj, name)` helper looks a
|
|
|
method up by string and binds it, or returns `undefined`. This is why the
|
|
|
attribute-change path can "try the setter, else no-op" without knowing
|
|
|
anything about the concrete class.
|
|
|
|
|
|
## `leaflet-map` is special
|
|
|
|
|
|
`src/elements/leaflet-map.ts`. `LeafletMapElement` still extends
|
|
|
`WithProps(PROPS)` with `attach: 'none'`, but additionally:
|
|
|
|
|
|
- builds its own **Shadow DOM** in `connectedCallback` (a `<div>` container
|
|
|
for Leaflet, a `<style>` for `:host`, and a `<link>` to Leaflet's CSS),
|
|
|
- is the **root of the component tree**: it listens for the bubbling
|
|
|
`leaflet-register` event and terminates it with `layer.addTo(this.map)`
|
|
|
(plus `leaflet-add-layer` / `leaflet-remove-layer` → `map.addLayer` /
|
|
|
`removeLayer`, which only `leaflet-control-layers` dispatches),
|
|
|
- runs a `ResizeObserver` on the host to call `map.invalidateSize()`,
|
|
|
- treats its `css-*` attributes as describing the shadow-root stylesheet, not
|
|
|
the map — changing one just re-links the `<link>` and re-derives
|
|
|
`Icon.Default.imagePath` from the same URL.
|
|
|
|
|
|
See [05](./05-special-cases.md) for the CSS/escape-hatch details.
|