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.

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.