Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mapbox integration #89

Closed
whitetigle opened this issue Apr 20, 2017 · 8 comments
Closed

Mapbox integration #89

whitetigle opened this issue Apr 20, 2017 · 8 comments

Comments

@whitetigle
Copy link

Hi,
I'm trying to make my mind around the integration of a mapbox component into the elmish architecture.

So currently, a mapbox component is rendered to a known HtmlElement. Following our discussion of yesterday, I've tried to initialize my map when the View is ready by using Ref. Therefore the mapbox map is correctly appended.

let root =
  div
    [  
      Id "mapid" 
      Ref( fun x ->
        printfn ("init map")
        Mapbox.AccessToken <- "my.access.token"
        Mapbox.Map("mapid","mapbox.streets") |> ignore
      )
    ] []

Now my problem arises on unmount since I would like to destroy the mapbox component (= calling map.remove() , not removing the actual Html element) when I render a new view.

The leaflet component exists in the DOM and has it's own lifecycle independant on the VDom.

So my question is: where should I store a reference to my map component so that I can call map.remove()? In a more generic fashion, what would be the best architecture for this kind of problem?

Thanks!

@et1975
Copy link
Member

et1975 commented Apr 20, 2017

The solution we have for D3 is really hacky and not something I'd recommend to anyone.
If you think lifecycle events can solve your problem you can certainly create your own React class and use that from elmish. Maybe look at how JS folks solve this problem with Redux?
Maybe @Pauan or @alfonsogarciacaro have an idea?

@whitetigle
Copy link
Author

Thanks @et1975!
I was just reading how I could create my own React class from Fable... 😄
I am going to see what I can do.
Meanwhile if anyone comes with any other an idea I would be more than happy to know about it!

In all cases thanks for your help!

@alfonsogarciacaro
Copy link
Contributor

alfonsogarciacaro commented Apr 20, 2017

@whitetigle I'm not sure, but I think the Ref callback is also called when the element is unmounted, but with a null value for the parameter. Can you try that?

      Ref( fun x ->
        if x <> null then
          printfn ("init map")
          Mapbox.AccessToken <- "my.access.token"
          Mapbox.Map("mapid","mapbox.streets") |> ignore
        else
          printfn "remove map"
          // Remove map
      )

Reference: https://facebook.github.io/react/docs/refs-and-the-dom.html#adding-a-ref-to-a-dom-element

React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts.

@whitetigle
Copy link
Author

Thanks @alfonsogarciacaro if x <> null then works ! 👍

However, I still miss a proper strategy to store the current state, aka my map object.

let root model dispatch =
  div
    [  
      Id "mapid" 
      Ref( fun x ->
        if x <> null then
          printfn "init map"
          Mapbox.AccessToken <- "my.access.token"
          let map = Mapbox.Map("mapid","mapbox.streets") 

          // update model
          MountMap map |> dispatch
        else
          printfn "remove map"
          match model with
          | Some (ma : MapboxMap) ->
            ma.off()
            ma.remove()
          | None -> 
            printfn ("no map") )
    ] []

If I use dispatch to update my model, it updates the view with x argument being not null another time (=refresh). So I think it's not the proper way to do this. Any idea?

@whitetigle
Copy link
Author

Ah no excuse me! MyMsg |> dispatch sets x to null = unmount.
So If I mount and then dispatch, it refreshes the view with x == null

@whitetigle
Copy link
Author

Ok here is the flow:

> User clicks on page link to display map view (#map)

> Map.View: x<>null (map component is created in Ref )
 
> dispatch (send Message with map object)

> App.State.update

> Map.State.update (model is updated with map set to Some(map))

> Map.View: x == null (model is retrieved but has not changed yet, map == None) 

> Map.View: x <> null (model is retrieved and has been updated, map == Some(map))

which gives us:

let root model dispatch =
  div
    [  
      Id "mapid" 
      Ref( fun x ->
        if x <> null then
          printfn "mount"
          match model with
          | None ->
            Mapbox.AccessToken <- "my.access.token"
            let map = Mapbox.Map("mapid","mapbox.streets")     
            MountMap map |> dispatch            
          | _ -> 
            printfn "Map object already initialied. Nothing to do"
        else
          printfn "unmount"
          match model with
          | Some (ma : MapboxMap) ->
            ma.remove()
          | None -> 
            printfn ("no map") ][]

And so it works on mount. 👍

However on unmount, it fails because, if I understand well, the container has already been destroyed when Ref is called, so before I even make my call to map.remove() which leads to this error:

leaflet-src.js:2935 Uncaught Error: Map container is being reused by another instance
    at e.remove (leaflet-src.js:2935)
    at ref (View.fs:32)
    at detachRef (ReactRef.js:29)
    at Object.ReactRef.detachRefs (ReactRef.js:84)
    at Object.receiveComponent (ReactReconciler.js:122)
    at Object.updateChildren (ReactChildReconciler.js:109)
    at ReactDOMComponent._reconcilerUpdateChildren (ReactMultiChild.js:208)
    at ReactDOMComponent._updateChildren (ReactMultiChild.js:312)
    at ReactDOMComponent.updateChildren (ReactMultiChild.js:299)
    at ReactDOMComponent._updateDOMChildren (ReactDOMComponent.js:936)

At this point, I thinks it's time for me to drink a 🍺
Thanks for your patience !

@et1975
Copy link
Member

et1975 commented Apr 20, 2017

Since you have the reference to the map in your model, you could call remove() anytime, such as in update function - when you process a message that would lead to the disposal of the container element.

@whitetigle
Copy link
Author

I put my call to remove the map when on urlUpdate and it works!

code used in urlUpdate

let urlUpdate (result: Option<Page>) model =
  
  let model =
    match model.map with 
      | Some map -> 
        map.off()
        map.remove()
        { model with map = None }
      | None -> model

   ...

And just for sharing here's my current Mapbox integration

Thanks again for your help guys! 👍

@et1975 et1975 closed this as completed Apr 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants