Extending Leaflet
Leaflet has literally hundreds of plugins. These expand the capabilities of Leaflet: sometimes in a generic way, sometimes in a very use-case-specific way.
Part of the reason there are so many plugins is that Leaflet is easy to extend. This tutorial will cover the most commonly used ways of doing so.
Please note that this tutorial assumes you have a good grasp of:
- JavaScript
- DOM handling
- Object-oriented programming (understanding concepts like classes, instances, inheritance, methods and properties)
Leaflet architecture
Let’s have a look at a simplified UML Class diagram for Leaflet 1.0.0. There are more than 60 JavaScript classes, so the diagram is a bit big. Luckily we can make a zoomable image with a L.ImageOverlay
:
See this example stand-alone. |
From a technical point of view, Leaflet can be extended in different ways:
- The most common: creating a new subclass of
L.Layer
,L.Handler
orL.Control
, withL.Class.extend()
- Layers move when the map is moved/zoomed
- Handlers are invisible and interpret browser events
- Controls are fixed interface elements
- Including more functionality in an existing class with
L.Class.include()
- Adding new methods and options
- Changing some methods
- Using
addInitHook
to run extra constructor code.
- Changing parts of an existing class (replacing how a class method works) with
L.Class.include()
.
This tutorial covers some classes and methods available only in Leaflet 1.0.0. Use caution if you are developing a plugin for a previous version.
L.Class
JavaScript is a bit of a weird language. It’s not really an object-oriented language, but rather a prototype-oriented language. This has made JavaScript historically difficult to use class inheritance in the classic OOP meaning of the term.
Leaflet works around this by having L.Class
, which eases up class inheritance.
Even though modern JavaScript can use ES6 classes, Leaflet is not designed around them.
L.Class.extend()
In order to create a subclass of anything in Leaflet, use the .extend()
method. This accepts one parameter: a plain object with key-value pairs, each key being the name of a property or method, and each value being the initial value of a property, or the implementation of a method:
var MyDemoClass = L.Class.extend({
// A property with initial value = 42
myDemoProperty: 42,
// A method
myDemoMethod: function() { return this.myDemoProperty; }
});
var myDemoInstance = new MyDemoClass();
// This will output "42" to the development console
console.log( myDemoInstance.myDemoMethod() );
When naming classes, methods and properties, adhere to the following conventions:
- Function, method, property and factory names should be in
lowerCamelCase
. - Class names should be in
UpperCamelCase
. - Private properties and methods start with an underscore (
_
). This doesn’t make them private, just recommends developers not to use them directly.
L.Class.include()
If a class is already defined, existing properties/methods can be redefined, or new ones can be added by using .include()
:
MyDemoClass.include({
// Adding a new property to the class
_myPrivateProperty: 78,
// Redefining a method
myDemoMethod: function() { return this._myPrivateProperty; }
});
var mySecondDemoInstance = new MyDemoClass();
// This will output "78"
console.log( mySecondDemoInstance.myDemoMethod() );
// However, properties and methods from before still exist
// This will output "42"
console.log( mySecondDemoInstance.myDemoProperty );
L.Class.initialize()
In OOP, classes have a constructor method. In Leaflet’s L.Class
, the constructor method is always named initialize
.
If your class has some specific options
, it’s a good idea to initialize them with L.setOptions()
in the constructor. This utility function will merge the provided options with the default options of the class.
var MyBoxClass = L.Class.extend({
options: {
width: 1,
height: 1
},
initialize: function(name, options) {
this.name = name;
L.setOptions(this, options);
}
});
var instance = new MyBoxClass('Red', {width: 10});
console.log(instance.name); // Outputs "Red"
console.log(instance.options.width); // Outputs "10"
console.log(instance.options.height); // Outputs "1", the default
Leaflet handles the options
property in a special way: options available for a parent class will be inherited by a children class:.
var MyCubeClass = MyBoxClass.extend({
options: {
depth: 1
}
});
var instance = new MyCubeClass('Blue');
console.log(instance.options.width); // Outputs "1", parent class default
console.log(instance.options.height); // Outputs "1", parent class default
console.log(instance.options.depth); // Outputs "1"
It’s quite common for child classes to run the parent’s constructor, and then their own constructor. In Leaflet this is achieved using L.Class.addInitHook()
. This method can be used to “hook” initialization functions that run right after the class’ initialize()
, for example:
MyBoxClass.addInitHook(function(){
this._area = this.options.width * this.options.length;
});
That will run after initialize()
is called (which calls setOptions()
). This means that this.options
exist and is valid when the init hook runs.
addInitHook
has an alternate syntax, which uses method names and can fill method arguments in:
MyCubeClass.include({
_calculateVolume: function(arg1, arg2) {
this._volume = this.options.width * this.options.length * this.options.depth;
}
});
MyCubeClass.addInitHook('_calculateVolume', argValue1, argValue2);
Methods of the parent class
Calling a method of a parent class is achieved by reaching into the prototype of the parent class and using Function.call(…)
. This can be seen, for example, in the code for L.FeatureGroup
:
L.FeatureGroup = L.LayerGroup.extend({
addLayer: function (layer) {
…
L.LayerGroup.prototype.addLayer.call(this, layer);
},
removeLayer: function (layer) {
…
L.LayerGroup.prototype.removeLayer.call(this, layer);
},
…
});
Calling the parent’s constructor is done in a similar way, but using ParentClass.prototype.initialize.call(this, …)
instead.
Factories
Most Leaflet classes have a corresponding factory function. A factory function has the same name as the class, but in lowerCamelCase
instead of UpperCamelCase
:
function myBoxClass(name, options) {
return new MyBoxClass(name, options);
}
Naming conventions
When naming classes for Leaflet plugins, please adhere to the following naming conventions:
- Never expose global variables in your plugin.
- If you have a new class, put it directly in the
L
namespace (L.MyPlugin
). - If you inherit one of the existing classes, make it a sub-property (
L.TileLayer.Banana
).