Reasoning behind VueMapbox

Photo by Ian on Unsplash

Good news

Mapbox Gl JS library is great.
Hard to find a better tool if you need to create a complex web application for working with geographical maps.
It's pretty looking.
It's fast.
It's open source.
It's unique in some sense: AFAIK it's only one live, updated and open vector map implementation for the web browser. There was also MapZen, but it's not developed anymore, so there is not much choice actually.

Vue is also good. This is a UI framework with a good balance between simplicity and power, "magic" and fine-tuning capabilities. Its reactive system is beneficial in UI development and almost never stands in your way. Almost… However, it's a topic for a separate post 😉.

News that not so good

By themselves both libraries are good, but using them together is tricky.
If your interface logic build in Vue paradigm and you want to continue using its capabilities, then you face a question: how Mapbox GL JS fits in this paradigm?

The answer is not very comforting: it's not.

Now we need a little excursus into Vue and Mapbox.

Vue declarative style

Vue uses declarative programming style. You create a component, pass data in it's props and add it to your component tree.
If we want to implement map in Vue paradigm, we need to make the map as a component. Something like this would be good:

<template>
    <MyMap
        :style="mapStyle"
        :center="mapCenterCoordinates"
        …some other props…
    />
</template>

Usually, we want to add to map some objects, e.g., markers to mark some places. Again, it would be natural to place them as components inside the map component, similar to HTML-elements.
If we have a list of places, we want to create a list of markers using v-for directive and show and hide them depending on some conditions using v-show or v-if directives:

<template>
    <MyMap
        :style="mapStyle"
        :center="mapCenterCoordinates"
        …some other props…
    >
        <MapMarker
            v-for="marker in markers"
            v-if="markerCondition"
            :coordinates="marker.coordinates"
        >
            <span>I am marker!</span>
        </MapMarker>
    </MyMap>
</template>

Components must react to data changes. Vue gives us an elegant solution for two-way data binding through synced props. If some method changes map state (e.g., we trigger a pretty animated flight to someplace), it would be great to use two-way data-binding to update data.

Also, map and map elements can generate events, and it would be natural to listen to them on map components.
Something like this (take note on .sync modificator on center prop):

<template>
    <MyMap
        :style="mapStyle"
        :center.sync="mapCenterCoordinates"
        @movend="moveHandler"
        …some other props…
    >
        <MapMarker
            v-for="marker in markers"
            v-if="markerCondition"
            @click="markerClickHandler(marker)"
            :coordinates="marker.coordinates"
        >
            <span>I am marker!</span>
        </MapMarker>
    </MyMap>
</template>

Feels like Vue.

Mapbox GL JS imperative style

However, Mapbox GL JS is a very different beast and designed to work in another programming style.
If you want to create a map, you need to call its constructor and pass configuration object with necessary properties, including id of HTML-element for the map to draw. Constructor returns Map object, and we need to work with it.
Example from Mapbox GL JS documentation:

import mapboxgl from 'mapbox-gl'

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v9'
})

If we want, for example, change map center, we should call the map method:

import mapboxgl from 'mapbox-gl'

const map = new mapboxgl.Map({ /* …map config… */})

map.setCenter([50, 50])

And so on. All map manipulations can be done through methods of Map object.
Same thing with fetching data from the map. If we want to get coordinates of the center of the map we should use the map method:

import mapboxgl from 'mapbox-gl'

const map = new mapboxgl.Map({ /* …map config… */})
const center = map.getCenter()

If we want to listen map events, we need to add event listeners to map object:

const map = new mapboxgl.Map({ /* …map config… */})

const moveHandler = event => { /* …some actions… */ }

map.on('moveend', moveHandler)

If map elements, for example, layers, generate events, we need again to listen to them on map object passing layer id as an argument to .on method:

const map = new mapboxgl.Map({})

const layerId = 'myLayer'

const clickHandler = event => { /* …some actions… */ }

map.on('click', myLayer, clickHandler)

It's all doesn't look like our example above in declarative style...

Layers in Mapbox GL JS

To add a layer to map, we need to use .addLayer map method and pass data source:

Example from Mapbox GL JS documentation:

map.on('load', function () {
    map.addLayer({
        'id': 'maine',
        'type': 'fill',
        'source': {
            'type': 'geojson',
            'data': {/* …GeoJSON data */}
                 }
        'layout': {},
        'paint': {
            'fill-color': '#088',
            'fill-opacity': 0.8
        }
    });
});

All work with layers done with map methods with layer id passed as an argument.
It makes work in OOP and declarative style not very convenient. In particular, it would be great to use Vue lifecycle hooks for adding and removing layers, but it's not possible straight away. It's necessary to implement a declarative wrapper for this purpose and take care of translating all changes to the map.

Asynchronous methods and events

Some time ago I used to work on a project, where I need to show a series of map animations in order, and stop or change them depending on user actions.
I ran into a problem: it's a little bit tricky to catch the moment, when animations, triggered by asynchronous map methods like .flyTo() completes.
JavaScript has Promise for this purpose, but, unfortunately, Mapbox async methods not return Promises and judging by this feature request it's would not be done soon. When some action completes, Mapbox GL JS generates events like moveend, rotateend etc. We can subscribe to these events, but how determinate what action caused what event if several animations going on simultaneously?
There is a solution to this issue, but it does not look very convenient,
Special object eventData can be passed as an argument to the asynchronous method.
When this method completes its work, Mapbox GL JS generates an event and return this eventData in event payload. In this way, can generate a unique id for every action, pass this id in eventData and later it to determinate action:

const actionId = 'uniqueActionId'
map.on('moveend', event => {
    if (actionId === 'uniqueActionId') {
        // Do something!
    }
})
map.flyTo({ center: [50, 50] }, { actionId: 'uniqueActionId' })

Or use similar solution: pass into eventData callback and trigger it when binded event will be caught:

const callback = () => { console.log('Done!') }
map.on('moveend', event => {
    if (event.callback) {
        event.callback()
    }
})
map.flyTo({ center: [50, 50] }, { callback })

Doing all this every time I need to is tedious, so I wrote small library called map-promisified, that wraps asynchronous maps methods and returns Promise. When action is complete, Promise resolves with payload containing new map parameters.

Happy end: VueMapbox

In result of fighting with a different approach in two great libraries third was born: VueMapbox. It solves the problems described above and provide nice declarative API for working with the map.

MIT licensed, about 7kb gzipped.
Source
Bug reports and feature requests