@element-editor/adapter-leaflet
Complete API reference for the Leaflet editing layer.
Install
npm install @element-editor/adapter-leaflet leaflet react-leafletPeer dependencies
| Package | Version |
|---|---|
leaflet | ^1.9 |
react-leaflet | ^5 |
react | ^19.2 |
react-dom | ^19.2 |
React 18 is not supported. @element-editor/core and @element-editor/ui are installed as regular dependencies (not bundled). To import from them directly, add them to your own package.json at the same version. The geometry worker is inlined, so no bundler configuration is needed.
CSS import
Warning: Always import the adapter stylesheet. The JavaScript entry doesn't import any CSS; without it, map tiles, markers and the editor UI break silently.
import '@element-editor/adapter-leaflet/style.css'It includes Leaflet's CSS (leaflet/dist/leaflet.css) and the data table's CSS. It doesn't reset your page: Tailwind's base styles apply only inside [data-elm-theme] elements and the editor's own portals.
Requirements
ElementEditorUIProvidermust be an ancestor ofElementEditorControls. It provides the error boundary, config, shortcut configuration, events and toasts, and the map controller.ElementEditorControlsthrows without the shortcut and event contexts.GeoStoreProvidermust wrapMapContainer(or any ancestor thereof). It creates the isolated Zustand store. One per editor.- The map and all Element Editor UI must be inside an element with
data-elm-theme="light"or"dark". It scopes the editor's styles and its keyboard shortcuts. ElementEditorControlsmust be rendered as a child ofMapContainer. It usesuseMap()from react-leaflet internally.
import { MapContainer, TileLayer } from 'react-leaflet'
import { ElementEditorUIProvider, GeoStoreProvider, ElementEditorControls } from '@element-editor/adapter-leaflet'
import '@element-editor/adapter-leaflet/style.css'
export default function MyMap() {
return (
<ElementEditorUIProvider>
<GeoStoreProvider>
<div data-elm-theme="light" style={{ width: '100%', height: '600px' }}>
<MapContainer center={[48.86, 2.35]} zoom={5} style={{ width: '100%', height: '100%' }}>
<TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png" />
<ElementEditorControls onChange={(data) => console.log(data)} />
</MapContainer>
</div>
</GeoStoreProvider>
</ElementEditorUIProvider>
)
}<ElementEditorControls>
import { ElementEditorControls } from '@element-editor/adapter-leaflet'Renders no visible HTML. Mounts the Element editing layer into the Leaflet map. Also mounts the shared keyboard-shortcut listeners — do not call useKeyboardShortcuts() again in your tree or every shortcut will fire twice.
ElementEditorControlsProps
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
initialData | FeatureCollectionInput | GeoJSONFeatureInput | null | — | No | The starting data: any GeoJSON FeatureCollection (@types/geojson's type-checks) or a single Feature. Applied on mount, with no undo step and no onChange. Data that arrives later (null → data) is applied once too, while the editor is empty and unedited (the controls don't move the map: fit it with ref.fitBounds). Other later changes are ignored, with a development warning: use ref.setFeatureCollection(data, { silent: true }). Invalid data reports an error event and the editor starts empty; features with geometry: null are left out and reported. |
schema | Schema | — | No | A schema added to the store on mount. Re-renders that keep the same schema.id are no-ops. |
onChange | (data: FeatureCollection) => void | — | No | Fires on every committed change with the full current FeatureCollection. |
onSubmit | (data: FeatureCollection) => void | — | No | Called by ref.submit() when all features pass validation. Receives the collection with _validation stripped. The adapter renders no submit button. |
onValidationError | (errors: Record<string, string[]>) => void | — | No | Called by ref.submit() when features have validation errors. Keys are feature IDs. Blocks onSubmit. |
onEvent | (event: ElementEditorEvent) => void | — | No | Receives every message the editor would show the user; when set, no toasts are shown. Wraps a local EventProvider, so you don't need ElementEditorUIProvider onEvent too. Read once at mount. See Events. |
onError | (error: ElementEditorError) => void | false | string | — | No | Called first for every runtime failure, with a stable code, source and English message; return false to stop the toast, or a string to replace its text. Runs before the enclosing ElementEditorUIProvider's handlers. See Events. |
layout | 'panel' | 'minimal' | 'split' | 'panel' | No | Layout hint read by the @element-editor/ui panels. |
tools | DrawingTool[] | all tools | No | Restrict which drawing tools are available. Keyboard shortcuts for excluded tools are suppressed. |
maxFeatures | number | — | No | Maximum number of features. Enforced by the store for every way of adding features: drawing tools and their shortcuts are disabled at the limit, duplicate, paste and break apart are refused, and imports are truncated (ImportResult.truncated). Each refusal reports an error event. Undo and redo are never blocked. |
readonly | boolean | false | No | Disable all editing interactions. Features remain visible. |
onFeatureAdd | (feature: GeoJSONFeature) => void | — | No | Fires when a feature is added. Fires after onChange for the same operation. |
onFeatureUpdate | (id: string, feature: GeoJSONFeature) => void | — | No | Fires when a feature's geometry or properties change. Fires after onChange. |
onFeatureDelete | (id: string) => void | — | No | Fires when a feature is deleted. Fires after onChange. |
onSelectionChange | (ids: string[]) => void | — | No | Fires when selection changes. Fires independently of feature data changes. |
onFeatureSelect | (feature: GeoJSONFeature | null) => void | — | No | Fires with the single selected feature, or null when nothing or several features are selected. |
onZoomChange | (zoom: number) => void | — | No | Fires when the map zoom changes. |
onToolChange | (tool: DrawingTool | null) => void | — | No | Fires with the tool that turned on (as ref.getTool() reports it), or null when it turned off. |
routingApiKey | string | — | No | Deprecated (dev warning): set config.network.routing.apiKey on the surrounding ElementEditorUIProvider, which wins when both are set. All servers the controls use (routing, Overpass, search) come from config.network: see Third-party services and self-hosting. |
bounds | [number, number, number, number] | null | — | No | Leaflet max bounds: the user can't pan outside this box. A GeoJSON bbox [west, south, east, north] ([lng, lat] order, like your data). null clears. Deprecated: the nested [[south, west], [north, east]] Leaflet-order form still works and logs a one-time dev warning with the bbox equivalent. |
getFeatureStyle | (feature: GeoJSONFeature) => (FeatureStyle & PathOptions) | undefined | null | — | No | FeatureStyle plus any Leaflet PathOptions per feature (null/undefined = default look), applied to polygons, lines and point pins (pins take fillColor, then color). fillColor falls back to color, as on MapLibre, so a FeatureStyle callback works unchanged. Wins over config.featureColor per key; the selected look wins over it, hover keeps the colour. Called on every render; keep it fast. See Colouring features. |
renderer | 'canvas' | 'svg' | 'canvas' | No | How unselected polygons and lines are drawn. 'canvas' paints them on one shared <canvas> (in a pane named elmIdleFeatures, just below overlayPane), so panning and zooming stay smooth with thousands of features. 'svg' gives each feature its own <path>; use it if your app styles or queries feature paths through CSS or the DOM. Selected features, editing handles and drawing previews are always SVG. Points are markers in both modes. |
onReady | (map: L.Map) => void | — | No | Called once per map, as soon as Element's layers and panes are on it. |
ElementEditorControlsRef
The imperative handle exposed via ref: ElementEditorImperativeAPI<L.Map> from @element-editor/ui, the same surface as ElementEditorRef in @element-editor/react. Data-changing methods behave like the user's own edits (validation, maxFeatures, undo, onChange / onFeature* callbacks) and also work in readonly mode.
interface ElementEditorControlsRef {
/** Returns the current FeatureCollection. `_validation` is stripped. */
getFeatureCollection: () => FeatureCollection
/** One feature by id (hidden ones too), or null for an unknown id. */
getFeature: (id: string) => GeoJSONFeature | null
/**
* Replaces all features (like importGeoJSON(data, 'replace')). Fires onChange and is one undo
* step unless `{ silent: true }` (no data callbacks) / `{ undoable: false }` (clears the history).
*/
setFeatureCollection: (data: FeatureCollectionInput, options?: { silent?: boolean; undoable?: boolean }) => void
/** Returns the current FeatureCollection serialised as GeoJSON text. */
exportGeoJSON: () => string
/** Triggers a `features.geojson` file download. */
exportAs: (format: 'geojson') => void
/** Loads a FeatureCollection. `mode` defaults to 'replace'. */
importGeoJSON: (raw: FeatureCollectionInput | unknown, mode?: 'replace' | 'merge') => ImportResult
/** Adds a feature; returns it as stored (with its id), or null (invalid geometry, id taken, maxFeatures). Leaves the selection alone. */
addFeature: (feature: GeoJSONFeatureInput, options?: { undoable?: boolean }) => GeoJSONFeature | null
/** Patches one feature (`properties` / `geometry` / `schemaId` / `hidden`). False for an unknown id or a malformed patch. */
updateFeature: (id: string, patch: FeaturePatch, options?: { undoable?: boolean }) => boolean
/** Updates many features in one commit and one undo step. See ElementEditorRef. */
updateFeatures: (updates: { id: string; patch: FeaturePatch }[], options?: { undoable?: boolean }) => void
/** Removes features in one undo step; returns how many were removed. */
removeFeatures: (ids: string[], options?: { undoable?: boolean }) => number
/** Ids of the selected features. */
getSelection: () => string[]
/** Replaces the selection (unknown / hidden ids left out); `fit` zooms to it. Fires onSelectionChange. */
select: (ids: string[], options?: { fit?: boolean }) => void
/** Turns a drawing tool on (`null`: off). False when unavailable (unknown, `tools`, maxFeatures, readonly). */
setTool: (tool: DrawingTool | null) => boolean
/** The tool that is on, or null (also while a shape operation such as cut or buffer runs). */
getTool: () => DrawingTool | null
/** Whether undo() / redo() would change something. */
canUndo: () => boolean
canRedo: () => boolean
/** Validates every feature against its schema and geometry; changes nothing, calls no callback. */
validate: () => { valid: boolean; errors: Record<string, string[]> }
/** The L.Map you passed in (null only before the controls have mounted). */
getMap: () => L.Map | null
/** Flies the map to `center` (`[lng, lat]`). `zoom` defaults to 15. The `([lat, lng], zoom?)` tuple form is deprecated (dev warning). */
flyTo: (options: { center: [number, number]; zoom?: number }) => void
/** Sets the zoom, keeping the center. Returns false if the map isn't ready or the zoom is invalid. */
setZoom: (zoom: number) => boolean
/**
* Fits the map to a bbox tuple `[minLng, minLat, maxLng, maxLat]` or a list
* of `[lng, lat]` points. No-op when the map controller isn't ready yet.
*/
fitBounds: (target: [number, number][] | [number, number, number, number]) => void
/** Validates all features, then calls onSubmit or onValidationError. Returns true when valid. */
submit: () => boolean
/** Undo the last committed change. */
undo: () => void
/** Redo the last undone change. */
redo: () => void
/** Clears the current selection. */
clearSelection: () => void
}
interface ImportResult {
skipped: number
duplicates: number
truncated: boolean
}Same methods as ElementEditorRef in @element-editor/react (both are ElementEditorImperativeAPI from @element-editor/ui; here getMap() is typed L.Map). See ElementEditorRef for ImportResult, MutationOptions and the details of each method.
Usage:
import { useRef } from 'react'
import { ElementEditorControls, type ElementEditorControlsRef } from '@element-editor/adapter-leaflet'
import { getFeatureCollectionBBox } from '@element-editor/core' // add @element-editor/core to your package.json
const ref = useRef<ElementEditorControlsRef>(null)
<ElementEditorControls ref={ref} />
// Later:
const data = ref.current?.getFeatureCollection()
ref.current?.setFeatureCollection(newData)
ref.current?.undo()
ref.current?.redo()
ref.current?.clearSelection()
ref.current?.select(['a', 'b'], { fit: true })
ref.current?.setTool('polygon')
const added = ref.current?.addFeature({ type: 'Feature', geometry: { type: 'Point', coordinates: [14.42, 50.09] }, properties: {} })
ref.current?.updateFeature(id, { properties: serverProps }, { undoable: false }) // e.g. a server push: not undoable, survives the user's undo
// Zoom-to-fit after an import:
const bbox = getFeatureCollectionBBox(newData)
if (bbox) ref.current?.fitBounds(bbox)DrawingTool
type DrawingTool =
| 'polygon' | 'rectangle' | 'circle' | 'line' | 'point' | 'freehand' | 'route'
| 'picker' | 'measure' | 'search'GeoStoreProvider
import { GeoStoreProvider } from '@element-editor/adapter-leaflet'Creates an isolated Zustand store instance. All @element-editor/ui components and ElementEditorControls within the same provider share this store. Re-exported from @element-editor/ui for convenience — both are the same component.
ElementEditorUIProvider
import { ElementEditorUIProvider } from '@element-editor/adapter-leaflet'The same component as @element-editor/adapter-maplibre's and @element-editor/ui's (it lives in @element-editor/ui; both adapters re-export it).
Bundles the full UI context stack — ElementEditorErrorBoundary, ElementEditorConfigProvider, ShortcutConfigProvider, EventProvider, MapControllerProvider — in the order @element-editor/ui panels expect. Wrap your map component with this when you want toolbars / panels / modals from @element-editor/ui outside the MapContainer. ElementEditorControls needs it too.
| Prop | Type | Default | Description |
|---|---|---|---|
config | ElementEditorConfig | — | Theme mode, tools, UI visibility, toolbar layout, units. See ElementEditorConfig. ui.geojsonEditorOpen opens the inspector on mount (it sets codeModalOpen), so it shows when you render GeoJSONModal from that flag as in the quickstart; pass fallback={<GeoJSONDockPlaceholder />} to its <Suspense> so the controls don't jump while its chunk loads. |
accentColor | string | — | Brand accent: any CSS colour. Wins over config.accentColor. <ElementEditorControls> writes it as --elm-accent onto the nearest [data-elm-theme] ancestor of the map container; Element's portals get it too. Updates live. See Theming. |
featureColor | string | — | Default colour of every feature (polygon outline + fill, lines, points). Wins over config.featureColor; getFeatureStyle and the selected look win over it, hover keeps it. Opacity and weight keep their defaults. Updates live. See Colouring features. |
onEvent | (event: ElementEditorEvent) => void | — | Receives every user-facing message; when set, no toasts are shown. |
onError | (error: ElementEditorError) => void | false | string | — | Called first for every runtime failure (code, source, English message), including a render crash caught by the error boundary (code: 'crash', the original error is cause). Return false to stop the toast, or a string to replace its text. See Events. |
toaster | boolean | true | Render Element's own sonner <Toaster>. Its toasts carry a per-instance id, so a <Toaster> your app renders, or a second editor, never shows them twice. Pass false to send toasts to your own untagged <Toaster>. |
children | ReactNode | — | Your map, GeoStoreProvider and UI. |
It doesn't set data-elm-theme or ThemeContext; render those yourself (see Theming).
Exports
| Export | Kind | Description |
|---|---|---|
ElementEditorControls | Component | The editing layer component. |
ElementEditorUIProvider | Component | Context stack for the editor and the @element-editor/ui components. |
GeoStoreProvider, StoreContext | Component / context | Store provider. Also exported by @element-editor/ui. |
MapControllerProvider, useMapController, useSetMapController | Component / hooks | Map controller context (fly-to, fit-bounds). Re-exported from @element-editor/ui. |
useMapInteractionMachine | Hook | The interaction state machine (draw/select/vertex/transform modes). |
FloatingToolbar, EditingToolbar, ActiveDrawingToolbar, useKeyboardShortcuts | Components / hook | Re-exported from @element-editor/ui. Don't call useKeyboardShortcuts yourself: ElementEditorControls already mounts it. |
FeatureStyleProvider, useFeatureStyle | Component / hook | Per-feature style context (what getFeatureStyle feeds). |
FitBounds, DrawingLayer, FeatureInteractionLayer, DrawingPreviewLayer, SnapController, TransformControls, RoutePreviewLayer, BoundaryLayer, MeasurementTooltip | Components | Individual map layers, for advanced use. ElementEditorControls renders them for you, except FitBounds (fits the map to the store's features once on mount; mount it inside the MapContainer if you want that). FeatureInteractionLayer draws every feature once (unselected ones on a shared canvas unless renderer="svg"). |
toLeaflet, toGeoJSON, findSnapPoint, snapToAngle | Functions | Coordinate conversion and snapping utilities. |
useDataStore | Hook | Access the data store (features, schemas, groups). |
useUIStore | Hook | Access the UI store (modal state, tool selection, etc.). |
useGeoStore | Hook | Access the raw store instances (dataStore, uiStore). |
ElementEditorError | Class | Re-exported from @element-editor/core. What onError receives (see Events). |
ElementEditorEvent, ElementEditorErrorHandler, ErrorCode, ErrorSource | Types | onEvent / onError types. |
ElementEditorControlsProps, ElementEditorControlsRef, DrawingTool, FeatureRenderer, ElementEditorUIProviderProps, MapController, FeatureStyle, FeatureStyleFn, MapInteractionMachine, InteractionPhase, ElementEditorConfig, ResolvedConfig, ToolbarPosition | Types |
The data types (FeatureCollection, GeoJSONFeature, Schema, …) are not re-exported. Import them from @element-editor/core.