Angular Directives and Hooking into Google Maps API

Use Angular Directives to hook into Google Maps API

Written by 42ID on October 04 2014

Directives are Angular's way of extending HTML syntax. They are markers on DOM element much like an attribute, element name or CSS class that tells Angular to attach a specific behavior to that DOM element. You have already seen some of the built-in Angular directives in the form of ng-app and ng-controller.

In this post, we are going to create a custom directive and hook it to Google Maps API. We also want to enable a way for the controller to post notification to the directive in order to update markers on the map once the map is rendered.

There's a little bit of convention used in how Angular determines what tags are directives and how it matches the tags to corresponding JavaScript code. Directives in Angular are identified by camelCase normalized name, and since HTML is case insensitive, the directives in DOM are identified by a lower case dash-limited attributes.

E.g on the DOM, directive ng-app corresponds to camelCased ngApp directive on the JavaScript side, ng-controller corresponds to ngController.

Creating Directives

We are going to create a directive called mapDirective. This will correspond to the HTML tag <map-directive>. 

    var app = angular.module('app', []);

    app.directive('mapDirective', function(){
	    return {
	    }
     });

The link function of the directive is where we will hook into Google API to create a map object.

app.directive('mapDirective', function(){
	return {
		restrict: 'E',
		template: '<div id="mapSize"></div>',
		link: function (scope, elem, attrs){
			var map;
			map = new google.maps.Map(elem[0], {
				center: new google.maps.LatLng(40.440625,-79.995886),
				zoom: 15,
				mapTypeId: google.maps.MapTypeId.ROADMAP
			});
		},
		replace: true
	}
});

A note on link functions. Testing link function within a directive becomes complicated. This is because unit testing functions that perform DOM manipulations is not straightforward. So it makes sense to separate out the logic for the link function into a section that performs DOM manipulations and a section that performs core business logic. The core business logic can then be easily unit tested.

The link function provides an optional controller function for exactly this purpose. The controller option on the directive specifies the controller that will be passed into the link function.

app.controller('mapDirectiveController', ['$scope', function($scope){
// add business logic in here. unit testable

var self = this;
self.map;
self.init = function(map) {
	self.map = map;
};
}]);


app.directive('mapDirective', mapDirective);

function mapDirective(){
	return {
		restrict: 'E',
		controller:'mapDirectiveController',
		template: '<div id="mapSize"></div>',
		link: function (scope, elem, attrs, ctrl){
			var map;
			map = new google.maps.Map(elem[0], {
				center: new google.maps.LatLng(40.440625,-79.995886),
				zoom: 15,
				mapTypeId: google.maps.MapTypeId.ROADMAP
			});
			ctrl.init(map);
		},
		replace: true
	}
}

This allows us to keep our DOM manipulating operations within the link function and move testable code to a controller.

We can now display a map using a directive. But, how do we interact with this map? Often times, you may have scenarios where an user event will need to manipulate the DOM element. Consider for example, an user event that will add markers to the map that is currently displayed. These markers could change based on the action the user is performing.. for example, they could be moving about in a map and list of makers displayed should change based on their new map center. Or they could be performing a search and with each additional criteria new markers should be displayed. The common thread is that interaction happens after the initial DOM has been rendered.

As always, there are many ways to handle this, but in this post, I am going to subscribe to an event in my directive that will listen to events originating from the controller handling the user interaction. When new markers need to be displayed, the controller responsible for handling the user interaction will raise an event (using $scope.$emit or $rootScope.$broadcast) passing in any additional event arguments that will needed for the directive to use in rendering this additional information.

Here's the code for the controller handling the user requests. We have a button bound to a function that returns a random location that should be displayed on the map rendered by the directive:

    var app = angular.module('app', []);
    // common service module to return rootScope
    app.factory('common', ['$rootScope', function ($rootScope) {
        return {
            rootScope: $rootScope
        }
    }]);

    // main application controller.
    // when new markers are added, the controller notifies the directive to update the map
    app.controller('MainCtrl', ['common', function (common) {
        var vm = this;
        vm.message = 'Click to add map markers';
        vm.downtownLocation = [40.440625, -79.995886];
        vm.markers = [];
        vm.addMarker = function () {
            var newMarkerLocation = [vm.downtownLocation[0], vm.downtownLocation[1]];
            newMarkerLocation[0] += (Math.random() > 0.5 ? -Math.random() / 100 : Math.random() / 100);
            newMarkerLocation[1] += (Math.random() > 0.5 ? -Math.random() / 100 : Math.random() / 100);
            vm.markers.push(newMarkerLocation);
            common.rootScope.$broadcast('markerAdded', newMarkerLocation);
        };
    }]);

Next, we wire up the mapDirectiveController to listen to these events and add a new maker to the map:

    // controller associated with the map directive
    // maintains an internal list of markers on the map
    app.controller('mapDirectiveController', ['$scope', function ($scope) {
        var self = this;
        self.map;
        self.mapMarkers = [];
        self.init = function (map) {
            self.map = map;
            self.setMarker({ key: '', location: [40.440625, -79.995886] });
            $scope.$on('markerAdded', function (event, args) {
                console.log('marker added event caught ' + args);
                self.setMarker({ key: '', location: args });
            });
        };
        self.setMarker = function (markerLocation) {
            var location = markerLocation.location;
            var key = markerLocation.key;
            if (location.length === 0)
                return;
            var marker = new google.maps.Marker({
                icon: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png',
                position: new google.maps.LatLng(location[0], location[1]),
                optimized: true,
                map: self.map
            });
        }
    }]);
    
    // map directive. initializes the map DOM element
    // off loads marker and event handling methods to the directive controller
    app.directive('mapDirective', mapDirective);
    
    function mapDirective() {
        return {
            restrict: 'E',
            controller: 'mapDirectiveController',
            template: '<div id="mapSize"></div>',
            link: function (scope, elem, attrs, ctrl) {
                var map;
                map = new google.maps.Map(elem[0], {
                    center: new google.maps.LatLng(40.440625, -79.995886),
                    zoom: 15,
                    mapTypeId: google.maps.MapTypeId.ROADMAP
                });
                ctrl.init(map);
            },
            replace: true
        }
    }

You can experiment with the full code on plunker here. 

Github code : https://github.com/mchengal/AngularJs/tree/master/GoogleMapDirective

Was this useful?

Have questions? We have answers. Contact us, we are glad to help! Spread the word if you found this useful! Tweet us on @thelatestbrian and @yanamegainfo