by Juan Pablo Novillo Requena on October 17, 2013 // Short URL

Move logic to the front end with AngularJS

How to speed up Drupal by migrating server-side logic to the browser

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:

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.

Juan Pablo Novillo Requena

Developer

Want Juan Pablo Novillo Requena to speak at your event? Contact us with the details and we’ll be in touch soon.

Comments

Add Your Comment

yakoub

why ow why ?!

why does people choose to write such ugly lines in javascript : var app = angular.module('myapp', []) .controller('MyModuleWeather', function($scope, $http, $log) {

and this $http.jsonp(url, { params : { q : $scope.city, units : $scope.units, callback: 'JSON_CALLBACK' }}).

there is no reason, you can break that line to much more readable clear code . javascript is great, but people who write javscript usually are very bad programers .

Reply

nod_

Really?

If you have a problem with it please post another way of writing the code so that everyone can benefit.

Reply

yakoub

yes ... really .

var app = angular.module('myapp', []); app.cont = function($scope, $http, $log) { }; app.controller('MyModuleWeather', app.cont); var params = { q : $scope.city, units : $scope.units, callback: 'JSON_CALLBACK' }; $http.jsonp(url, { params : { params });

it seems your comments doesn't support markdown code block ...

Reply

yakoub

bad code

this is clearly bad writing by most basic standards, and i don't understand why javascript community continue to adopt this style .

first you can't document such code .

second you have parenthesis that ends one inside the other at the end of file in very ugly way which make syntax unreadable

third it creates dependency in different sections of unrelated code, for example if you have error in function body then it is difficult to reach and identify it using debugger because you need to enter other layers of wrapping function calls and object instantiating .

Reply

yakoub

simple javscript

i believe people used to write this style of one line code because they used to do very simply tasks in javscript . but if you intend to write complex logic like you declare then you can't continue using that same one line code style .

Reply

mradcliffe

AngularJS is still the hot

AngularJS is still the hot "new" thing although it's not really new. By a coincidence, I was doing a bit of personal development on a simple html/angularjs/bootstrap3 app where I didn't want any back end and simple content injection.

I was trying to figure out how to create dynamic HTML without having to write a butt load of javascript to do it.

In the end I chose to use showdown/bt-markdown to inject content, and wrote the equivalent of theme_link to return either a link in markdown or a h3, if no href present. That way I didn't have to use the dreaded "unsafe" directive.

It seemed to me like I was trading off "blobs" in my content for "blobs" in my partials as partials just seem to end up looking like the output of WYSIWYG editors or nasty templates. I think as I keep learning Angular I might have to just write the equivalent of Views or something to keep my partials as simple and re-usable as possible.

Reply

eaton

Partials and templating

One of the interesting advances that's coming in Drupal 8 is the use of the Twig templating system for Drupal themes. There's an implementation of Twig in JS in addition to PHP -- I'm curious whether that could lead to a world where rendering a chunk of content would use the same templates on the client AND the server side.

Reply

Greg

AJAX

I'm curious why you chose to write this by adding a JS framework to Drupal rather than making a AJAX call to render it?

Reply

sreten

Why not #theme

In your block content you used #markup = theme(..). Isn't it better to do something like #theme = '..' ? Then another module could do a block_view_alter() before HTML is rendered.

Reply

Juampy

Definitively!

I have just updated the full example and related snippet with your suggestion.

Thanks!

Reply

Chris

jQuery

I am unconvinced that the amount or complexity of equivalent jQuery code for this feature would be sufficient to warrant adding Angular to the mix. I suspect that a much better example could have been presented to make the case.

Reply

Juampy

Added a jQuery example

I have just added a link to a jQuery example in the article. You can find it at https://gist.github.com/juampy72/7609085.

The main problem I find with this jQuery implementation is the lack of separation between logic and markup. This makes the code more verbose; having to define longer ids for each form element and then using long selectors to set or obtain their values.

AngularJS also takes care of logging errors (console.log() may not be available in some browsers). In JQuery, using JSONP, if the request fails, you seem to have to chance to catch the error (or at least I was not able to while writing the example).

Reply

Tristan

Angular vs Backbone

I've used angular myself for simplifying javascript functionality in Drupal projects. I just learned though that Drupal 8 will be shipping with Backbone. It makes me wonder if we should start getting used to the incumbent Backbone instead of the edgy Angular. What are your thoughts? Could they conflict?

Reply

David Corbacho

Great article!

Great article again Juampy,

The main idea of the article is how useful can be to decouple dynamic data from Drupal rendering.. so we can take advantage of Cache, and in the other hand, the power of libraries like AngularJS

The example is just that, an example. Probably is overkill to use a full framework for such a simple form. But in the moment you start to add more data, UI dependencies, events, etc, you will appreciate to have a JS framework helping you.

To complete the article I created jsfiddles with your examples, and a Backbone.js one, for comparison:

AngularJS: http://jsfiddle.net/corbacho/RPaNa/

jQuery: http://jsfiddle.net/corbacho/zEsJD/1/

Backbone http://jsfiddle.net/corbacho/Td6v6/

Reply

Juampy

Thanks for gathering those up

Thanks for gathering those up David! I totally agree with you that this article is just an example to encourage people to consider a different approach.

Just a note: AngularJS has a very small footprint and that has let us use it on projects along with many other libraries without any issues.

Reply

Paul Mackay

Saving data back to Drupal?

Hi, I'm looking at an Angular/Drupal integration, but I need to save some data created by the Angular app back to Drupal, e.g. to create a new node. Do you have any suggestions of what would be the best approach for doing that?

Reply

Juampy

Open or secure your API

Hello Paul,

If you want to alter the data in Drupal from your AngularJS application then you either need to open up this node creation to the general public, or you need a mechanism to authenticate users. You could implement cookie-based authentication along with something like Drupal Services module or RESTful Web Services module (which supports HTTP Basic authentication). Alternatively, you could implement OAuth three-legged-authentication using OAuth module.

Cookie-based authentication means implementing a login form in your application that sends the user credentials to Drupal, which returns a session id if the authentication was successful. Once you get the session id you can add it into the header of a request that, for example, creates a node on the backend Drupal website.

Using OAuth three-legged-authentication means setting up OAuth module as an authentication server so when the user clicks Login in your AngularJS application it is redirected to Drupal to authenticate himself and, if successful, returns to the AngularJS application with an OAuth token which will be used in the following request to create content.

The biggest concern here is security. Make sure you have appropriate restrictions on who can and cannot perform these operations and also use HTTPS protocol.

Reply

Paul Mackay

reusing the session?

Thanks Juampy! If the Angular code is just one part of the Drupal page, and the user is already logged in, can the Angular code get hold of and send back the session ID already created, without needing a separate login mechanism via REST or similar?

Reply

Juampy

Session cookie not readable

Hello Paul,

Drupal uses HttpOnly flag when setting the cookie. Most browsers support this setting, which makes the session cookie to be unreadable by scripts. Is it a safety measure, since otherwise not only your code, but also attacking scripts, could hijack your session cookie and use it for their own benefit.

It seems that you can enable the cookie to be readable by adding ini_set('session.cookie_httponly', '1'); to your settings.php. Note that this opens a security whole though.

An alternative would be to set a token when you are authenticated in the JavaScript Drupal.settings object so that AngularJS could use it to authenticate requests.

More info on HttpOnly at https://www.owasp.org/index.php/HTTPOnly

Reply

James Wilson

data-ng-app attribute placement

Is the data-ng-app attribute required to be on the HTML tag? It seems that if you have multiple unrelated mini angular apps on one page, this model wont really work.

I wonder if you could move the attribute to a wrapper div for the specific block?

Reply

Juampy

Only one data-ng-app per HTML document

Hi James!

As per the documentation, only one ng-app directive can be used per HTML document. See https://docs.angularjs.org/api/ng/directive/ngApp.

The way we normally do it is by implementing different AngularJS modules. Each of those modules may have a controller which defines the $scope for that piece of HTML.

The example in the article defines a module with its controller. You can define as many of these as you need and they can be totally independent one from another.

Reply

James Wilson

You can define as many of

You can define as many of these [modules with their controlers] as you need and they can be totally independent one from another.

Totally independent? Don't they all need to know about and reference the string "myapp"? This is the fundamental problem that I see about distributing AngularJS plug-n-play modules.

The documentation does mention that you can have multiple applications but you cannot auto-bootstrap them (you have to call angular.bootstrap which comes with its own tradeoffs) and that you cannot nest them.

Reply

Add Your Comment