Documents the toolkit third-party components are built from, now that
it's exported from the package root: the WithProps + PROPS table
pattern, the prop builder reference, reusing the shared fragments
(pathProps/latLngProps/tileLayerProps/urlProp), how nesting works
(attach modes, and why Leaflet plugin classes register correctly without
any special-casing on either side), typing events with
LeafletAddEventListener and the event-types.ts fragments, the
emitIconChanged protocol for custom marker icons, and the
customElements.define ordering pitfall for a plugin with multiple
interdependent custom elements of its own.
@ -567,6 +567,123 @@ The following attributes apply to `<leaflet-circle>`, `<leaflet-circle-marker>`,
- **`<leaflet-icon>` / `<leaflet-div-icon>` as child of `<leaflet-marker>`** → swapped in via `setIcon()`.
- **Layer as child of `<leaflet-control-layers>`** → intercepted by the control and registered as a base or overlay entry. The control stops propagation so the layer doesn't reach the map directly. Use `active` to start the layer visible.
## Building your own components
This library's own components are built from a small toolkit — a mixin, some prop-codec builders, and a few reusable fragments — and that whole toolkit is exported from the package root. A component wrapping a Leaflet plugin (a custom layer, control, or icon) is built exactly the same way `<leaflet-marker>` or `<leaflet-circle>` are, whether or not it ever ends up in this package.
### The pattern
1. Describe your attributes as a `PROPS` table, then extend `WithProps(PROPS, options?)`.
2. Implement `createLeafletObject(options)`, returning whatever Leaflet object your plugin provides.
3. `declare readonly leafletObject?: TheType;` — `WithProps`'s own inference of the object type from the `PROPS` table alone isn't reliable enough to skip this (every component in this package does it).
That's it — no other integration point is needed. `WithProps` handles `observedAttributes`, attribute↔property sync, building the options object from attributes on connect, dispatching attribute changes to the matching Leaflet setter (or a `set` you provide per prop, for anything a setter can't handle), and re-emitting every event your object fires as `leaflet:<type>`.
| `bool(default?, opts?)` | a boolean | present/absent by default; use `bool(true, ...)` for an option Leaflet defaults to `true`, so `attr="false"` can opt out |
| `choice<'a' \| 'b'>(default, opts?)` | a string literal union | not runtime-validated, just typed |
| `json<T>(default, opts?)` | JSON | decodes/encodes via `JSON.parse`/`JSON.stringify` — for points, bounds, arbitrary objects |
| `disabled(opts?)` | a boolean | the inverse of `bool(true)`: attribute is auto-named `disable-<kebab-name>`, presence means `false` |
| `positional(def)` | — | wraps any of the above; marks a prop your Leaflet constructor takes as an argument rather than an option, so it's excluded from the options object `createLeafletObject` receives |
Each builder's `opts` can include `set(obj, value, el)` (called on attribute change instead of the default `setXyz()` lookup — needed when there's no matching setter, or the update needs more than one value, like a lat/lng pair), `get(obj)` (backs the live property getter, and — paired with `event` — keeps the attribute in sync whenever your object fires that event), and `attribute` (override the default kebab-cased name).
### Reusing the built-in fragments
Common attribute groups are already factored out and exported: `pathProps` (Leaflet `Path` styling — color, weight, dashes, fill, etc.), `latLngProps` (a synced lat/lng pair, for anything positioned on the map), `tileLayerProps` (the base `GridLayer`/`TileLayer` options), and `urlProp`. A custom vector layer plugin, for instance, can just spread `pathProps` in instead of redeclaring styling attributes:
```ts
import { WithProps, pathProps, positional, json } from 'leaflet-components';
class MyShapeLayer extends WithProps({
...pathProps,
data: positional(json(null)),
}) {
/* ... */
}
```
### Nesting into the tree
By default (`attach: 'children'`, the default when you omit `options`), your component registers itself with its nearest ancestor and accepts registering descendants as child layers/popups/tooltips. Use `attach: 'self'` for something that has a parent but manages no children of its own (a popup, a tooltip, a control); use `attach: 'none'` for something that's neither (an icon).
The registration check on the _receiving_ side keys off `instanceof Layer` / `instanceof Popup` / `instanceof Tooltip` — Leaflet's own base classes, not this package's. Since real Leaflet plugins are conventionally built by extending those same classes, a plugin layer or control nests correctly under `<leaflet-map>`, `<leaflet-layer-group>`, `<leaflet-feature-group>`, or `<leaflet-control-layers>` with no special-casing on either side.
### Typing events
`LeafletAddEventListener<TEvents>`/`LeafletRemoveEventListener<TEvents>` narrow `addEventListener`/`removeEventListener` for `leaflet:<name>` events, the same way every component in this package does:
`event-types.ts`'s fragments (`MouseEvents`, `PopupBindEvents`, `DragEvents`, `TileEvents`, ...) and their compositions (`PathEvents`, `MarkerEvents`, `TileLayerEvents`, `GroupEvents`, `MapEvents`, ...) are all exported for reuse; compose your own if your plugin fires events none of them cover. If you also want `document.createElement('my-cluster-layer')` to infer your class, augment `HTMLElementTagNameMap` the same way this package does for its own tags:
```ts
declare global {
interface HTMLElementTagNameMap {
'my-cluster-layer': MyClusterLayer;
}
}
```
### Custom marker icons
`<leaflet-marker>` listens for a generic `icon-changed` event and calls `setIcon()` with whatever `detail.icon` holds — it doesn't check which component fired it. A component wrapping a plugin that provides its own icon (e.g. a themed marker icon set) just needs to call `emitIconChanged`:
```ts
import { emitIconChanged } from 'leaflet-components';
// after building or updating your icon:
emitIconChanged(this, myPluginIcon);
// and on disconnect:
emitIconChanged(this, null);
```
and it plugs into any `<leaflet-marker>` immediately, the same as this package's own `<leaflet-icon>`/`<leaflet-div-icon>`.
### One ordering pitfall, if your plugin has multiple custom elements
`customElements.define()` upgrades every matching element already parsed into the page immediately and synchronously. If one of your custom elements listens for a bubbling announcement fired by another of your own custom elements on connect (the way `<leaflet-polygon>` listens for `<leaflet-line>`), the listening one must be registered (`customElements.define`d) first — otherwise, on a real static-HTML page, every instance of the "announcer" tag upgrades and fires its one-shot announcement before the "listener" tag even exists, and it's lost for good. This only matters for relationships _within_ your own plugin; it doesn't affect nesting into this package's components, which is handled by the bubbling registration protocol above, not a one-shot announcement.