Initializing MapboxGL.js within a single page app


I have been trying to figure out a non-hacky way of initializing a MapboxGL map within the a single page application.

The MapboxGL library exposes a method Map([options]) which allows you specify the id of the node which should act as the parent and I am able to call this via a port. Clearly the node must exist at the point that call is made.

My application is structured along the lines of @rtfeldman single page app example.

The problem arises since I don’t know when to send my port messages after loading a specific page since I don’t know when the node onto which Mapbox will mount has been rendered.

My current approach involves queueing port messages when the target node does not exist and using a MutationObserver to replay then when the node is added but this seems both a long winded way to doing it and slightly fragile.

Does anyone know of other approaches for this kind of thing?



Use a custom element

customElements.define('mapbox-gl', class extends HTMLElement {
  constructor() {
    this._zoom = 0
  get zoom() {
    return this._zoom
  set zoom(value) {
    if (this._zoom === value) return
    this._zoom = value
    if (!this._map) return
  connectedCallback() {
    this._map = new Map({
      container: this,
      zoom: this._zoom
  disconnectedCallback() {

zoom : Int -> Html.Attribute msg
zoom howMuch = "zoom" ( howMuch)

mapbox : List (Html.Attribute msg) -> Html msg
mapbox attrs =
    Html.node "mapbox-gl" attrs []

You can try it here if you fill in your mapbox token in the HTML editor

Browser support for this ellie:

That’s… awesome :heart_eyes:

Is it possible to use the same, declarative, approach for sources and layers i.e. using nested custom elements? If so, do you have any references you could point me toward?

that one is a little trickier because it requires shadow dom to do correctly and browser support and polyfills for shadow dom are much shakier than custom element definitions. the way i would go first is to make types that describe layers and sources and then encode them to json and pass them as properties in the same way that the example passes zoom as an int. still declarative! but it just doesn’t look as satisfying in the dom explorer.

Cool - I’ll give it a try that way.

One thing that isn’t immediately clear is how to set up properties when there are multiple values (as is the case for both layers and sources).

Would you simply encode as a JSON object (or array) and iterate through using the API add them on the JS side?

yep! the values you can pass as properties are in the set of anything you can represent as a Json.Encode.Value, which includes arrays and objects.

Perfect. Thank you for your help Luke.

I’ve been working for a while on a project that wraps all this in a library. It is getting relatively close to being published:

I’ll post here when I have a version actually published.


That looks cool!

I threw together something that just covered my initial use case; the parts I can share are here.

It’s not as complete as what you have shared but the part I spent most time on was the Expressions.

I took the approach of having an underlying untyped AST (here) and a second type which wraps it and uses a phantom type to give a degree of type safety when constructing expressions (here).

For example, the exposed case_ function ensures all branches and the default are of the same type and each case test is of type Bool:

case_ : List ( Expression Bool, Expression a ) -> Expression a -> Expression a
case_ cases (E default) =
        cs =
   (\( E b, E a ) -> ( b, a )) cases
        E <| Case cs default

Feel free to grab it if you think it’s useful :slightly_smiling_face:

The code below is the example for a heatmap layer from the Mapbox docs.

Note that each of the heatmaps properties is also typed, e.g.

heatmapColor : Expression Color -> Heatmap -> Heatmap

heatmapOpacity : Expression Float -> Heatmap -> Heatmap


The expression is then:

import Mapbox.GL.Expression as Expr
import Mapbox.GL.Layer.Heatmap as Heatmap
import Mapbox.GL.Layer as Layer
import Mapbox.GL.Source as Source

heatmapLayer =
    Heatmap.heatmap "earthquakes-heat" (Source.reference "earthquakes")
        |> Heatmap.heatmapWeight
                (Expr.get_ "mag")
                [ ( Expr.number 0, Expr.number 0 )
                , ( Expr.number 6, Expr.number 1 )
        |> Heatmap.heatmapIntensity
                [ ( Expr.number 0, Expr.number 0 )
                , ( Expr.number 9, Expr.number 3 )
        |> Heatmap.heatmapColor
                [ ( Expr.number 0, Expr.color (Color.rgba 33 102 172 0) )
                , ( Expr.number 0.2, Expr.color (Color.rgb 103 168 207) )
                , ( Expr.number 0.4, Expr.color (Color.rgb 209 229 240) )
                , ( Expr.number 0.6, Expr.color (Color.rgb 253 219 199) )
                , ( Expr.number 0.8, Expr.color (Color.rgb 239 138 98) )
                , ( Expr.number 1.0, Expr.color (Color.rgb 178 24 43) )
        |> Heatmap.heatmapRadius
                [ ( Expr.number 0, Expr.number 2 )
                , ( Expr.number 9, Expr.number 20 )
        |> Heatmap.heatmapOpacity
                [ ( Expr.number 7, Expr.number 1 )
                , ( Expr.number 9, Expr.number 0 )
        |> Layer.heatmap

That’s very similar to what I have. Although I don’t bother with an internal type but directly encode into JSON. Under 0.19 this should be as fast as manually constructing JSON. I also use phantom types for type safety, though I also track whether the expression is a DataExpression or CameraExpression to keep stuff a bit safer that way as well:

Also this library is now ready to be published, so would appreciate any feedback on it.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.