leaflet
Version:
JavaScript library for mobile-friendly interactive maps
436 lines (334 loc) • 20.1 kB
Markdown
# Migrating from Leaflet 1.x to 2.0
Leaflet 2.0 is published as ES modules, uses classes instead of factory functions, and handles input with Pointer
Events. Your maps, layers, options and most method calls work as before. The changes are in how you load Leaflet and
create objects, plus a set of smaller renames and removals.
Most changes are loud. A removed API throws an error or logs one console message that says what to use instead, so
running your app finds most of what's left to fix. Changes that give no error are marked **Silent**, so check
those by hand.
Leaflet 2.0 supports [Baseline 2022](https://web.dev/baseline) browsers (Safari 15.4+). Internet Explorer and other
legacy browsers are no longer supported.
The guide has two parts:
- [Part 1, upgrading an app](#part-1-upgrading-an-app), with the most common changes first.
- [Part 2, plugins and extensions](#part-2-plugins-and-extensions), for code that subclasses Leaflet or uses its
internals.
## Part 1: Upgrading an app
### 1. Load Leaflet as a module
`dist/leaflet.js` is now an ES module, so a plain `<script>` tag fails with a syntax error. Use an import map with a
module script:
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@2.0.0-beta.1/dist/leaflet.css">
<script type="importmap">
{"imports": {"leaflet": "https://cdn.jsdelivr.net/npm/leaflet@2.0.0-beta.1/dist/leaflet.js"}}
</script>
<script type="module">
import {LeafletMap, TileLayer, Marker} from 'leaflet';
const map = new LeafletMap('map').setView([51.505, -0.09], 13);
new TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19}).addTo(map);
new Marker([51.5, -0.09]).addTo(map);
</script>
```
Variables in a module script aren't global. If other scripts on the page need the map, assign it to `window` yourself.
If you'd rather keep a global `L`, use `dist/leaflet-global.js` with a regular script tag. It mirrors the module
exports, so the rest of this guide still applies (`new L.Marker(...)`, `L.ZoomControl`). It no longer supports AMD,
CommonJS or `L.noConflict()`.
With a bundler, the package no longer has a default export:
```js
// 1.x
import L from 'leaflet';
// 2.0
import * as L from 'leaflet'; // or named imports: import {LeafletMap, Marker} from 'leaflet';
```
`import 'leaflet/dist/leaflet.css'` works as before. Other deep imports, from `leaflet/src/...` or
`leaflet/dist/...`, no longer resolve.
The ES module doesn't define a global `L`. Plugins that expect `window.L` need a version that supports Leaflet 2, or
the global build.
### 2. Replace factory functions with `new`
The lowercase factory functions are gone. Call the class constructor instead:
```js
// 1.x
const map = L.map('map');
L.marker([51.5, -0.09]).bindPopup('Hi').addTo(map);
L.control.layers(baseLayers, overlays).addTo(map);
// 2.0
const map = new LeafletMap('map');
new Marker([51.5, -0.09]).bindPopup('Hi').addTo(map);
new LayersControl(baseLayers, overlays).addTo(map);
```
Most factories become the capitalized class name: `L.tileLayer` → `TileLayer`, `L.geoJSON` → `GeoJSON`,
`L.latLng` → `LatLng`. The exceptions are the nested names in the next section, and `L.map`, which becomes
`LeafletMap`.
`Map` is exported too, but importing it shadows JavaScript's built-in `Map` in that module, so the docs use
`LeafletMap`. With the global build, `new L.Map(...)` is fine.
Factories passed `null` through (`L.latLng(null)` returned `null`), but `new LatLng(null)` and `new Point(null)`
throw. Check with `LatLng.validate()` first if the input can be missing.
### 3. Use the flat class names
Classes that were nested under another class are now top-level exports with their own names:
| 1.x | 2.0 |
|---|---|
| `Control.Zoom`, `Control.Attribution`, `Control.Layers`, `Control.Scale` | `ZoomControl`, `AttributionControl`, `LayersControl`, `ScaleControl` |
| `TileLayer.WMS` (`L.tileLayer.wms`) | `WMSTileLayer` |
| `Icon.Default` | `DefaultIcon` |
| `CRS.EPSG3857`, `CRS.EPSG3395`, `CRS.EPSG4326`, `CRS.EPSG900913` | `EPSG3857`, `EPSG3395`, `EPSG4326`, `EPSG900913` |
| `CRS.Simple`, `CRS.Earth` | `SimpleCRS`, `EarthCRS` |
| `Projection.LonLat`, `Projection.Mercator`, `Projection.SphericalMercator` | `LonLatProjection`, `MercatorProjection`, `SphericalMercatorProjection` |
| `Map.BoxZoom`, `Map.DoubleClickZoom`, `Map.Drag`, `Map.Keyboard`, `Map.ScrollWheelZoom`, `Map.TapHold`, `Map.TouchZoom` | `BoxZoomHandler`, `DoubleClickZoomHandler`, `DragHandler`, `KeyboardHandler`, `ScrollWheelZoomHandler`, `TapHoldHandler`, `PinchZoomHandler` |
```js
// 1.x
const map = L.map('map', {crs: L.CRS.Simple});
L.control.scale().addTo(map);
// 2.0
const map = new LeafletMap('map', {crs: SimpleCRS});
new ScaleControl().addTo(map);
```
### 4. Use pointer events instead of mouse events
Leaflet listens to [Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events), which cover
mouse, touch and pen with one set of events. The `mouse*` events on the map and layers are renamed:
| 1.x | 2.0 |
|---|---|
| `mousedown`, `mouseup`, `mousemove` | `pointerdown`, `pointerup`, `pointermove` |
| `mouseover`, `mouseout` | `pointerover`, `pointerout` |
| `keypress` | `keydown` |
`click`, `dblclick` and `contextmenu` don't change.
```js
// 1.x
layer.on({mouseover: highlight, mouseout: reset});
// 2.0
layer.on({pointerover: highlight, pointerout: reset});
```
Listening to a removed event logs an error once, and the listener never runs.
**Silent:** pointer events fire for touch and pen too. For example, `pointermove` fires while a finger drags the
map, while `mousemove` didn't. If a handler is meant for the mouse only, check `e.originalEvent.pointerType`.
A few related renames:
- `map.mouseEventToContainerPoint`, `mouseEventToLayerPoint` and `mouseEventToLatLng` → `pointerEventTo...`.
- `DomEvent.getMousePosition` → `DomEvent.getPointerPosition`.
- The `bubblingMouseEvents` option → `bubblingPointerEvents`. The old name still works.
- The `touchZoom` map option and handler → `pinchZoom`. The old name still works and logs a warning.
### 5. Delete your default marker icon workaround
The default marker is now an inline SVG and works with any bundler or CDN as is. The common 1.x fix for broken marker
images breaks in 2.0, because the image files and `Icon.Default` are gone. Delete it:
```js
// 1.x: delete all of this
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({iconUrl, iconRetinaUrl, shadowUrl});
L.Marker.prototype.options.icon = L.icon({...});
```
The same goes for `L.Icon.Default.imagePath = ...` and the
[leaflet-defaulticon-compatibility](https://github.com/ghybs/leaflet-defaulticon-compatibility) plugin. To use your
own icon for all markers, see [section 6](#6-change-defaults-with-mergeoptions).
Other changes to the default icon:
- Pass `color` to get a different pin color: `new Marker(latlng, {icon: new DefaultIcon({color: 'crimson'})})`.
- Markers can be rotated with the `rotation` option (in degrees) and `setRotation()`. If you use
leaflet-rotatedmarker, replace its `rotationAngle` option with `rotation`. **Silent:** the plugin doesn't work on
2.0, so `rotationAngle` does nothing.
- **Silent:** the shadow is part of the icon image, so the icon is 25×46 pixels instead of 25×41 and there's no
separate shadow image. If you pass your own 25×41 images to `DefaultIcon`, also pass `iconSize: [25, 41]` and
`shadowUrl`, or use `Icon`.
### 6. Change defaults with `mergeOptions`
Classes keep their default options in a static `defaultOptions` property instead of `prototype.options`, so code
that sets defaults on the prototype throws a "Cannot set properties of undefined" error:
```js
// 1.x
L.Marker.prototype.options.icon = myIcon;
// 2.0
Marker.mergeOptions({icon: myIcon});
```
`mergeOptions` also works in 1.x.
### 7. Write custom classes as ES classes
`L.Class.extend()` and `L.Control.extend()` are gone, and so is `initialize()`. Write a class that extends the Leaflet
class, put defaults in `static defaultOptions`, and put setup code in `constructor()` after `super()`:
```js
// 1.x
const InfoControl = L.Control.extend({
options: {position: 'topright'},
initialize(title, options) {
this._title = title;
L.setOptions(this, options);
},
onAdd(map) {
const div = L.DomUtil.create('div', 'info');
div.textContent = this._title;
return div;
}
});
// 2.0
class InfoControl extends Control {
static defaultOptions = {position: 'topright'};
constructor(title, options) {
super(options);
this._title = title;
}
onAdd(map) {
const div = DomUtil.create('div', 'info');
div.textContent = this._title;
return div;
}
}
```
The defaults in `static defaultOptions` are merged with the parent's defaults, as `options` were in 1.x. A class
with an `initialize()` method throws when constructed, with a message to rename it.
If the class was created with `extend` in code you don't control, such as an old plugin, that plugin needs a version
that supports Leaflet 2.
### 8. Use the platform instead of removed utilities
Utilities with native equivalents are removed. Most are one-line replacements:
| 1.x | 2.0 |
|---|---|
| `L.extend(a, b)`, `Util.extend(a, b)` | `Object.assign(a, b)` |
| `L.bind(fn, obj)`, `Util.bind(fn, obj)` | `fn.bind(obj)` |
| `L.stamp(obj)`, `L.setOptions(obj, options)` | `Util.stamp(obj)`, `Util.setOptions(obj, options)` |
| `Util.create(proto)` | `Object.create(proto)` |
| `Util.isArray(a)` | `Array.isArray(a)` |
| `Util.indexOf(arr, x)` | `arr.indexOf(x)` |
| `Util.trim(str)` | `str.trim()` |
| `Util.getParamString(obj)` | `'?' + new URLSearchParams(obj)` |
| `Util.requestAnimFrame(fn)`, `Util.cancelAnimFrame(id)` | `requestAnimationFrame(fn)`, `cancelAnimationFrame(id)` |
| `Util.emptyImageUrl` | `'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='` |
| `DomUtil.addClass(el, name)`, `removeClass`, `hasClass` | `el.classList.add(name)`, `remove`, `contains` |
| `DomUtil.setClass(el, name)`, `getClass(el)` | `el.setAttribute('class', name)`, `el.getAttribute('class')` |
| `DomUtil.remove(el)` | `el.remove()` |
| `DomUtil.empty(el)` | `el.replaceChildren()` |
| `DomUtil.setOpacity(el, value)` | `el.style.opacity = value` |
| `DomUtil.getStyle(el, prop)` | `getComputedStyle(el)[prop]` |
| `DomUtil.TRANSFORM`, `TRANSITION`, `TRANSITION_END` | `'transform'`, `'transition'`, `'transitionend'` |
| `DomUtil.disableTextSelection()`, `enableTextSelection()` | `user-select: none` in CSS |
| `DomEvent.addListener`, `removeListener` | `DomEvent.on`, `DomEvent.off` |
| `DomEvent.getPropagationPath(e)` | `e.composedPath()` |
| `layer.addEventListener`, `removeEventListener`, `addOneTimeEventListener`, `fireEvent`, `hasEventListeners` | `layer.on`, `off`, `once`, `fire`, `listens` |
| `layerGroup.invoke('method', arg)` | `layerGroup.eachLayer(l => l.method(arg))` |
The `Browser` namespace is removed too. Detect what you need directly:
| 1.x | 2.0 |
|---|---|
| `Browser.mobile` | `matchMedia('(pointer: coarse)').matches` |
| `Browser.retina` | `devicePixelRatio > 1` |
| `Browser.touch`, `Browser.pointer` | always `true` |
| `Browser.ie`, `webkit`, `android`, `safari` and other engine flags | feature detection, or remove the branch |
The `leaflet-safari` CSS class and the `L_NO_TOUCH` and `L_DISABLE_3D` globals are gone as well.
### 9. Smaller changes
Map and layer options:
- `preferCanvas: true` is removed and logs an error. Use `renderer: new Canvas()` instead.
- A circle no longer takes the radius as the second argument, and passing it there throws. `L.circle(latlng, 200)`
becomes `new Circle(latlng, {radius: 200})`.
- **Silent:** `aria-keyshortcuts` is no longer set on the map container, because it only matched US keyboards. Set
`ariaKeyShortcuts: true` to bring it back, or pass a string for other layouts.
Behavior:
- **Silent:** the `leaflet-dragging` class is set on the map container instead of `<body>`. CSS like
`body.leaflet-dragging .sidebar` no longer matches. Use the `dragstart` and `dragend` events, or
`map.dragging.moving()`.
- **Silent:** text inside the map is no longer selectable, except in popups and controls. If you put text in custom
panes or in controls placed elsewhere on the page, set `user-select: text` on it.
- **Silent:** Enter and Space fire `click` on a focused marker, so `click` listeners also run from the keyboard.
- **Silent:** the GeoJSON `filter` and `onEachFeature` callbacks get a Feature when the input is a bare geometry
(the same object as `layer.feature`), and features with a `null` geometry are skipped.
- **Silent:** `latLngToLayerPoint` and `latLngToContainerPoint` return fractional pixel values. Round them if you
need integers.
- **Silent:** `setView(center, zoom, true)` no longer forces a view reset. Use `setView(center, zoom, {reset: true})`.
- **Silent:** `closeTooltip()` returns the layer instead of the tooltip, and `isTooltipOpen()` returns `false`
instead of `undefined` when no tooltip is bound.
- **Silent:** popups and tooltips are no longer `instanceof Layer`. They still work with `addTo()`, `openOn()` and
`map.addLayer()`.
- **Silent:** tiles no longer have `filter: inherit` in the CSS. Page-wide rules like `img {filter: ...}` now apply
to tiles, and a filter on a tile container is no longer applied twice.
- **Silent:** the map container has `isolation: isolate`, so `z-index` inside the map only orders things within the
map. Dropdowns and dialogs on the page no longer end up under map panes and controls, but a pane or control can't
be raised above page elements either. To put the whole map on top, set `z-index` on the map container.
- **Silent:** the layers control list is a `<fieldset>` instead of a `<section>`. Update CSS that targets it by tag.
Defaults that changed:
| Option | 1.x | 2.0 |
|---|---|---|
| `smoothFactor` (polylines and polygons) | `1` | `0.5` |
| `inertiaMaxSpeed` | `Infinity` | `1400` |
| `referrerPolicy` (tile layers) | `false` | `'strict-origin-when-cross-origin'` |
| `tapHold` | on in iOS Safari only | on |
### 10. TypeScript
Leaflet now ships its own types, so uninstall `@types/leaflet`. TypeScript uses the package's types once they're
there, and they differ from `@types/leaflet` in a few ways:
- Type names are flat like the classes: `Control.LayersOptions` → `LayersControlOptions`, `TileLayer.WMSOptions` →
`WMSTileLayerOptions`.
- `LeafletMouseEvent` → `LeafletPointerEvent`. `LeafletEventHandlerFnMap` is gone: each class has its own event
map, and `on()` infers the event type from the event name.
- Generic parameters are options and events, not GeoJSON properties. Instead of `GeoJSON<MyProps>`, type the
callback parameter: `onEachFeature(feature: Feature<Point, MyProps>, layer)`. Groups take the member type first:
`LayerGroup<Marker>`.
- Events that bubble up to a group still have the 1.x `layer` property at runtime, but it isn't typed. Use
`e.propagatedFrom` instead.
- There's no global `L` namespace for script-tag code checked with `// @ts-check`.
- Types are stricter in places. For example, `getPopup()` can return `undefined`, and `getPixelWorldBounds()` can
return `null`.
Typings for plugins (`@types/leaflet.*`) were written against `@types/leaflet`, so they may not compile until they're
updated.
## Part 2: Plugins and extensions
This part covers code that subclasses Leaflet classes, adds hooks or uses its internals. Everything in Part 1 applies
too, especially [sections 7](#7-write-custom-classes-as-es-classes) and
[8](#8-use-the-platform-instead-of-removed-utilities).
### 11. Support 1.x and 2.0 from one release
ES classes can extend the 1.x classes too, so a plugin can support both versions:
```js
import * as L from 'leaflet';
class MyLayer extends L.Layer {
constructor(options) {
super();
L.Util.setOptions(this, options);
}
}
// 1.x keeps defaults on the prototype, and without a copy of its own,
// mergeOptions would change the defaults of every L.Layer
if (L.version.startsWith('1.')) {
MyLayer.prototype.options = Object.create(L.Layer.prototype.options);
}
MyLayer.mergeOptions({opacity: 0.5});
```
1. Use `class X extends L.Layer` with a `constructor()`, not `extend` or `initialize`. `Layer` doesn't apply options
passed to `super()` in either version, so call `L.Util.setOptions(this, options)` yourself, as 1.x `initialize`
methods did. Classes like `Control` and `Marker` take options in `super()` in both versions.
2. Set defaults with `X.mergeOptions({...})` after the class, after giving it its own `prototype.options` on 1.x as
above. `static defaultOptions` is the 2.0 way, but 1.x doesn't read it.
3. Reach renamed classes through the namespace, `import * as L from 'leaflet'`, and fall back:
`L.ZoomControl ?? L.Control.Zoom`. A static import of a flat name fails on 1.x.
4. Use `new` instead of factories. It works on both versions.
5. Pick event names by version (`L.version.startsWith('1.')`) instead of listening to both `mouse*` and `pointer*`.
Declare both versions in `peerDependencies`: `"leaflet": "^1.9.0 || ^2.0.0-beta.1"`. A plain `^2.0.0` doesn't match
the betas, so npm refuses to install the plugin next to one.
On 1.x, `addInitHook` on an ES class adds the hook to its parent, so it runs for every `L.Layer`. Only add hooks to
your own classes on 2.0.
`include()`, `mergeOptions()` and `addInitHook()` keep their 1.x signatures. `setDefaultOptions()`, added in the 2.0
alphas, is a deprecated alias of `mergeOptions()`.
### 12. Init hooks run after the whole constructor
Hooks added with `addInitHook` run once per instance, after the constructors of Leaflet's own classes have
finished. A hook on `Layer` sees a fully built `Polygon`, as in 1.x.
For a class of your own that extends a Leaflet class, hooks run before the body of your constructor. If other
code adds hooks to your class and they need its fields, mark the class with `static initHooks = true` and end its
constructor with `this.callInitHooks()`.
### 13. Removed internals
- `Class.__super__` is gone; use `super` or `Object.getPrototypeOf()`.
- `L.Mixin.Events` is gone; extend `Evented`.
- `LineUtil._flat` and `Polyline._flat` → `LineUtil.isFlat`.
- `Draggable` takes `(element, dragHandle, options)`. The third argument used to be `preventOutline`, and passing a
boolean there throws. `DomUtil.preventOutline` and `restoreOutline` are gone; focus outlines use
`:focus-visible`.
- `PosAnimation` is no longer exported.
- `DomEvent.PointerEvents`, `getWheelPxFactor` and `isExternalTarget` are gone. Pointer tracking is internal, and
`Draggable` works outside a map without setup.
- `DomEvent` functions return nothing, so they can't be chained.
- `DomUtil.setPosition` stores positions in a `WeakMap` instead of the `_leaflet_pos` property. Read them with
`DomUtil.getPosition`.
- The `_leaflet_events`, `_leaflet_disable_click`, `_leaflet_disable_events`, `_stopped` and
`_leaflet_focus_handler` properties are no longer set on elements and events. Use
`DomEvent.disableClickPropagation` and `DomEvent.stopPropagation`.
- `KeyboardHandler.keyCodes` → `KeyboardHandler.keys`, which holds `KeyboardEvent.key` values instead of key codes.
- `LayersControl.onRemove` takes `map`, like `onAdd`, and `collapse()` takes no event.
- `openPopup()` without a `latlng` throws on a layer that has `_latlng` but no `getLatLng()`, `getCenter()` or
`getBounds()`.
### 14. Types for plugins
Augment the `leaflet` module to add members to Leaflet classes:
```ts
declare module 'leaflet' {
interface Map {
editTools?: EditTools;
}
}
```
Augment `Map`, not `LeafletMap`. Augmenting both loses one of them. A layer whose `getEvents()` adds its own map
events should add them to `MapEventMap`.
Subclasses pass their options and events as generic parameters: `class MyGroup extends FeatureGroup<Layer, MyOptions>`.