AngularJS: Lessons learned

by Jettro CoenradieMarch 14, 2013

AngularJS largeAt Devoxx 2012 I attended the AngularJS presentation by Igor Minar and Misko Hevery. I was very enthusiastic about the capabilities of this front-end framework. Therefore I started experimenting with it. I created a sample for the Axon Framework, read more about it here. After my experiments I felt confident enough to start using it in real projects. One of them was adding management reporting using the HighCharts library.

The next step was a bigger project, writing an Elasticsearch plugin to query your Elasticsearch instance. This project has to integrate with a javascript library to interact with Elasticsearch. The layout and other front-end components were implemented using Twitter Bootstrap. Therefore I also used another AngularJS plugin to integrate with Bootstrap.

In this blog post I’ll give you some lessons learned with respect to AngularJS.

Introduction

I am not going to write down a complete guide into AngularJS. Check my previous blog post for more startup documentation. For more background on the Elasticsearch plugin check my other blog post Introducing Query tool for Elasticsearch (part 1).

If you want to tag along using the sources, checkout my Github project.

https://github.com/jettro/angular-es-client

The AngularJS tricks

Use the angular quick start (aka angular-seed)

The Angular team provides a project template that they call the angular-seed. This project gives a complete skeleton for an AnagularJS project. Including the option to run unit tests and integration tests. It has all the libraries at the right location, wires the different components like controllers, services, directives and filters. More information about the angular-seed project can be found here:

https://github.com/angular/angular-seed

Using partials

When you have the plugin installed in Elasticsearch and you browse to the plugin, you’ll get to see the index.html. This is where it all starts. In the html tag you can find the ng-app directive with the name of the application. You’ll also find the ng-controller directive for the navigation and my own navbar directive. Finally you’ll see the ng-view directive that is the placeholder for the actual content coming from the partials. Now how is everything initialized. That starts with the initialization of the different components in the application module. This can be found in the file app.js

angular.module('myApp', ['myApp.filters', 'myApp.services', 'elasticjs.service', 'myApp.directives', 'ui.bootstrap']);

In this module we configure the different components by loading them for the myApp module. Notice that we inject more than our own components. We also inject the Elasticsearch module.

Another important part of the application module setup is the routing configuration. Angular is created to be a one page application. Usually every application needs more than one functionality. Therefore we need at least navigation. When using multiple views, you could store everything in one big html file. This is of course hard to maintain. Therefore routing also introduces the partial concept. This way you can inject content coming from another html file into the existing location. The following code block shows the configuration

angular.module('myApp', ['myApp.filters', 'myApp.services', 'elasticjs.service', 'myApp.directives', 'ui.bootstrap']).
        config(['$routeProvider', function ($routeProvider) {
            $routeProvider.when('/dashboard', {templateUrl: 'partials/dashboard.html', controller: DashboardCtrl});
            $routeProvider.when('/node/:nodeId', {templateUrl: 'partials/node.html', controller: NodeInfoCtrl});
            $routeProvider.when('/stats', {templateUrl: 'partials/stats.html', controller: StatsCtrl});
            $routeProvider.when('/query', {templateUrl: 'partials/query.html', controller: QueryCtrl});
            $routeProvider.otherwise({redirectTo: '/dashboard'});
        }]);

Notice that we also provide a default, in our case the dashboard. At the moment /query is the most interesting one. Here I have created the most functionality. Three things are important to know, in the url the current items can be found in the format #/query. The content of a partial is added to the tag containing ng-view. The configuration also contains the Controller that picks up the request and the view or partial to load.

Custom directive for navigation

One very nice aspect of AngularJS is about adding your own html elements or attributes. I wanted to have navigation on the top of the page with the current page highlighted. The following code block shows the html for adding the navigation. As you can see this is not to much. It contains one element with one attribute.

<div ng-controller="NavbarCtrl">
    <navbar heading="Elasticsearch GUI"/>
</div>

The following code block shows the JavaScript directive for handling this new html element. This directive can be found in the directives.js file.

angular.module('myApp.directives', []).
        directive('navbar', ['$location', function ($location) {
            return {
                restrict: 'E',
                transclude: true,
                scope: {heading: '@'},
                controller: 'NavbarCtrl',
                templateUrl: 'template/navbar/navbar.html',
                replace: true,
                link: function ($scope, $element, $attrs, navbarCtrl) {
                    $scope.$location = $location;
                    $scope.$watch('$location.path()', function (locationPath) {
                        navbarCtrl.selectByUrl(locationPath)
                    });
                }
            }
        }]);

