Wednesday, June 28, 2017

Angular Carto: Custom Zoom Buttons

This is part of a series covering how to create reusable Carto map components. Read the introduction to learn why one would even want to do such a thing. Read the previous installment, Carto Map Factory, to learn about adding a Carto map to a webpage and registering the map with the Angular dependency injector.

In this post, we will discuss how to add custom zoom buttons to the map.

What are directives?

We will implement our custom zoom buttons by encapsulating them in an Angular directive. Directives are a technique for packaging up bundles of HTML content and JavaScript code. Once a directive has been registered in your Angular module, it can be interpolated into the HTML of your web page, most commonly as an HTML element or as an attribute on a standard element. A good use case for directives is a text field with a date picker. The date picker should pop up a window with a navigable calendar, allow the user to browse through the calendar to find the date they want, and then save the date in the text field attached to the date picker. Additionally, the ideal date picker should be reusable throughout a website with a minimum of effort on the part of the programmer.

If directives are a new concept to you, I recommend Sandeep Panda’s “A Practical Guide to AngularJS Directives.”

The zoom buttons directive

Registering the directive

Directives are added to Angular modules using the directive function. The first argument is the name under which we are registering the directive; the second argument is a factory function for creating instances of the directive. Note that the registered name of the directive, zoomButtons, is camel-cased.

Note that in this sample code, the directive factory is an anonymous function, but you can (and perhaps should!) define a named function in a site-wide library and register the named function wherever zoom buttons are needed. Note also that the directive factory takes a parameter named map. At run time, the Angular dependency injector will pass whatever is registered under the name “map” as an argument to the directive factory. For more information about registering a Carto map with the Angular dependency injector, please see my previous post “Map Factory.”

Restricting calling style

Angular provides several different ways of calling a directive from HTML. The two most common are element style (<zoom-buttons></zoom-buttons>) and attribute style (<div zoom-buttons></div>). All calling styles produce the same results, so the Angular team recommends choosing the calling style to signal how the directive is used.

When should I use an attribute versus an element? Use an element when you are creating a component that is in control of the template. The common case for this is when you are creating a Domain-Specific Language for parts of your template. Use an attribute when you are decorating an existing element with new functionality.
To allow those two calling styles, we specify A (for attribute-style) and E (for element-style) in the restrict property of the directive. There are other directive calling styles, but it is not necessary to discuss them here.

Writing HTML to be interpolated

Next, we need to define an HTML template for the content that will be interpolated into the web page when the directive is invoked.

Each directive has its own $scope. If you are not familiar with $scope, for the purposes of this article it is best understood as the view model for an Angular component; in this case, $scope is the view model for the zoom buttons directive. The ng-click data binding and double-brace string interpolation are bound to methods and properties on this directive’s $scope.

This directive defines two buttons for zooming in and out. When clicked, the buttons call, respectively, the zoomIn() and zoomOut() functions on the $scope. The labels on the buttons are likewise defined in the zoomInLabel and zoomOutLabel properties on the $scope. Finally, to ensure that the caller of the directive has control over the appearance of the buttons, their class and style attributes are bound to the buttonClass and buttonStyle properties of the $scope.

You can define your HTML template either using a string literal, or by pulling it into a separate file if it is too unwieldy to manage within a string literal. Beware: the sample code below shows both techniques, but you should pick one or the other.

Configuring the directive $scope

When configuring a directive, we define a link() function that configures the directive $scope. The link() function is takes information from the directive invocation in the web page and translates that into the $scope required by the HTML template.

The first parameter of the link() function is the $scope to be configured. The second parameter is the HTML element on which the directive was invoked. The third parameter is the attributes of said element. In this particular link() function, information from the attributes are used to set the label and styling properties on the $scope. Note that the $scope.zoom*() functions are set to be map.zoom*.bind(map). Why not just set them to map.zoom* instead? Because the map.zoom* functions contain a variety of references to this. If you use $scope.zoom* = map.zoom*, those this references will be treated as referring to $scope rather than map. Using $scope.zoom* = map.zoom*.bind(map) binds the zoom methods so that they will still treat their thises as the map rather than the scope.

Bringing it all together

