Best practises when you have a dominant, context driven view, for example a map


#1

I’m in the process of designing an application which will be fairly centered around a map. What’s plotted on the map, which tools are available and what various map events/actions result in is supposed to be context driven by parallel navigation in other views. You may for example view an index of all trees in an area, switch to editing a single tree, or switching all-together to go on and watch flowers instead.

Imagine something like this: http://i.stack.imgur.com/ttwBx.png

My first question is, does it make sense to make the map (which we can assume will always be visible) be a parent view/controller where navigation in the lefthand panels are nested? I.e.

MapView -> ParentView -> SubView 1 -> Subview 2 -> …

Or would it make more sense to have a setup like the following (where controllers can communicate and setup their relationship via the application, or another central service):

     Application
      /       \
     /         \
   Map     [Context Views]

This can in essence be generalised to “What is the best practise when you have one dominant context driven view and several others that you navigate freely in?”

What I would like to achieve is a fairly basic base map, where I somehow, based on context, setup interactions, components and two-way bindings with the panel views (so that if I add a marker on the map is show in the panel and vice versa).

I did have a look at the Ember Leaflet project on github and it gave me some ideas for data binding etc. however it seemed the design was influenced on the fact that it’s meant to be used as a library rather than a straight forward implementation - please correct me if I’m wrong and this is the way to go. Aside from data binding it also offers few hints as how to accomplish the context driven view/controller setup.

Any feedback is welcome!


#2

I’m working on a map-centric app right now that uses Google Maps. We’re taking the “map is a parent” approach because we only want to render the map once and that also means we can rely heavily on event bubbling to take the appropriate actions based on the active child route.

Let’s say you have:

# router.js
App.Router.map ->

  @resource "map", ->
      @route "one"
      @route "two"
      @route "three"
<!-- map.hbs -->
<div class="map">
  {{google-map mapInserted="handleMapInserted" mapIdle="handleMapIdle" ...}}
</div>

<div class="child-container">
  {{outlet}}
</div>

In the above scenario, if you are in the “two” route and the map component fired the “mapInserted” event, the “handleMapInserted” event would bubble from MapController -> TwoRoute -> MapRoute. This has proven sufficient for us, especially with the new query params code. We’re able to define route specific query params on the child controllers, then bundle them up with the general map query params (lat, lng, zoom, etc.) and pass them on to the child routes to load up new data and then on to the parent route to do any setup/cleanup.

The outlet in particular allows you to insert whatever panels you want over/next to the map based on the active child view and loaded data. We’ve also added a {{yield}} statement inside the Google Map component so we can do “global” panels and a loading spinner.


#3

Thanks a lot for sharing your experiences! Your approach pretty much summarises what I had in mind trying out first - it simply strikes me as the most straight forward approach.

Do you happen to carry out plotting of markers/features as well? Any insights as to how you’ve accomplished data binding between what’s shown on the map and the models backing the “one”, “two” and “three” controllers? Or does your controllers get direct access to the map, i.e. every controller is responsible for managing any layers on the map directly and then clean up everything on deactivation (layers, listeners, observers etc.)?

Thanks again for your input.


#4

It struck me as the most straightforward as well – with great the side benefit of only loading the map one time when you enter the parent route.

Yes, our Google Map component expects arrays of objects to turn into markers/polygons. Something like:

{{google-map markers=markerObjects mapInserted="handleMapInserted" ...}}

Since this is in the map.hbs file, it means we have to update the mapController’s data from the child route:

App.OneRoute = Ember.Route.extend({
  ...
  ajaxSuccess: (data) ->
    ...
    markerObjects = data.markers
    @controllerFor("map").set("markerObjects", markerObjects)
    #...
  ...
})

Removing existing markers and inserting new ones is handled by an observer within the component. Not sure if this is the most elegant way to do this, but it’s working well so far for us and is performant. I’m in the process of shoring up the code re: deactivation, so I can’t speak to that as much yet.


#5

Again, thanks for sharing!

This may drift away from topic… but as I’m coming from backbone I’m shooting anyway… what’s the benefit of isolating the map in a component, compared to just creating a custom View? I know that components provide isolated contexts and therefore well defined interfaces - but isn’t there a risk in this scenario that this will lead to a bloated component interface as your number of routes increases? Especially if they interact with the map in very different ways.


#6

If your child views have wildly different behavior, a view may be more useful, although I wonder if the “map as parent” pattern would work best in that instance anyway. Our maps are pretty similar, so we only need to expose a few actions.


#7

Thanks. I’ll begin by mimicking your approach and see where it leads me!


#8

Cool, check back in when you’ve got a working version and I’ll see if I can anonymize my stuff so we can compare code.


#9

I have also built a couple Ember applications that made heavy use of mapping. The approach I took was to have a MapController and a MapView with the map initialized in didInsertElement. The MapController was an ArrayController with the Ember.Evented mixin. Since I only had one type of markers I set the marker array as the content/model and I used arrayContentDidChange and arrayContentWillChange to trigger an addLayers and removeLayers event on the controller. My MapView observed those events and created or removed markers using the map library api.


#10

Thanks for all the detail, @matthooks and @aveskogh. These are exactly the kind of issues I’ve been trying to figure out for my own map-centric app (and I’m also coming from prior experience with Backbone/Marionette patterns)

Would any of you be willing to share some of your code in an anonymized fashion? Maybe a directory structure and a few key lines from each Route, Controller, View, etc?


#11

I might open source my google map component sometime in the future. My route/controller structure is detailed above.


#12

@aveskogh Hey, I’m attempting a similar approach to yours and am having trouble getting my templates not to overlap. Do you have an example that you wouldnt mind showing of your map as ParentView implementation?