Introduction
This post describes the integration between Google Maps and KnockoutJS. Concretely, you can learn how to make the maps marker part of the View and automatically change its position any time when the ViewModel
behind changes. The ViewModel
obviously has to contain the latitude and longitude positions of the point that you wish to visualize on the map.
Previously, I have worked a bit with Silverlight/WPF which in general leaves one mark on a person: the preference for declarative definition of the UI leveraging the rich possibilities of data binding provided by the previously mentioned platforms. At this moment, I have a small free-time project where I am visualizing a collection of points on a map. This post describes how to make the marker automatically change its position after the model values behind changes. Just like in this picture below, where the position changes when user changes the values of latitude and longitude in the input boxes.
Since I like Model-View-ViewModel pattern, I was looking for a framework to use this pattern in JS, obviously KnockoutJS saved me. The application that I am working on has to visualize several markers on Google Maps. As far as I know, there is no way to define the markers declaratively. You have to use JS:
marker = new google.maps.Marker({
map:map,
draggable:true,
animation: google.maps.Animation.DROP,
position: parliament
});
google.maps.event.addListener(marker, 'click', toggleBounce);
So let's say you have a ViewModel
which holds a collection of interesting points, that will be visualized on the map. You have to iterate over this collection to show all of them on the map. One possible way around would be to use the subscribe
method of KO. You could subscribe for example to the latitude of the point (assuming that the latitude would be an observable) and on any change perform the JS code. There is a better way.
Defining Custom Binding for Google Maps
The way to go here is to define a custom binding, which will take care of the update of the point on the map, any time, that one of the observable properties (in basic scenario: latitude and longitude) would change ko.bindingHandlers.map = {
.
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var position = new google.maps.LatLng(allBindingsAccessor().latitude(),
allBindingsAccessor().longitude());
var marker = new google.maps.Marker({
map: allBindingsAccessor().map,
position: position,
icon: 'Icons/star.png',
title: name
});
viewModel._mapMarker = marker;
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var latlng = new google.maps.LatLng(allBindingsAccessor().latitude(),
allBindingsAccessor().longitude());
viewModel._mapMarker.setPosition(latlng);
}
};
<div data-bind="latitude: viewModel.Lat, longitude:viewModel.Lng, map:map" ></div>
So let's describe what is going on here. We have defined a map
binding. This binding is used on a div
element. Actually, the type of the element is not important. There are also latitude
and longitude
bindings, which are not defined. That is because the map
binding takes care of everything. The binding has two functions: init
and update
, first one called only once, the second one called every time the observable value changes.
The allBindingsAccessor
parameter contains a collection of all sibling bindings passed to the element in the data-bind
attribute. The valueAccessor
holds just the concrete binding (in this case the map value, because we are in definition of the map binding). So from the allBindingsAccessor
, we can easily obtain the values that we need:
allBindingsAccessor().latitude()
allBindingsAccessor().longitude()
allBindingsAccessor().map()
Notice that the map
is passed to the binding in parameter (that is concretely the google.maps.Map
object, not the DOM element. Once we have these values, there is nothing easier than to add the marker to the map.
And there is one important thing to do at the end – save the marker, somewhere so we can update its position later. Here again, KO comes to the rescue because we can use the viewModel
parameter passed to the binding and we can attach the marker to the ViewModel
. Here, I suppose that there is no existing variable with name _mapMarker
in the ViewModel
and JS can happily add the variable to the ViewModel
.
viewModel._mapMarker = marker;
The update
method has an easy job, because the marker has been stored, and we only need to update its position.
viewModel._mapMarker.setPosition(latlng);
Almost Full Example
Just check it here on JsFiddle.
Possible Improvements
One thing that I do not like about this, is the fact, that you have to pass the map as an argument to the binding and the div
element has to be outside of the map. Coming from Silverlight/WPF, you would like to do something like this:
<div id="map_canvas">
<div data-bind="latitude: vm.Lat, longitude:vm.Lng">Whatever I want to show on the map marker</div>
</div>
That is actually the beauty of declarative UI definition. You can save a lot of code only by composing the elements in the correct order. However this is not possible – at least, I was not able to get it to work. I was close however:
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var map = element.parentNode;
var googleMap = new google.maps.Map(.map.);
var contentToAddToMarker = element;
}
Again thanks to KO, here the element variable represents the DOM element to which the binding is attached. If the div
element is inside the map, then we can get the parent element (which is the div
for the map) and we are able to create a new map on this element. The problem which I had is that once the new map was created, the div
elements nested inside the map disappeared. Even if that would work, some mechanism would have to be introduced, in order to create the map only the first time (in case there are more markers to show on the map) and store it somewhere (probably as global JS variable).
On the other hand, thanks to the element, you can get all the div
which should be for example given as the description to the marker.
Summary: KnouckoutJS is great. It lets me get rid of the bordelic JS code. I am working on a small application and once it’s done, I will show some more useful functions. For now, I have been really surprised by the flexibility of the framework. Every time I have some “nice to have” feature which I am adding, Knockout has the solution for me.