Initializing MapboxGL.js within a single page app

Hi,

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?

Thanks

Michael

Use a custom element

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

zoom : Int -> Html.Attribute msg
zoom howMuch =
    Html.Attributes.property "zoom" (Json.Encode.int howMuch)


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

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

https://ellie-app.com/xJCBDRbbcqa1


Browser support for this ellie: https://caniuse.com/#feat=custom-elementsv1

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.

3 Likes

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) =
    let
        cs =
            List.map (\( E b, E a ) -> ( b, a )) cases
    in
        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.interpolate
                Expr.linear
                (Expr.get_ "mag")
                [ ( Expr.number 0, Expr.number 0 )
                , ( Expr.number 6, Expr.number 1 )
                ]
            )
        |> Heatmap.heatmapIntensity
            (Expr.interpolate
                Expr.linear
                Expr.zoom
                [ ( Expr.number 0, Expr.number 0 )
                , ( Expr.number 9, Expr.number 3 )
                ]
            )
        |> Heatmap.heatmapColor
            (Expr.interpolate
                Expr.linear
                Expr.heatmapDensity
                [ ( 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.interpolate
                Expr.linear
                Expr.zoom
                [ ( Expr.number 0, Expr.number 2 )
                , ( Expr.number 9, Expr.number 20 )
                ]
            )
        |> Heatmap.heatmapOpacity
            (Expr.interpolate
                Expr.linear
                Expr.zoom
                [ ( 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.