Events

Reacting to changes in the editor.


Two patterns

Full snapshot (onChange) fires on every committed change and delivers the complete current FeatureCollection. Straightforward to wire up. Appropriate for most use cases under ~500 features.

Granular events (onFeatureAdd, onFeatureUpdate, onFeatureDelete) fire per feature. Appropriate for large datasets or real-time database sync where re-sending the full collection on every change is undesirable.

Both patterns can coexist. onChange and granular callbacks can all be provided simultaneously.


onChange

Available on: @element-editor/react, @element-editor/adapter-leaflet, @element-editor/adapter-maplibre

onChange?: (data: FeatureCollection) => void

Fires after every committed change — feature added, updated, deleted, or undo/redo applied. The data argument is the complete, current FeatureCollection with metadata (schemas and groups) included. _validation is stripped from all features.

<ElementEditor onChange={(data) => saveToDatabase(data)} />

Granular events

Available on: @element-editor/react, @element-editor/adapter-leaflet, @element-editor/adapter-maplibre

onFeatureAdd?: (feature: GeoJSONFeature) => void
onFeatureUpdate?: (id: string, feature: GeoJSONFeature) => void
onFeatureDelete?: (id: string) => void
onSelectionChange?: (ids: string[]) => void

onFeatureAdd

Fires when a new feature is added to the store. Fires after onChange for the same operation.

onFeatureUpdate

Fires when an existing feature's geometry or properties change. Fires after onChange for the same operation.

onFeatureDelete

Fires when a feature is removed from the store. Receives the deleted feature's id. Fires after onChange for the same operation.

onSelectionChange

Fires when the set of selected feature IDs changes. Fires independently of feature data changes — selecting or deselecting a feature does not trigger onChange, onFeatureAdd, onFeatureUpdate, or onFeatureDelete.

<ElementEditorControls
  onFeatureAdd={(feature) => db.insert(feature)}
  onFeatureUpdate={(id, feature) => db.update(id, feature)}
  onFeatureDelete={(id) => db.delete(id)}
  onSelectionChange={(ids) => setSelectedIds(ids)}
/>

Selection, zoom, submit and notification events

Available on: @element-editor/react, @element-editor/adapter-leaflet, @element-editor/adapter-maplibre

onFeatureSelect?: (feature: GeoJSONFeature | null) => void
onZoomChange?: (zoom: number) => void
onSubmit?: (data: FeatureCollection) => void
onValidationError?: (errors: Record<string, string[]>) => void
onError?: (error: ElementEditorError) => void | false | string
onEvent?: (event: ElementEditorEvent) => void

onFeatureSelect

Fires when a single feature is selected. Passes null when no feature is selected or when multiple features are selected.

onZoomChange

Fires when the map zoom level changes. Receives the new zoom level as a number.

onSubmit

Fires when a submit is triggered and all required schema fields pass validation. Receives the full FeatureCollection, with _validation stripped. On <ElementEditor>, providing onSubmit renders an "Apply changes" button (unless the editor is read-only). The adapters render no button: call ref.submit() from your own UI.

onValidationError

Fires when ref.submit() is triggered but one or more required schema fields are empty or invalid. Receives a Record<string, string[]> where keys are feature IDs and values are arrays of error messages. Blocks onSubmit from firing.

onError

Called first for every runtime failure, with an ElementEditorError: an Error (it has a stack, so Sentry and similar tools take it as is) with a stable machine-readable code, a source, and an honest English message (what happened, what to do). Use it to log failures, show your own UI, or translate.

type ElementEditorErrorHandler = (error: ElementEditorError) => void | false | string

class ElementEditorError extends Error {
  code: ErrorCode
  source: ErrorSource
  featureId?: string   // the feature the failed operation was about
  cause?: unknown      // the underlying error (HTTP error, DOMException, worker error, …)
}

The return value decides what Element Editor shows:

ReturnBuilt-in toastonEvent error event
nothing (undefined)shownsent
falsenot shown: you show itnot sent
a stringshows that text insteadsent with that text

onError fires in managed mode too (with onEvent set), before onEvent. It also fires for quiet failures that show no toast: a failed geometry preview (buffer / simplify / smooth), the system clipboard refusing a copy, the search box's own error line, a map style without glyphs. Validation (onValidationError) and dev-only console.warns for API misuse are not errors and never reach it. Refusals of an action (deleting a polygon's third-last vertex, an invalid zoom value) are warning events, not errors. A maxFeatures refusal is an error ('limit-reached'), because the limit is your policy and you may want to act on it.

<ElementEditor
  onError={(e) => {
    Sentry.captureException(e, { tags: { code: e.code, source: e.source } })
    if (e.code === 'rate-limited') return t('errors.rateLimited') // translated toast
    if (e.code === 'limit-reached') { openUpgradeDialog(); return false } // own UI, no toast
  }}
/>

ErrorCode and ErrorSource are closed unions covered by semver (a new code is a minor release; renaming or removing one is a major):

