For the recent Humangeo hackathon, team ShowMeRaces set off to create a unified road racing webapp. The project would consist of a scraping platform to pull and aggregate race data from several disparate websites, an Elasticsearch cluster for storage and quick and powerful geospatial searches, and finally an Angular/Django webapp. This being a hackathon where exploration and experimentation are put at a premium, I decided to dive head first into the new Angular Component syntax.

ShowMeRaces Webapp

Component Syntax

Angular 1.5 introduced Component syntax to the ecosystem. Components are self-contained sections of a web UI. If you are familiar with development in Angular prior to 1.5 then a Component is basically a directive and controller baked into one discrete structure that has been optimized for the best practices learned by Angular developers over its lifetime. While Angular’s standard directive and controller syntax can be very expressive, misuse can lead to an unwieldy code base. I know I’ve seen my fair share of projects with hard to debug nested controllers. Components allow for easy separation of duties on the front-end and supports one-way data flow that is easier to comprehend and allows for a consistent flow of data.

Another side benefit is that Angular 2 is based strictly on Components. While there are several differences outside the scope of this post, understanding these Component interactions in Angular 1.5 will make the jump to Angular 2 easier.

Through the experiences of developing the ShowMeRaces webapp as a guide, I hope to convey the usefulness of Angular’s new Component syntax.

Structural Design

The ShowMeRaces webapp is comprised of four Components and two Services. Services are used primarily as helpers that are narrowly focused, live the lifetime of the app, and are available to all Components to run view-independent business logic.

Services and Components

The raceService handles all HTTP API calls to our web server while the mapService handles all map specific logic such as configuration, manipulation, data addition and deletion. Having all of this functionality defined in services outside of the view/controller layer allows for easy testability as well as decoupled separation of concerns.

Component Design

The 4 components developed were raceHome, raceMap, raceDetail and raceList.

Highlight Components

raceHome is the main parent component that controls all the rest. It handles the retrieval, coordination and linking of the race data as well as reacting to events from its child components. It interacts with all of the child Components so they don’t have to interact with each other. This way the flow of data is always from the top down.

Component Data Flow

Bindings

Achieving this structure is extremely easy with the new directional data binding syntax. Notice the one-way bound data objects are defined with a ‘<’ rather then the standard two way binding syntax of ‘=’.

angular
    .module('app.races.components')
    .component('raceList', {
        bindings: {
            raceListBounded: '<',
            selectedRace: '<',
            onSelect: '&',
            onClickSelected: '&'
        },
        templateUrl: 'static/app/races/race-list.html',
        controller: RaceListController
    });

By componentizing our Angular code we can ensure separation of duties by only providing relevant data to each component. For example the selectedRace object (the currently highlighted race) is needed by all three Components while the raceListBounded list (the list of races currently visible on the map) is only needed by the raceList component.

Component Lifecycle Hooks

Component lifecycle hooks give developers a set of specific methods that will be invoked by the framework during the lifetime of the component. For example $onInit sets up the controller, and $onChanges allows the controller to execute code when one of the bound objects changes. For the raceDetail component it’s important to know when the selectedRace changes in order to update the detail map by adding a new “selected race route”.

ctrl.$onInit = function () {

    ctrl.mapObject = mapService.getDefaultMapObject();

    mapService.addBaseLayer(ctrl.mapObject, 'mapboxLight', mapService.baselayers.mapboxLight);
    mapService.addBaseLayer(ctrl.mapObject, 'mapboxSat', mapService.baselayers.mapboxSat);

};

ctrl.$onChanges = function (changesObj) {

    if (changesObj.selectedRace && changesObj.selectedRace.currentValue != changesObj.selectedRace.oldValue) {
        mapService.updateSelectedRaceDetailRoute(ctrl.mapObject, changesObj.selectedRace.currentValue);
    }

};

Function Call Backs

Since we are dealing with one-way data bindings, the child components need a way of telling the parent raceHome component that the user has selected a race. Component syntax allows you to define callback functions in the bindings to accomplish this by using an ‘&’ in the binding configuration.

angular
	.module('app.races.components')
	.component('raceList', {
	    bindings: {
	        raceListBounded: '<',
	        selectedRace: '<',
	        onSelect: '&',
	        onClickSelected: '&'
	    },
	    templateUrl: 'static/app/races/race-list.html',
	    controller: RaceListController
	});

With that setup, we have created a good symbiotic relationship between Components, with data flowing down and actions propagating up.

Leaflet Angular Integration

Though not the main focus of this article I was very impressed with the ease of use of the UI-Leaflet Angular library. Leaflet is a very mature and robust Javascript tile mapping library for interactive maps that Humangeo has frequently relied upon and provides lots of functionality and configurability. The UI-Leaflet Angular library wraps up the standard Leaflet syntax into an Angular directive. It’s default configuration allows for an easy starting platform, while its deep configuration hooks to standard Leaflet functionality make it very powerful.

During development, the UI-Leaflet directive allowed me bite off small chunks of map functionality at a time. I first added the default map with markers for race start locations, and later on in the development cycle, I added race routes and map layers with little additional work. The directive also behaves as expected for an Angular library in that data changes are immediately propagated to the screen.

Code Demo

Lets look at how we can incorporate all of these pieces of Component syntax into the raceMap Component, which will be our own data and interaction wrapper around the Leaflet map directive.

