# 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(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 `` can turn them off. | | `disabled(opts?)` | the inverse of `bool(true)`: attribute named `disable-`, `` reads as `dragging === false` | | `json(default, opts?)` | `JSON.parse` ↔ `JSON.stringify` — bounds, icon sizes/anchors, GeoJSON data | `positional(def)` wraps any of the above to set `option: false`. ## Shared fragments `src/core/shared-props.ts` — spread these into a `PROPS` table: - **`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.