I’ve been doing quite a bit of learning about ESRI JavaScript API to make creating, structuring, and displaying data that much easier.

Being that the ESRI API is based on Dojo, we are duplicating some of that functionality by bringing Angular into the picture. I’ve built a full app using only Dojo with the ERSI API and I’ve had issues with doing it with pure Dojo. Those being:

Dojo doesn’t do two-way binding. When developing a data-heavy map app, that starts to become quite useful. Dojo’s widgets don’t seem to integrate well with other widgets. They do have an event structure to communicate between modules, but that’s primarily the only way it can be done. This is somewhat of a by-product of Dojo being a toolkit, rather than a framework. An application structure is not implied by Dojo. Dojo’s unit testing support is something that I cannot understand. They have their own harness that is used for testing Dojo code but I cannot get it to work. That may just be how I was trying to use it, or how I structured the widgets in the app, but I was having a difficult time.

For those three reasons, Angular seemed like a good idea as it resolves those issues. Angular also does all three of them quite well. Angular is a framework built with MVC in mind, this implied structure makes communication and creating modules much easier. It was also built with unit testing in mind. Apart from the necessary map widgets, I’ve tried to avoid using Dojo to increase testability. Obviously, mocking the Dojo pieces could prove difficult but I’ll go more in depth about that in another post.

I figured I would share a simple example of creating a map with layers using Angular. This may not be the most efficient or best way but it works for what I’m doing for the city maps. Let me know if there’s any ways that it can be done better as I’m very much still learning Angular.

This post does assume a working knowledge of Angular. If you know nothing about it, I suggest going to their site, do some examples, and come back as the code will make much more sense. It also assumes a little knowledge of the ESRI API. I’ve got a couple posts on displaying data. That is a good starting point and is more than enough to get you going with this post.

Let’s get to the code.

Getting Going

To start, we need to bring in the ESRI API and Angular, obviously:

<link rel="stylesheet" href="http://js.arcgis.com/3.9/js/esri/css/esri.css" /> <script src="http://js.arcgis.com/3.9/"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.10/angular.min.js"></script>

We also need to setup Dojo’s AMD, so that we can seperate our modules into seperate files, to be able to pull local modules. I’m just going to do this inline instead of creating a new file since we only need to setup our local package.

<script>window.dojoConfig = { packages: [{ name: 'local', location: '/js' }] };</script>

This line needs to be added before you bring the ESRI API. Update the location with where you’re local script files will be.

Create the Map Directive

We need to create a directive for the map container. This directive will instantiate the map and attach some events to the map that various scopes can tap into.

Let’s create the map module:

