refactor: extract child-registration boilerplate into WeakMap helpers

Adds registerChildren / unregisterChildren / getChildren to
register.ts using module-scoped WeakMaps, removing the duplicated
#children / #childHandler fields + lifecycle wiring from 15
components.

registerChildren(el, layer) creates the children Map + handler,
stores both in WeakMaps, wires the event listener, and calls
registerWithParent — all in one call. unregisterChildren(el)
tears it down. getChildren(el) gives access to the map for
components that need custom child iteration in disconnectedCallback
(geojson, layer-group, feature-group).

Net: -101 lines, 15 components lose 2-4 fields + 4-8 lifecycle
lines each. Components with extra lifecycle work (marker -> dragend,
polygon/polyline -> observer + syncCoords) keep their custom
additions inline, just the register boilerplate is replaced.
main
Buddy 3 months ago
parent c18829a0e3
commit e2752aa7e0

@ -1,12 +1,8 @@
import { CircleMarker } from 'leaflet';
import { registerWithParent, buildOptions, numAttr } from '../core/utils.js';
import { buildOptions, numAttr } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef } from '../types/props.js';
@ -24,22 +20,17 @@ const PROPS = {
export class LeafletCircleMarker extends withProps(HTMLElement, PROPS) {
#obj?: CircleMarker;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
this.#obj = new CircleMarker(
[numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')],
buildOptions(this, PROPS, ['lat', 'lng']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { Circle } from 'leaflet';
import { registerWithParent, buildOptions, numAttr } from '../core/utils.js';
import { buildOptions, numAttr } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
type LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef } from '../types/props.js';
@ -24,22 +20,17 @@ const PROPS = {
export class LeafletCircle extends withProps(HTMLElement, PROPS) {
#obj?: Circle;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
this.#obj = new Circle(
[numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')],
buildOptions(this, PROPS, ['lat', 'lng']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,15 +1,9 @@
import { FeatureGroup, Layer } from 'leaflet';
import { buildOptions, registerWithParent } from '../core/utils.js';
import {
createChildRegisterHandler,
type ChildEntry,
type LeafletRegisterEvent,
} from '../core/register.js';
import { buildOptions } from '../core/utils.js';
import { registerChildren, unregisterChildren, getChildren } from '../core/register.js';
export class LeafletFeatureGroup extends HTMLElement {
#obj?: FeatureGroup;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
static get observedAttributes() {
return [];
@ -17,19 +11,16 @@ export class LeafletFeatureGroup extends HTMLElement {
connectedCallback() {
this.#obj = new FeatureGroup([], buildOptions(this, {}));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
for (const [el, entry] of this.#children) {
for (const [el, entry] of getChildren(this) ?? []) {
if (entry.type === 'popup') this.#obj?.unbindPopup();
else if (entry.type === 'tooltip') this.#obj?.unbindTooltip();
else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
}
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { GeoJSON, PathOptions, Layer } from 'leaflet';
import { buildOptions, registerWithParent } from '../core/utils.js';
import { buildOptions } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren, getChildren } from '../core/register.js';
import type { PropDef } from '../types/props.js';
const PROPS = {
@ -31,8 +27,6 @@ const PROP_BY_ATTR = new Map<string, string>(
export class LeafletGeoJSON extends withProps(HTMLElement, PROPS) {
#obj?: GeoJSON;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
const raw = this.getAttribute('data');
@ -41,19 +35,16 @@ export class LeafletGeoJSON extends withProps(HTMLElement, PROPS) {
Object.entries(buildOptions(this, PROPS, ['data'])).filter(([, v]) => v !== ''),
);
this.#obj = new GeoJSON(data, { style: styleOpts as PathOptions });
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
for (const [el, entry] of this.#children) {
for (const [el, entry] of getChildren(this) ?? []) {
if (entry.type === 'popup') this.#obj?.unbindPopup();
else if (entry.type === 'tooltip') this.#obj?.unbindTooltip();
else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
}
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { ImageOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { buildOptions, parseAttributeValue } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import type { PropDef } from '../types/props.js';
const PROPS = {
@ -27,8 +23,6 @@ const PROP_BY_ATTR = new Map<string, string>(
export class LeafletImageOverlay extends withProps(HTMLElement, PROPS) {
#obj?: ImageOverlay;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
const url = this.getAttribute('url') || '';
@ -37,14 +31,11 @@ export class LeafletImageOverlay extends withProps(HTMLElement, PROPS) {
this.#parsedBounds(),
buildOptions(this, PROPS, ['url', 'bounds']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,15 +1,9 @@
import { LayerGroup, Layer } from 'leaflet';
import { buildOptions, registerWithParent } from '../core/utils.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { buildOptions } from '../core/utils.js';
import { registerChildren, unregisterChildren, getChildren } from '../core/register.js';
export class LeafletLayerGroup extends HTMLElement {
#obj?: LayerGroup;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
static get observedAttributes() {
return [];
@ -17,19 +11,16 @@ export class LeafletLayerGroup extends HTMLElement {
connectedCallback() {
this.#obj = new LayerGroup([], buildOptions(this, {}));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
for (const [el, entry] of this.#children) {
for (const [el, entry] of getChildren(this) ?? []) {
if (entry.type === 'popup') this.#obj?.unbindPopup();
else if (entry.type === 'tooltip') this.#obj?.unbindTooltip();
else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
}
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { Marker } from 'leaflet';
import { registerWithParent, buildOptions, numAttr, parseAttributeValue } from '../core/utils.js';
import { buildOptions, numAttr, parseAttributeValue } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import type { PropDef } from '../types/props.js';
const PROPS = {
@ -26,8 +22,6 @@ const PROP_BY_ATTR = new Map<string, string>(
export class LeafletMarker extends withProps(HTMLElement, PROPS) {
#obj?: Marker;
#syncing = false;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
this.#obj = new Marker(
@ -35,15 +29,12 @@ export class LeafletMarker extends withProps(HTMLElement, PROPS) {
buildOptions(this, PROPS, ['lat', 'lng']),
);
this.#obj.on('dragend', this.#onDragEnd);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#obj?.off('dragend', this.#onDragEnd);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { Polygon } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { buildOptions } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef } from '../types/props.js';
import type { LeafletLine } from './leaflet-line.js';
@ -23,14 +19,10 @@ const PROPS = {
export class LeafletPolygon extends withProps(HTMLElement, PROPS) {
#obj?: Polygon;
#observer?: MutationObserver;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
this.#obj = new Polygon(this.#getCoords(), buildOptions(this, PROPS));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
this.addEventListener('line-updated', this.#syncCoords);
this.#observer = new MutationObserver(() => this.#syncCoords());
@ -41,8 +33,7 @@ export class LeafletPolygon extends withProps(HTMLElement, PROPS) {
this.#observer?.disconnect();
this.#observer = undefined;
this.removeEventListener('line-updated', this.#syncCoords);
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { Polyline } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { buildOptions } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef } from '../types/props.js';
import type { LeafletLine } from './leaflet-line.js';
@ -23,14 +19,10 @@ const PROPS = {
export class LeafletPolyline extends withProps(HTMLElement, PROPS) {
#obj?: Polyline;
#observer?: MutationObserver;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
this.#obj = new Polyline(this.#getCoords(), buildOptions(this, PROPS));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
this.addEventListener('line-updated', this.#syncCoords);
this.#observer = new MutationObserver(() => this.#syncCoords());
@ -41,8 +33,7 @@ export class LeafletPolyline extends withProps(HTMLElement, PROPS) {
this.#observer?.disconnect();
this.#observer = undefined;
this.removeEventListener('line-updated', this.#syncCoords);
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { Rectangle, LatLngBoundsExpression } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { buildOptions } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef } from '../types/props.js';
@ -22,19 +18,14 @@ const PROPS = {
export class LeafletRectangle extends withProps(HTMLElement, PROPS) {
#obj?: Rectangle;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
this.#obj = new Rectangle(this.#parsedBounds(), buildOptions(this, PROPS, ['bounds']));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { SVGOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { buildOptions, parseAttributeValue } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import type { PropDef } from '../types/props.js';
const PROPS = {
@ -24,8 +20,6 @@ const PROP_BY_ATTR = new Map<string, string>(
export class LeafletSVGOverlay extends withProps(HTMLElement, PROPS) {
#obj?: SVGOverlay;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
const svg = this.querySelector('svg');
@ -35,14 +29,11 @@ export class LeafletSVGOverlay extends withProps(HTMLElement, PROPS) {
this.#parsedBounds(),
buildOptions(this, PROPS, ['bounds']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { TileLayer } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { buildOptions, parseAttributeValue } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import type { PropDef } from '../types/props.js';
const PROPS = {
@ -25,8 +21,6 @@ const PROP_BY_ATTR = new Map<string, string>(
export class LeafletTileLayerWMS extends withProps(HTMLElement, PROPS) {
#obj?: TileLayer.WMS;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
const url = this.getAttribute('url') || '';
@ -34,14 +28,11 @@ export class LeafletTileLayerWMS extends withProps(HTMLElement, PROPS) {
url,
buildOptions(this, PROPS, ['url']) as Record<string, string>,
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { TileLayer } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { buildOptions, parseAttributeValue } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import type { PropDef } from '../types/props.js';
const PROPS = {
@ -24,20 +20,15 @@ const PROP_BY_ATTR = new Map<string, string>(
export class LeafletTileLayer extends withProps(HTMLElement, PROPS) {
#obj?: TileLayer;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
const url = this.getAttribute('url') || '';
this.#obj = new TileLayer(url, buildOptions(this, PROPS, ['url']));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,12 +1,8 @@
import { VideoOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { buildOptions, parseAttributeValue } from '../core/utils.js';
import { withProps } from '../core/with-props.js';
import {
createChildRegisterHandler,
type ChildEntry,
LeafletRegisterEvent,
} from '../core/register.js';
import { registerChildren, unregisterChildren } from '../core/register.js';
import type { PropDef } from '../types/props.js';
const PROPS = {
@ -28,8 +24,6 @@ const PROP_BY_ATTR = new Map<string, string>(
export class LeafletVideoOverlay extends withProps(HTMLElement, PROPS) {
#obj?: VideoOverlay;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: (e: LeafletRegisterEvent) => void;
connectedCallback() {
const url = this.getAttribute('url') || '';
@ -38,14 +32,11 @@ export class LeafletVideoOverlay extends withProps(HTMLElement, PROPS) {
this.#parsedBounds(),
buildOptions(this, PROPS, ['url', 'bounds']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children);
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
registerChildren(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
unregisterChildren(this);
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,4 +1,5 @@
import { Layer, LayerGroup, Popup, Tooltip } from 'leaflet';
import { registerWithParent } from './utils.js';
export type LeafletRegisterEvent = CustomEvent<{
leafletObject: Layer;
@ -34,3 +35,27 @@ export function createChildRegisterHandler(layer: Layer, children: Map<HTMLEleme
}
};
}
const childrenMap = new WeakMap<HTMLElement, Map<HTMLElement, ChildEntry>>();
const handlerMap = new WeakMap<HTMLElement, (e: LeafletRegisterEvent) => void>();
export function registerChildren(el: HTMLElement, layer: Layer): Map<HTMLElement, ChildEntry> {
const children = new Map<HTMLElement, ChildEntry>();
const handler = createChildRegisterHandler(layer, children);
childrenMap.set(el, children);
handlerMap.set(el, handler);
el.addEventListener('leaflet-register', handler);
registerWithParent(el, layer);
return children;
}
export function unregisterChildren(el: HTMLElement): void {
const handler = handlerMap.get(el);
if (handler) el.removeEventListener('leaflet-register', handler);
childrenMap.delete(el);
handlerMap.delete(el);
}
export function getChildren(el: HTMLElement): Map<HTMLElement, ChildEntry> | undefined {
return childrenMap.get(el);
}

Loading…
Cancel
Save