You can Fiddle around with a working Angular Carto map with custom buttons below.

Why would you do such a thing?

Carto/Leaflet maps come with a built-in zoom control. We had to explicitly turn it off with the option zoomControl: false. What’s so great about writing our own control in an Angular directive instead of using the built-in control? The main reason is that it gives us control over the look-and-feel of the zoom controls. If the built in Leaflet controls clash with the styling we’re using in the rest of the site, we can create buttons with the same functionality but a more appropriate appearance. Still not convinced? That’s okay. You’ve learned a lot about Angular directives, and that will serve you well in the rest of the series.

Other posts in this series

Friday, June 2, 2017

Angular Carto: Map Factory

This is part of a series covering how to create reusable Carto map components. Read the introduction to learn why one would even want to do such a thing.

In this post, we will discuss how to add a Carto map to a website and how to register the map with Angular so that our Angular components can manipulate the map display.

Reference the scripts

Before jumping into your new Carto map, you will first need to make sure that your web page has references to the Carto and Angular javascript libraries. In order for the map to tile correctly, you will also have to reference Carto’s standard stylesheet.

Sizing web page and containers

If you’re looking for an immersive map that takes up the entire web page, you will need to make sure that the html and body elements and the map container are all styled to fill all space available to them. My preferred method is to define a site-wide style implementing those requirements, including a carto-map-container CSS class that can be applied to the element where the Carto map is inserted.

Creating a container for the map

The map is inserted in a placeholder div element. You will need to assign that element an id. This id will later be passed to Carto to indicate where the map will be created. You will also need to assign the container div to the carto-map-container CSS class, which we defined above to expand to fill all space available. Experience has shown me that the map container div must be the first child of the body element for Carto to work correctly in Internet Explorer.

Registering a map factory with Angular

In a vanilla Carto web page, adding a map is as simple as instantiating the L.map class. (Technically, this is a Leaflet class, but the distinction is not relevant to the present post.) The complication of using Angular is that we will be building Angular components that manipulate the map, so we need a reliable way for those components to find the Leaflet/Carto map object. The method Angular uses for this is dependency injection. As we build angular components throughout this series, they will all take a parameter named map as a parameter. The Angular dependency injector will inspect each component’s constructor and provide the map object registered under the name “map” as the argument to the map parameter.

To register the map with Angular, we will register an anonymous function that instantiates and configures the map as factory under the name “map”. (The many options and functions to customize Leaflet/Carto map objects are beyond the scope of this post, so review the Leaflet documentation if you need to learn more.) Once another Angular component requests the map by having a parameter named “map” in its constructor, Angular will invoke the anonymous function to build the map, and store the resulting value to provide for all future requests of the map. In other words, the map creation function is called only once.

You should also note that there is another dependency injection at work in this code. The first argument of the L.map initializer is the element ID of the Carto map container. Rather than providing this ID as a string literal, map factory takes a parameter mapContainerID. The actual element ID is registered with the Angular dependency injector using the value. Putting the dependency injector in control of where the map is created loosens the coupling between the map factory and the HTML.

Bringing it all together

In a normal application where there’s at least one component that requires the map, all we would have to do is register the map factory. However, the example code doesn’t have any Angular components, so we need to request the map from the Angular dependency injector ourselves. You can safely ignore the window.onload handler in the sample code, because having at least one map-dependent Angular component on your page will obviate the need for it.

You can Fiddle around with a working Angular Carto map below.

Why would you do such a thing?

Why are we registering an anonymous function as an Angular factory, then jumping through hoops to get the Angular dependency injector to invoke the registered function? Why not just instantiate and configure the Leaflet map and be done with it? Good question! The example code provided in this post is definitely more complicated than necessary for the task of putting a map on a web page. Dependency injection shines when we have a complicated application with many components that manipulate the map. Over the course of this series, I think you will begin to see what I mean.

Design pattern bingo

  • Dependency injection
  • Angular provider (factory)

Other posts in this series

Revision history

  • 2017-06-06: The example code in this page originally passed the element ID carto-map of the Carto map container as a string literal to the map initialization function L.map. That was poor Angular style and has been rectified.