(function (define, angular) { 'use strict'; require(['esri/map'], function (Map) { var esriMap = angular.module('esri.map', []); // Continued }); }(window.define, window.angular));

Next, we want to create the directive definition:

esriMap.directive('esriMap', function () { return { restrict: 'EA', controller: 'MapController', link: function (scope, element, attrs, ctrl) { ctrl.init(element); } }; });

If you are not familiar with some of the options, I suggest looking through Angular’s ui.bootstrap, which has a collection of directives based on Twitter Bootstrap. This article breaks down that method better than I could. They talk about controllers being easier to unit test than putting everything inside the directive. We can test the controller in isolation from the directive. Some say the controller should not have access to the DOM, but this way we can mock the DOM element when testing the controller as well.

Now for our map controller:

esriMap.controller('MapController', ['$rootScope', '$scope', '$attrs', function ($rootScope, $scope, $attrs) { var self = this; // Reference to 'this' to use in functions var mapDiv, layers = []; this.init = function (element) { if (!$attrs.id) { throw new Error('\'id\' is required for a map.'); } self.$element = element; self.createDiv(); self.createMap(); }; this.createDiv = function () { mapDiv = document.createElement('div'); mapDiv.setAttribute('id', $attrs.id); self.$element.removeAttr('id'); self.$element.append(mapDiv); }; this.createMap = function () { var options = { center: $attrs.center ? JSON.parse($attrs.center) : [-56.049, 38.485], zoom: $attrs.zoom ? parseInt($attrs.zoom) : 3, basemap: $attrs.basemap ? $attrs.basemap : 'streets' }; $scope.map = new Map($attrs.id, options); $scope.map.on('load', function () { $rootScope.$broadcast('map-load'); }); $scope.map.on('click', function (e) { $rootScope.$broadcast('map-click', e); }); if (layers.length > 0) { $scope.map.addLayers(layers); layers = []; } }; $scope.addLayer = function (layer) { if ($scope.map) { $scope.map.addLayer(layer); } else { layers.push(layer); } }; }]);

The createDiv function basically makes the container for the map to attach to, this way if there’s any directives inside the map directive, the ESRI map won’t remove whats in there. We also give support for changing some of the map options via HTML attributes (there’s an example of this at the end of the post). The only attribute that is made required is the id attribute as that’s the way the map attaches itself to the div. createMap is pretty self-explanatory; create the map and attach the events. We also check to see if there’s any layers to add that were created before the map was created.

We added an addLayer method to the scope so that layers inside the map can get added. We need to check if the map exists so we defer the adding of the layer. This is just to make sure layers won’t attempt to get added before the map is fully loaded. This will be used in the next section.

The HTML:

<esri-map zoom="4" basemap="streets"></esri-map>

Adding Layers to the Map

What’s a map if you can’t add layers to display data on it? We will follow the same directive structure as we did with the map.

I’m going to only show creating feature layers in this post, which you can read more about in a previous post I made about displaying custom data using feature layers. I’ll be using a sample layer that ESRI uses through some of their samples. Other layers like the Tiled, or Map Service, will somewhat follow the same structure. I can go more in depth about those if needed in another post.

Module definition:

(function (define, angular) { 'use strict'; define(['esri/layers/FeatureLayer'], function (FeatureLayer) { var featureLayer = angular.module('esri.feature.layer', []); }); }(window.define, window.angular));

Now for the directive:

featureLayer.directive('featureLayer', function () { return { restrict: 'EA', replace: true, require: ['featureLayer', '^esriMap'], controller: 'FeatureLayerController', link: function (scope, element, attrs, ctrls) { ctrls[0].init(); } }; });

This is similar to how we setup the map directive with some small differences, one being the require attribute. This forces the directive to be inside our esriMap directive. Angular gives us a bunch of other benefits like being able to access esriMap‘s scope meaning we can get to the map if we need to, as well as other methods we allow access to, like the _addLayers_ method. We also don’t need to pass in the element to the controller since we won’t be modifying the element itself at all.

Let’s create the controller:

featureLayer.controller('FeatureLayerController', ['$scope', '$attrs', function ($scope, $attrs) { this.init = function () { if (!$attrs.id) { throw new Error('\'id\' is required for a feature layer.'); } if (!$attrs.url) { throw new Error('\'url\' is required for a feature layer.'); } var layer = new FeatureLayer($attrs.url, { id: $attrs.id }); $scope.addLayer(layer); }; }]);

The controller is simple. We just check some required attributes, instantiate the Feature Layer, then add it to the map. Here’s an example of the HTML with map and layer together:

<esri-map id="map" zoom="11" center="[-97.395, 37.537]"> <feature-layer id="sampleLayer" url="http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Petroleum/KSFields/FeatureServer/0"></feature-layer> </esri-map>

That’s it. Now you have a simple structure to create a map and add feature layers to it using only HTML. With all that being said, you should get something like the following:

Wrap Up

This was meant to just be a ‘getting started’ post. I have plans to talk about clicking on features to display their attributes and creating custom map specific widgets like a toolbar or street/satellite selector. I also want to show how to write unit tests for the two directives created above. Also, if you’re an Angular master and can let me know how my code can be improved, let me know in the comments, or let me know if there’s any issue you have using Angular with the ESRI API.

Update 8/29/2014: Typo in the one of the code samples that would make it not run. Thanks for pointing that out Jeffry Houser!