The basic structure of the raceMap Component definition.

  • Bindings and template URL
  • Object declarations
  • Lifecycle hooks
  • Action function declarations
(function () {
    'use strict';

    angular
        .module('app.races.components')
        .component('raceMap', {
            bindings: {
                raceList: '<',
                selectedRace: '<',
                clickedRace: '<',
                onMapBoundsUpdate: '&',
                onSelect: '&',
                onClickSelected: '&'
            },
            templateUrl: 'static/app/races/race-map.html',
            controller: RaceMapController
        });

    RaceMapController.$inject = ['mapService', '$scope'];

    function RaceMapController(mapService, $scope) {
        var ctrl = this;

        ctrl.mapObject = {};
        ctrl.initialBoundsSet = false;

        ctrl.$onInit = function () {
            ...

        };

        ctrl.$onChanges = function (changesObj) {
            ...
        };

        $scope.$watch('$ctrl.mapObject.bounds', _.debounce(function (newValue, oldValue) {
            ...
        }, 1000));
    }
})();

The view template for the raceMap component is used to wire up the UI-Leaflet directive to the objects being manipulated by the controller:

<leaflet
	id="raceMap"
	watch-options="ctrl.mapObject.watchOptions"
	lf-center="$ctrl.mapObject.center"
	bounds="$ctrl.mapObject.bounds"
	layers="$ctrl.mapObject.layers"
	markers="$ctrl.mapObject.markers"
	paths="$ctrl.mapObject.paths"
	flex></leaflet>

The $onInit function is used to configure the base map objects, as well as define any event hooks necessary for communicating map changes or interactions to the raceHome Component.

ctrl.$onInit = function () {

    // Build Leaflet Map Object
    ctrl.mapObject = mapService.getDefaultMapObject();

    mapService.addBaseLayer(ctrl.mapObject, 'mapboxRunBikeHike', mapService.baselayers.mapboxRunBikeHike);
    mapService.addBaseLayer(ctrl.mapObject, 'openStreetMap', mapService.baselayers.openStreetMap);
    mapService.addBaseLayer(ctrl.mapObject, 'openCycle', mapService.baselayers.openCycle);
    ...

    mapService.addOverlay(ctrl.mapObject, 'races', mapService.overlays.races);
    mapService.addOverlay(ctrl.mapObject, 'routes', mapService.overlays.routes);
    mapService.addOverlay(ctrl.mapObject, 'selectedRace', mapService.overlays.selectedRace);

    mapService.updateBounds(ctrl.mapObject, mapService.bounds.northEasterUS);

    //Add Event Actions
    $scope.$on('leafletDirectiveMarker.raceMap.mouseover', function (event, args) {

        ctrl.onSelect({race: args.model.raceObj});
    });

    $scope.$on('leafletDirectiveMarker.raceMap.click', function (event, args) {

        ctrl.onClickSelected({race: args.model.raceObj});
    });

    ...

};

The $onChanges method is heavily used in the raceMap Component. Most of the time in Angular with one way binding, simple values like strings or dates can just directly link the data to the DOM and as new data is received the DOM is updated. However, with the raceMap Component, when we receive new data (i.e. a list of geolocated races), the data needs to be formatted and passed to the Leaflet Directive to update the map. This is also a perfect opportunity to utilize a decoupled mapService for this “formatting” logic.

ctrl.$onChanges = function (changesObj) {

    if (changesObj.raceList && changesObj.raceList.currentValue != changesObj.raceList.previousValue) {
        mapService.updateRacesAndRoutes(ctrl.mapObject, changesObj.raceList.currentValue);
    }

    if (changesObj.selectedRace &&
        changesObj.selectedRace.currentValue != changesObj.selectedRace.previousValue) {
        mapService.updateSelectedRacePath(ctrl.mapObject, changesObj.selectedRace.currentValue);
    }

    if (changesObj.clickedRace &&
        changesObj.clickedRace.currentValue != changesObj.clickedRace.previousValue) {
        mapService.toggleRaceRoute(ctrl.mapObject, changesObj.clickedRace.currentValue);
    }
};

Finally, there is a watch for the map bounds. The UI-Leaflet directive exposes the map’s viewport bounds as a javascript object and updates that object when ever the map is panned or zoomed by the user. Just like with the $onChanges method, changes propagate up to the raceHome Component by invoking the callback changeBounds method. This allows the Component to react to the new map bounds by firing off a geolocation search using the raceService.

$scope.$watch('$ctrl.mapObject.bounds', _.debounce(function (newValue, oldValue) {
    if (newValue != oldValue && ctrl.initialBoundsSet === true) {
        ctrl.onMapBoundsUpdate({bounds: ctrl.mapObject.bounds});
    }
    // When the page is rendered bounds are initially set to the size of the page, we don't want to use those bounds to search
    if (ctrl.initialBoundsSet === false) {
        ctrl.initialBoundsSet = true;
    }

}, 1000));

And we’ve come full circle. Angular has been a useful tool in Humangeo’s ever expanding toolbox. As a webapp framework we’ve used it to create beautiful and functional websites with large teams quickly. I hope that my experiences using the new Angular Component syntax and UI-Leaflet library have been helpful to those of you choosing to live on the forefront of technology. Thats what we do and if that excites you, we’re hiring!

Team ShowMeRaces

Showmeraces Team Photo