The name of the directive is the same as the used html element. The heading value is added to the scope, this way the template can use the parameter as taken from the heading attribute of the html element. During the linking phase we add a watch to the current location. If it changes we check all navigation items and change the active item. The code to do this is coming from the configured controller. The next code block shows this controller.

function NavbarCtrl($scope) {
    var items = $scope.items = [
        {title: 'Home', link: 'dashboard'},
        {title: 'Queries', link: 'query'},
        {title: 'Statistics', link: 'stats'}
    ];

    this.select = $scope.select = function (item) {
        angular.forEach(items, function (item) {
            item.selected = false;
        });
        item.selected = true;
    };

    this.selectByUrl = function (url) {
        angular.forEach(items, function (item) {
            if ('/' + item.link === url) {
                $scope.select(item);
            }
        });
    };
}

Creating your own service

AngularJS is an mvc framework. Just like we did on the server, we want controllers to have as little amount of functionality as possible. That is why we try to extract functionality into services and inject the service into the controller. The following code block shows creating and registering the service. For now just one method is shown, obtaining all the indexes.

var serviceModule = angular.module('myApp.services', []);
serviceModule.factory('elastic', ['$http', function (http) {
    function ElasticService(http) {
        this.indexes = function (callback) {
            http.get('/_status').success(function (data) {
                callback(data.indices);
            });
        };
	}
    return new ElasticService(http);
}]);

In the second line we register the service elastic. This service has one method called indexes. What the service does is not that important yet. Now let us have a look at how this is used in a controller.

function QueryCtrl($scope, $dialog, ejsResource, elastic) {
    $scope.loadIndices = function () {
        elastic.indexes(function (data) {
            $scope.indices = data;
        });
    };
}
QueryCtrl.$inject = ['$scope', '$dialog', 'ejsResource', 'elastic']

By reusing the name elastic we can inject the service. In line 3 we demonstrate using the indexes function. The last line is used to be able to inject objects by name even if we use minify and obfuscate functionality. To make the picture complete the next block shows the html to print the buttons for the different indexes.

<div class="row-fluid">
    <div class="span2"><span class="text-info">Available indexes:</span>
        <button popover-placement="right"
                popover="Here you can select the index that we query over. If you select nothing, we query over all data"
                class="btn btn-mini btn-link"><i class="icon-question-sign"></i></button>
    </div>
    <div class="span10">
        <div id="chooseIndexBtn" class="btn-group" data-toggle="buttons-checkbox">
            <div class="btn btn-mini" ng-click="chooseIndex(key)" ng-repeat="(key,value) in indices">{{key}}</div>
        </div>
    </div>
</div>

Creating a popover for help buttons

The html block in the previous section also shows the integration with the popover. There are two additional attributes for the button called popover-placement and popover. This functionality is provided by the angular/bootstrap integration. You can find more information about this plugin here. Using it is as easy as initializing the ui.bootstrap library as a module. Check the section describing the app.js file.

Creating a modal for adding facets

Creating the module is similar to the popover. We just make use of the bootstrap/angularjs integration. The following code block show the function that opens a dialog and handles the response of the dialog.

    $scope.openDialog = function () {
        var opts = {
            backdrop: true,
            keyboard: true,
            backdropClick: true,
            templateUrl: 'template/dialog/facet.html',
            controller: 'FacetDialogCtrl',
            resolve: {fields: angular.copy($scope.fields)}};
        var d = $dialog.dialog(opts);
        d.open().then(function (result) {
            if (result) {
                $scope.facets.push(result);
                $scope.changeQuery();
            }
        });
    };

The source code shows we obtain the template from the file facet.html and the logic is in the FacetDialogCtrl controller. These files contain some logic for selecting the right facet and showing different input fields based on the selected facet. The code is straightforward and therefore I leave it up to the reader to have a look at the code.

Concluding

This blog post has given an idea about writing components with AngularJS. You have seen how to use the different Bootstrap components using AngularJS. I hope you have more understanding now about what you can do with AngularJS. Of course I still only scratched the surface here.

References