Create a plugin component

Overview

The purpose VueMapbox is to wrap up Mapbox Gl JS library. Any other functions are out of scope. However, there are some plugins for Mapbox Gl JS, that provides additional capabilities, and it where plugin components come into play.

Plugin components are essentially just Vue components that utilize mapbox and map objects provided by basic MglMap.

VueMapbox internally use dependency injection mechanism of Vue (provide/inject in Vue docs). When MglMap created, it waits for map loads and initializes then renders it's child components, and provide them mapbox (Mapbox GL JS library), map (initialized instance of the Map) and actions (promisified Mapbox Map methods). Inject these objects in your component, and you can add to map features you need.

The basic idea is to keep the declarative style of Vue, so it's good to add for example additional controls or layer types to map as a component. It's a right place to wrap Mapbox Gl JS plugins, but it can be used for various purpose.

Basic example for plugin component

App template

<template>
  <MglMap :accessToken="accessToken" :mapStyle="mapStyle">
    <MyPluginComponent />
  </MglMap>
</template>

<script>
import Mapbox from "mapbox-gl";
import { MglMap } from "vue-mapbox";

export default {
  components: {
    MglMap
  },
  data() {
    return {
      accessToken: ACCESS_TOKEN,
      mapStyle: MAP_STYLE
    };
  }
};
</script>

Plugin comonent

<template>
  <button @click="fly">Fly!</button>
  <div>Map center is: Lng={{ center.lng }}, Lat={{ center.lat }}</div>
</template>

<script>
import Mapbox from "mapbox-gl";
import { MglMap } from "vue-mapbox";

export default {
  name: "MyPluginComponent"
  inject: ["mapbox", "map", "actions"],

  data() {
    return {
      center: null
    };
  },

  created() {
    this.center = this.map.getCenter();
  },

  methods: {
    async fly() {
      const flyResult = await this.actions.flyTo({ center: [10, 10] });
      this.center = flyResult.center;
    }
  }
};
</script>

VueMapbox helpers experimental

Experimental

Helpers are experimenatal feature and will change in future, but we will try keep backward compatibility for a long time and provide deprecation warnings. For now they just mixins that used in VueMapbox internal implementation.

Beside providing base objects, VueMapbox give some useful helper mixins, that can be used in plugin components. You can access to them via $helpers named export:

import { $helpers } from "vue-mapbox";

const { withEvents, withSelfEvents, asControl, asLayer } = $helpers;

withEvents

Source.
Provides $_emitEvent and $_emitMapEvent methods to emit events in VueMapbox style.

withSelfEvents

Source
Provides $_bindSelfEvents, $_unbindSelfEvents and $_emitSelfEvent. They can be used to bind events to Mapbox GL JS objects that emit self events instead of Map object like controls, markers and popups.

asControl

Source.
Provides backbone for Map controls (like )

asLayer

Source.
Provides backbone for Map layer.
See also layers API doc

Creating component for Mapbox GL JS plugin

Example below can give you an idea how to create component for Mapbox GL JS plugin.

VueMaboxGeocoder — wrapper for mapbox-gl-geocoder:

// First, there is no separate HTML to render, so we don't need template and SFC, so it's just JS file

import MapboxGeocoder from "@mapbox/mapbox-gl-geocoder";
import { $helpers } from "vue-mapbox"; // Get $helpers from VueMapbox

// Define list of mapbox-gl-geocoder events
const geocoderEvents = {
  clear: "clear",
  loading: "loading",
  results: "results",
  result: "result",
  error: "error"
};

export default {
  name: "GeocoderControl",
  mixins: [$helpers.asControl], // MapboxGeocoder is a control, so we use mixin

  inject: ["mapbox", "map"], // Here we inject objects provided by MglMap

  props: {
    // MapboxGeocoder requires access token
    accessToken: {
      type: String,
      required: true
    },
    input: {
      type: String,
      default: null
    },
    proximity: {
      type: Object,
      default: null
    }
    // ...here goes other props...
  },

  data() {
    return {
      initial: true
    };
  },

  // Here we watch for props and and apply changes to MapboxGeocoder if needed
  watch: {
    input: {
      handler(next, prev) {
        if (this.control && next !== prev) {
          this.control.setInput(next);
        }
      },
      immediate: true
    },
    proximity(next, prev) {
      if (this.control && next !== prev) {
        this.control.setProximity(next);
      }
    }
  },

  created() {
    this.control = null; // Here we will store MapboxGeocoder instance. We don't want Vue reactivity system mess with it, so we store it non-reactive
    if (this.accessToken && !this.mapbox.accessToken) {
      this.mapbox.accessToken = this.accessToken;
    }
    this.control = new MapboxGeocoder(this.$props); // Creating MapboxGeocoder instance and pass props as options to it
    this.control.on("results", this.$_updateInput); // We need to update synchronized prop "input" when user enters some query to search field

    // Now we can add control to the map
    this.$_deferredMount();
  },

  beforeDestroy() {
    this.control.off("results", this.$_updateInput);
    // Also, control will be removed from map in beforeDestroy() lifecycle hook in `asControl` mixin
  },

  methods: {
    $_deferredMount() {
      // Because this component placed in MglMap sub-tree, map already initialized and injected above
      this.map.addControl(this.control);
      if (this.input) {
        // Set input in MapboxGeocoder if there is default data
        this.control.setInput(this.input);
      }
      // Emit added event. `$_emitEvent` method is came from `asControl` mixin
      this.$_emitEvent("added", { geocoder: this.control });
      this.$_bindSelfEvents(Object.keys(geocoderEvents)); // Bin events to emit them as Vue events
      this.initial = false; // Initialization done
    },

    $_bindSelfEvents(events) {
      // $_bindSelfEvents is provided by `asControl` mixin. but we need to replace it because MapboxGeocoder do not follow Mapbox Gl JS events schema and we need custom processing for them
      const vm = this;
      // Here we use this.$listeners to subscribe only on events that user listens on component
      Object.keys(this.$listeners).forEach(eventName => {
        if (events.includes(eventName)) {
          this.control.on(eventName, vm.$_emitControlEvent.bind(vm, eventName));
        }
      });
    },

    // Process event to line up with VueMapbox events format
    $_emitControlEvent(eventName, eventData) {
      return this.$_emitSelfEvent({ type: eventName }, eventData);
    },

    $_updateInput(results) {
      if (!this.initial) {
        const input = results.query ? results.query.join("") : "";
        this.$emit("update:input", input); // update synchormized prop "input"
      }
    }

    //...here goes other public methods
  }
};