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/02-props-and-attributes.md

91 lines
8.5 KiB
Markdown

# 02 — Props & attributes
`src/core/props.ts` defines the model; `src/core/shared-props.ts` bundles the
fragments components reuse. `WithProps` (`src/core/with-props.ts`) is the only
consumer — components declare a table and never touch the plumbing.
## The `PropDef`
Every element property is one `PropDef`:
| Field | Meaning |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `default` | Value when the attribute is absent. **Must equal Leaflet's own default** — an absent attribute is left out of the options object entirely, so it's Leaflet's default that actually takes effect. `default` only feeds the property getter's fallback. |
| `decode(raw)` | attribute string → value |
| `encode(value)` | value → attribute string, or `null` to _remove_ the attribute (which restores Leaflet's default) |
| `attribute?` | Override the derived kebab-case name. A function form receives the property name (used by `disabled()` to prefix `disable-`). |
| `option?: false` | Set only through `positional()`. Marks a value the Leaflet **constructor takes as an argument** (coordinates, url, bounds, GeoJSON data), so it's excluded from the options object and from `PropOptionValues`. |
| `set?(obj, value, el)` | Push a new value into the live object. Omitted ⇒ the mixin calls the naming-convention setter (`radius` → `obj.setRadius`) if it exists, else no-op. |
| `get?(obj)` | Read the live value back out. Used by the property getter and by two-way sync. |
| `event?` | Leaflet event after which `get` is re-read and written to the attribute (`move` keeps `lat`/`lng` current during a drag). Only meaningful together with `get`. |
## Codec factories
Instead of writing `decode`/`encode` by hand, components call a factory:
| Factory | Attribute semantics |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `num(default?, opts?)` | `Number``String` |
| `str(default?, opts?)` | identity both ways |
| `choice<T>(default, opts?)` | like `str`, but typed as a string union (`ControlPosition`, `CrossOrigin`, tooltip `Direction`); **no runtime validation** — the point is that the options object comes out with the type Leaflet's constructor expects |
| `bool(default?, opts?)` | present ⇒ `true`, `="false"``false`, absent ⇒ `default`. `encode` returns `null` when the value equals the default, so the attribute only appears when it's doing something. Use `bool(true)` for options Leaflet defaults on, so `<leaflet-popup auto-pan="false">` can turn them off. |
| `disabled(opts?)` | the inverse of `bool(true)`: attribute named `disable-<kebab>`, `<leaflet-map disable-dragging>` reads as `dragging === false` |
| `json<T>(default, opts?)` | `JSON.parse``JSON.stringify` — bounds, icon sizes/anchors, GeoJSON data |
`positional(def)` wraps any of the above to set `option: false`. Its result
type is aliased as `Positional<T, Obj>` (= `PropDef<T, Obj> & { option:
false }`) in `props.ts`, for use in the explicit `const PROPS` annotations
each element file needs (see [01](./01-architecture.md) and
[07](./07-tooling-and-build.md)).
## Shared fragments
`src/core/shared-props.ts` — spread these into a `PROPS` table. Each is
exported with an **explicit object type** (not just `as const`), so a
`PROPS` annotation can pull it in via `typeof pathProps` / `typeof
latLngProps` / `typeof tileLayerProps` without tripping JSR's slow-type
check:
- **`latLngProps`** (`lat`, `lng`) — the coordinate pair, shared by marker,
circle, circle-marker, popup, tooltip. Both are `positional` (passed to the
constructor). Because they travel together, each one's `set` re-issues
`obj.setLatLng([...])` using the _other_ axis read off the host element
(`el.lat` / `el.lng`). Both name `event: 'move'` for write-back, which is
what keeps the attributes live while a marker is dragged.
- **`pathProps`** — every SVG style option a Leaflet `Path` accepts. The
mutable ones (`color`, `weight`, `opacity`, `fill*`, …) use a `style(key)`
helper whose `set` calls `obj.setStyle({ [key]: value })`, because Leaflet
exposes these only through `setStyle`. The constructor-only tail
(`className`, `interactive`, `pane`, …) has no `set` and so no post-create
effect.
- **`tileLayerProps`** — the `GridLayer`/`TileLayer` options common to
`leaflet-tile-layer` and `leaflet-tile-layer-wms` (WMS options extend tile
layer options). Almost all constructor-only. `referrerPolicy` is a
hand-written `PropDef` rather than `choice()` because Leaflet's
`ReferrerPolicy` type has no "unset" member — its absent-attribute fallback
is `undefined`.
- **`urlProp`** — the positional source URL for `TileLayer` / `ImageOverlay`
/ `VideoOverlay`. Its `set` ignores a blank value so clearing the attribute
can't request an empty tile URL. No `get` — none of those classes expose
`getUrl()`.
- **`getBounds(obj)`** — a shared `get` returning `[[s,w],[n,e]]` (matching
the JSON attribute shape) rather than a `LatLngBounds` instance.
## Two-way sync and cycle prevention
1. User sets a property → setter encodes it onto the attribute.
2. `attributeChangedCallback` fires → prop's `set` (or the convention setter)
pushes it into the live object.
3. Leaflet mutates and fires an event (e.g. `move`).
4. The mixin's per-event listener reads `get(obj)` for every prop naming that
event and writes it back to the attribute — but does so with the private
`#syncing` flag set, so the `attributeChangedCallback` this write triggers
returns immediately instead of looping back to step 2.
`#syncing` is the single guard. `attributeChangedCallback` also bails when
`oldValue === newValue`, and when the element isn't connected yet.