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/05-special-cases.md

148 lines
7.9 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 05 — Per-component special cases
Where a component does something the `WithProps` mixin can't express from a
prop table alone.
## `leaflet-map` — shadow DOM, CSS, resize
`src/elements/leaflet-map.ts`.
- **Shadow root** built in `connectedCallback` (not via
`createLeafletObject`): a `<div>` at `100% × 100%` for Leaflet to render
into, a `<style>` that gives `:host` a `block` display and a default
`400px` height, and a `<link>` to Leaflet's stylesheet.
- **`css-url` / `css-integrity` / `css-crossorigin`** are _not_ Leaflet
options — they describe that `<link>`. Their `set` just calls `applyCss()`,
which re-links the stylesheet and re-derives `Icon.Default.imagePath` from
the same URL directory. `applyCss()` reads the _attributes_ directly
(not the properties) because absent and empty mean different things here.
Defaults point at `unpkg.com/leaflet@1.9.4` with a matching SRI hash.
- **`lat` / `lng` / `zoom`** are `positional` but not constructor args in the
usual sense — the map is positioned by `setView()` right after
construction, and they're written back on `moveend` / `zoomend`.
- **`ResizeObserver`** on the host calls `map.invalidateSize()`. jsdom lacks
`ResizeObserver`, so the test setup stubs it (see
[07](./07-tooling-and-build.md)).
- As tree root it also listens for `leaflet-add-layer` / `leaflet-remove-layer`
(used by group internals) alongside `leaflet-register`.
- **`fit-to-markers` / `fit-padding` / `fit-max-zoom`** are not Leaflet options.
With `fit-to-markers` present the map ignores `lat`/`lng`/`zoom` and instead
`fitBounds()`es a box around every layer it can locate — anything with a
`getLatLng()` (markers, circles) or `getBounds()` (rectangles, image/video
overlays); tile layers have neither and are skipped, as are open
popups/tooltips (a map layer with a `getLatLng()`). **The only reframe
trigger is a component registering** — initial page load and any child added
later. Panning, zooming, opening a popup and removing a marker all leave the
view exactly where it is. `#onRegister` calls `#scheduleFit()`, which
coalesces the burst of registrations during page load onto one microtask, so
it's a single `fitBounds` call, not one per marker. `fit-padding` (default
`20`) is the pixel gutter left around the bounds; `fit-max-zoom` (default
none) caps the zoom, which matters when a single marker would otherwise snap
to max zoom. `setView()` still runs once at construction so the map has a
valid view before the first frame.
- Panning / zooming always writes the live centre and zoom back to the
`lat` / `lng` / `zoom` attributes (`moveend` / `zoomend`, the standard `event:`
write-back in the prop table) — including the view `fitBounds()` itself lands
on. That write-back is one-way here: it never re-triggers a fit.
## `leaflet-polygon` / `leaflet-polyline` — vertices from children
`src/elements/leaflet-polygon.ts`, `leaflet-polyline.ts`,
`src/core/vertex-tracker.ts`.
Vertices come from `<leaflet-line>` children, **event-driven, never a
lookup**:
- `<leaflet-line>` is a plain `HTMLElement` (no mixin). On connect and on
every `lat`/`lng` change it fires `leaflet-line-sync` on itself, carrying
its own `[lat, lng]`. On disconnect it fires `leaflet-line-remove`.
- The polygon/polyline feeds those events into a shared `VertexTracker`,
which keeps an ordered coordinate list. A newly-registered vertex is
inserted at its **actual document position** via `compareDocumentPosition`
— registration order is not assumed to match DOM order.
- After every sync/remove the component calls
`obj.setLatLngs(tracker.coords())`.
**The `leaflet-line-remove` wrinkle:** `disconnectedCallback` fires _after_
the node is already detached from its parent, so a bubbling dispatch from the
node has nowhere to go. `<leaflet-line>` caches `parentNode` in
`connectedCallback` and dispatches the remove event from that cached
reference instead (`emitLineRemove(from, el)` in `register.ts`).
## `leaflet-popup` / `leaflet-tooltip` — content is markup
`src/elements/leaflet-popup.ts`, `leaflet-tooltip.ts`.
- Content is `this.innerHTML`, passed as the `content` option, not an
attribute.
- Both run a `MutationObserver` (`childList` + `characterData` + `subtree`)
and call `setContent(this.innerHTML)` on any change.
- A popup/tooltip only carries a position of its own when it's _not_ bound to
a parent layer — `createLeafletObject` sets `setLatLng` only if both `lat`
and `lng` attributes are present.
- `attach: 'self'` — they register with a parent but adopt no children.
## `leaflet-layer-group` / `leaflet-feature-group` — passthrough
`const PROPS: Record<never, never> = {}` — an empty prop table
(`new LayerGroup([])` / `new FeatureGroup([])`). `Record<never, never>`, not
`Record<string, never>`: the latter would put a `never` string-index
signature on the generated instance type that the subclass's own members
can't satisfy. No options of their own, but they still get the full
lifecycle, child registration (`attach: 'children'`), and `leaflet:` event
forwarding. A descendant layer's `leaflet-register` is claimed by the mixin's
own `#onChildRegister` (`Layer` + parent has `addLayer``obj.addLayer`),
stopping there instead of bubbling on to the map. The group itself then
registers with _its_ parent, so a group can nest in a group.
## `leaflet-control-layers` — dual role
`src/elements/leaflet-control-layers.ts`. `attach: 'self'` (it's a control —
registers with the map, no children through the standard path), but it _also_
attaches its own `leaflet-register` listener to intercept its child layer
entries (`<… name="OSM" type="base" active>`): `addBaseLayer` when
`type="base"`, else `addOverlay`, keyed by the child's `name` attribute. A
child marked `active` also gets a `leaflet-add-layer` event dispatched
upward, which `<leaflet-map>` handles with `map.addLayer(layer)` so that
layer is shown initially. (The map listens for `leaflet-add-layer` /
`leaflet-remove-layer` for exactly this; `leaflet-control-layers` is their
only dispatcher.)
## `leaflet-geojson` — style nesting
`src/elements/leaflet-geojson.ts`. Extends `pathProps`, but GeoJSON takes
style options nested under a `style` key so they apply to each generated
feature: `new GeoJSON(data, { style: options })`. `data` is `positional` +
`json`; its `set` does `clearLayers()` then `addData(value)`.
## `leaflet-tile-layer-wms` — extensible CRS
`src/elements/leaflet-tile-layer-wms.ts`.
- The `crs` **attribute** only covers the four CRSes Leaflet ships by name
(`EPSG3857`, `EPSG4326`, `EPSG3395`, `Simple`) via a `NAMED_CRS` lookup.
- A CRS Leaflet doesn't ship comes from a **nested child element** that fires
`leaflet-crs-changed` with a `CRS` instance — no base class, no registry,
just the event shape (see [03](./03-component-tree.md)). It takes priority
over the attribute. `crs: null` reverts to the attribute's named lookup.
- `crs` is constructor-only, so picking up a child CRS means
`recreateLeafletObject()` — a full rebuild of the layer.
- WMS request params (`layers`, `styles`, `format`, `transparent`,
`version`) have no individual setters; each prop's `set` merges through
`obj.setParams({ [key]: value })`.
## `leaflet-icon` / `leaflet-div-icon` — recreate on change
Icons are created with `WithProps(PROPS, { recreate: true })`: Leaflet gives
no way to mutate an `Icon` in place, so any attribute change throws the icon
away and rebuilds it. They're `attach: 'none'` — not tree members — and
announce themselves to a parent `leaflet-marker` via `icon-changed`.
## `leaflet-marker` — live `<img>` attributes
`title` and `alt` end up on the `<img>` Leaflet renders, so their `set`
reaches `obj.getElement()` and assigns the DOM property directly.
`draggable`'s `set` toggles `obj.dragging.enable()/disable()`. Note that
`getElement()` only exists after the marker is added to a real map — tests
touching it append through a `<leaflet-map>`.