codeMeaningTypical sources
networkThe request failed (no connection, CORS, a non-retryable HTTP status, a lazy chunk that didn't load).basemap, picker, routing, search
rate-limitedHTTP 429: wait and retry.picker, routing, search, basemap
busyHTTP 502–504: the service is overloaded, retry later.picker, routing, search, basemap
timeoutNo answer in time (Overpass, the 30 s geometry worker limit).picker, geometry, locate
unauthorizedHTTP 401/403: a refused key (e.g. the routing API key).routing, basemap
invalid-dataUnreadable input or response: invalid JSON, not a FeatureCollection, an HTML error page; features in initialData / setFeatureCollection data that have no usable geometry (geometry: null) and were left out.import, picker, search, basemap
too-largeA file over the import size cap.import
limit-reachedmaxFeatures refused an add or truncated an import.editor, import
geometryA geometry operation failed or produced an empty shape.geometry, editor
permission-deniedThe browser refused a permission (geolocation, clipboard).locate, clipboard
unsupportedThe browser or the map lacks something (no geolocation / clipboard API, a style without glyphs).locate, clipboard, basemap
crashThe editor crashed while rendering; the error boundary shows its fallback.editor
unknownAnything else.any

ErrorSource is one of 'basemap' | 'picker' | 'routing' | 'search' | 'import' | 'export' | 'clipboard' | 'geometry' | 'locate' | 'editor'. OverpassError (from fetchOverpassFeatures) is an ElementEditorError with source: 'picker'.

On the adapters, pass onError to ElementEditorUIProvider (it also gets render crashes) or to ElementEditorControls. A local onError on the controls runs first, then the event goes on to the provider (its onError, onEvent or toaster).

onEvent

Intercepts all internal library events. Every message Element shows the user (errors, warnings, confirmations, including errors such as invalid initialData) goes through this channel. When onEvent is provided, the library is fully silent: no toasts are shown, and the host application is responsible for surfacing notifications. onEvent is read once at mount; adding or removing it later has no effect.

Without onEvent, Element shows its own toasts. <ElementEditor> and ElementEditorUIProvider each render their own sonner <Toaster>, and every toast is tagged with that instance's id. A <Toaster> your app already renders, or a second editor on the page, never shows the same toast twice, and you don't need to add a <Toaster> yourself. On ElementEditorUIProvider, pass toaster={false} to send toasts to your own untagged <Toaster> instead.

Element's toasts are quiet: no icons and no per-type colours, and they follow the editor's theme (a dark toast on a dark editor, on the same surface as its panels). To get the same look in your own toaster, render <ElementToaster /> from @element-editor/ui (it takes every sonner <Toaster> prop) inside the editor's providers, or pass theme yourself outside them.

error, warning and success events may carry a description: a second line with more detail. The default toast shows it under the message.

<ElementEditor
  onEvent={(event) => {
    if (event.type === 'feature:deleted') {
      showToast(`Deleted "${event.label}"`, {
        action: { label: 'Undo', onClick: event.undo },
      })
    }
    if (event.type === 'error') {
      showErrorToast(event.message) // event.code / event.source as on onError
    }
  }}
/>

See the ElementEditorEvent type section below for all event shapes.


Firing order

ScenarioOrder
Feature addedonChange → onFeatureAdd
Feature updatedonChange → onFeatureUpdate
Feature deletedonChange → onFeatureDelete
Selection changedonSelectionChange (independent, no onChange)

The ref's data methods (addFeature, updateFeature(s), removeFeatures, select, importGeoJSON, …) fire the same callbacks as the equivalent user edit, synchronously before the method returns. Within one commit the order is onChange → per-feature callbacks → onSelectionChange → onFeatureSelect.

onChange fires once per commit. The granular per-feature callbacks then fire from a single diff pass — one onFeatureAdd per new id, one onFeatureUpdate per changed geometry/properties reference, one onFeatureDelete per removed id. A bulk delete fires N onFeatureDelete calls within the same commit.


ElementEditorEvent

type ElementEditorEvent =
  // Feature lifecycle
  | { type: 'feature:deleted'; featureId: string; label: string; undo: () => void }
  | { type: 'features:deleted'; count: number; undo: () => void }
  | { type: 'feature:duplicated'; featureId: string; label: string }
  | { type: 'features:duplicated'; count: number }
  | { type: 'feature:broken'; featureId: string; count: number; undo: () => void }
  | { type: 'selection:copied' }
  | { type: 'selection:pasted'; count: number }

  // History
  | { type: 'undo' }
  | { type: 'redo' }

  // Notifications
  | { type: 'error'; code: ErrorCode; source: ErrorSource; message: string; description?: string }
  | { type: 'warning'; message: string; description?: string }
  | { type: 'success'; message: string; description?: string }

Event reference

typeWhen it firesNotable fields
feature:deletedA single feature is deleted. No default toast — the disappearance is its own confirmation; Cmd/Ctrl+Z still undoes. Use onEvent if you want a custom undo affordance.featureId, label, undo()
features:deletedMultiple features are deleted at once. Same default-silence behaviour as feature:deleted.count, undo()
feature:duplicatedA single feature is duplicated. No default toast — the new copy on the map is its own confirmation.featureId, label
features:duplicatedMultiple features are duplicated. Same default-silence behaviour as feature:duplicated.count
feature:brokenA multi-geometry feature is broken into parts.featureId, count, undo()
selection:copiedThe selection is copied to the clipboard.—
selection:pastedFeatures are pasted from the clipboard.count
undoAn undo operation is performed.—
redoA redo operation is performed.—
errorA recoverable error occurs (after onError, unless it returned false).code, source, message, description?
warningA non-blocking warning or a refused action (e.g. circle converted to polygon, a vertex that can't be deleted, an import that skipped features).message, description?
successA positive confirmation (e.g. coordinate updated).message, description?

The undo() function on feature:deleted, features:deleted, and feature:broken events calls dataStore.undo() — the same as pressing Cmd/Ctrl+Z.


When to use which

ScenarioRecommended
Save to localStorageonChange
Send to a REST APIonChange
Real-time sync / CRDTGranular events (onFeatureAdd/Update/Delete)
500+ featuresGranular events
React to selection in your UIonSelectionChange
Custom toast notificationsonEvent
Error logging, own error UI, translated error messagesonError
Validation gate before saveonSubmit + onValidationError