by Juampy NROctober 17, 2013

Move logic to the front end with AngularJS

At Lullabot, we always aim to make sites as performant and maintainable as possible. Recently, we've started to decouple bits of logic from Drupal and move them to the client's browser using JavaScript.

Let's look at an example. We want to display the weather of a given city in our website. This involves:

  1. Calling a public API with some parameters. We have chosen OpenWeatherMap for this example.
  2. Extract weather data from the response.
  3. Show the data in the browser.

The result would look like the following:

null

In Drupal, we could create a block that uses drupal_http_request() to fetch the data, then passes its results to a theme function that renders it. That is simple and maintainable, but why does Drupal needs to take care of this? There is no database involved, nor session management. If our site relies on caching to improve performance, we'll have to clear that cache whenever the block's content is updated.

Instead, let's move this to pure JavaScript and HTML so the client's browser will be the one in charge of fetching, processing and caching the data.

Meet AngularJS

AngularJS is an MVC JavaScript framework which elegantly separates controller, business and model logic in your application. Although there is a lot to learn, it removes a lot of backend logic in our Drupal projects and we've had wonderful success with it so far.

The full example can be found at https://gist.github.com/juampy72/6003761. Let's go through its flow together.

Bootstrapping our AngularJS application

Let's start by adding a directive to bootstrap our AngularJS application. Add the following attribute to your html.tpl.php file:

https://gist.github.com/juampy72/6003761#file-html-tpl-php

  
<html data-ng-app="myapp" xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php print $language->language; ?>" version="XHTML+RDFa 1.0" dir="<?php print $language->dir; ?>"<?php print $rdf_namespaces; ?>>
  

The attribute data-ng-app="myapp" is telling AngularJS to bootstrap our application named "myapp". For the moment this is all we need, so let's move on. We will implement our AngularJS application later.

Rendering the skeleton in Drupal

Our custom Drupal module contains some simple code that implements a block. The mymodule_block_view() function also includes a JavaScript file (the AngularJS controller) and a template which holds the markup that the AngularJS controller will use:

https://gist.github.com/juampy72/6003761#file-mymodule-module

  
/**
 * Implements hook_block_view().
 */
function mymodule_block_view($delta = '') {
  $block = array();
 
  switch ($delta) {
    case 'weather':
      $path = drupal_get_path('module', 'mymodule');
      $block['subject'] = t('Weather status');
      $block['content'] = array(
        '#theme' => 'weather_status',
        '#attached' => array(
          'js' => array(
            'https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js',
            $path . '/mymodule.js',
          ),
        ),
      );
      break;
  }
  return $block;
}
  

That is all that Drupal will do; build the foundation.

Processing in the browser

When the page is delivered to the client, the AngularJS controller will kick in early to fetch the data from OpenWeatherMap, then process it for the view:

https://gist.github.com/juampy72/6003761#file-mymodule-js

  
/**
 * Renders the weather status for a city.
 */
var app = angular.module('myapp', [])
.controller('MyModuleWeather', function($scope, $http, $log) {
  // Set default values for our form fields.
  $scope.city = 'Madrid';
  $scope.units = 'metric';
 
  // Define a function to process form submission.
  $scope.change = function() {
    // Fetch the data from the public API through JSONP.
    // See http://openweathermap.org/API#weather.
    var url = 'http://api.openweathermap.org/data/2.5/weather';
    $http.jsonp(url, { params : {
        q : $scope.city,
        units : $scope.units,
        callback: 'JSON_CALLBACK'
      }}).
      success(function(data, status, headers, config) {
        $scope.main = data.main;
        $scope.wind = data.wind;
        $scope.description = data.weather[0].description;
      }).
      error(function(data, status, headers, config) {
        // Log an error in the browser's console.
        $log.error('Could not retrieve data from ' + url);
      });
  };
 
  // Trigger form submission for first load.
  $scope.change();
});
  

Rendering the results

Our template simply references our controller (that is how AngularJS does the binding) and outputs the variables we set in the $scope object previously.

https://gist.github.com/juampy72/6003761#file-weather-status-tpl-php

  
<div ng-controller="MyModuleWeather">
  <label for="city">City</label>
  <input type="text" ng-model="city" /></br>
  <label for="units">Units</label>
  <input type="radio" ng-model="units" value="metric"/> Metric
  <input type="radio" ng-model="units" value="imperial"/> Imperial</br>
  <button ng-click="change()">Change</button>
  <h3>{{data.name}}</h3>
  <p>{{description}}</p>
  <p>Temperature: {{main.temp}}</p>
  <p>Wind speed: {{wind.speed}}</p>
</div>
  

There you have it! We have a fully functional block that is processed in the browser. If we apply this pattern to other frequently-changing blocks on the page, we'll be able to simplify the work that Drupal does, make the page's caching more efficient, and achieving better performance. You can even use this pattern to lazy load content that varies from user to user, making the rest of the page easier to cache.

On consuming external APIs

Whenever you are building a page on one domain and requesting data from another in the client's browser, remember that browser security mechanisms can sometimes stand in the way. There are two popular ways of overcoming this. One is through Cross-Origin Resource Sharing: of course, there is a module for that on Drupal.org. The other method is using JSONP. That's the method we used in this example, and it is supported by AngularJS and JQuery.

Why not build it with jQuery?

Technically, it is possible to build the same functionality using JQuery. However, it would require more code: you would have to take care of hiding the template while the page is being built, define a listener for the submit button, sanitize data, and bind it to the template yourself. Even with such a simple example, AngularJS offers a simple, more structured approach. It's also possible to use jQuery within AngularJS code.

Here you can find a jQuery version of the example. Have a look and compare both implementations.

Next steps

Start playing with AngularJS. Try the AngularJS plugin for Chrome.