Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

AngularJS in Drupal Apps

Parent Feed: 

Angular.js is the hot new thing right now for designing applications in the client. Well, it’s not so new anymore but is sure as hell still hot, especially now that it’s being used and backed by Google. It takes the idea of a JavaScript framework to a whole new level and provides a great basis for developing rich and dynamic apps that can run in the browser or as hybrid mobile apps.

logo_drupal

In this article I am going to show you a neat little way of using some of its magic within a Drupal 7 site. A simple piece of functionality but one that is enough to demonstrate how powerful Angular.js is and the potential use cases even within heavy server-side PHP frameworks such as Drupal. So what are we doing?

We are going to create a block that lists some node titles. Big whoop. However, these node titles are going to be loaded asynchronously using Angular.js and there will be a textfield above them to filter/search for nodes (also done asyncronously). As a bonus, we will also use a small open source Angular.js module that will allow us to view some of the node info in a dialog when we click on the titles.

So let’s get started. As usual, all the code we write in the tutorial can be found in this repository.

Ingredients

In order to mock this up, we will need the following:

  • A custom Drupal module
  • A Drupal hook_menu() implementation to create an endpoint for querying nodes
  • A Drupal theme function that uses a template file to render our markup
  • A custom Drupal block to call the theme function and place the markup where we want
  • A small Angular.js app
  • For the bonus part, the ngDialog Angular module

The module

Let us get started with creating a custom module called Ang. As usual, inside the modules/custom folder create an ang.info file:

name = Ang
description = Angular.js example on a Drupal 7 site.
core = 7.x

…and an ang.module file that will contain most of our Drupal related code. Inside this file (don’t forget the opening <?php tag), we can start with the hook_menu() implementation:

/**
 * Implements hook_menu().
 */
function ang_menu() {
  $items = array();

  $items['api/node'] = array(
    'access arguments' => array('access content'),
    'page callback'     => 'ang_node_api',
    'page arguments' => array(2),
    'delivery callback' => 'drupal_json_output'
  );

  return $items;
}
/**
 * API callback to return nodes in JSON format
 *
 * @param $param
 * @return array
 */
function ang_node_api($param) {

  // If passed param is node id
  if ($param && is_numeric($param)) {
    $node = node_load($param);
    return array(
      'nid' => $param,
      'uid' => $node->uid,
      'title' => check_plain($node->title),
      'body' => $node->body[LANGUAGE_NONE][0]['value'],
    );
  }
  // If passed param is text value
  elseif ($param && !is_numeric($param)) {
    $nodes = db_query("SELECT nid, uid, title FROM {node} n JOIN {field_data_body} b ON n.nid = b.entity_id WHERE n.title LIKE :pattern ORDER BY n.created DESC LIMIT 5", array(':pattern' => '%' . db_like($param) . '%'))->fetchAll();
    return $nodes;
  }
  // If there is no passed param
  else {
    $nodes = db_query("SELECT nid, uid, title FROM {node} n JOIN {field_data_body} b ON n.nid = b.entity_id ORDER BY n.created DESC LIMIT 10")->fetchAll();
    return $nodes;
  }
}

In hook_menu() we declare a path (api/node) which can be accessed by anyone with permissions to view content and which will return JSON output created in the callback function ang_node_api(). The latter gets passed one argument, that is whatever is found in the URL after the path we declared: api/node/[some-extra-param]. We need this argument because of we want to achieve 3 things with this endpoint:

  1. return a list of 10 most recent nodes
  2. return a node with a certain id (api/node/5 for example)
  3. return all the nodes which have the passed parameter in their title (api/node/chocolate for example, where chocolate is part of one or more node titles)

And this is what happens in the second function. The parameter is being checked against three cases:

  • If it exists and it’s numeric, we load the respective node and return an array with some basic info form that node (remember, this will be in JSON format)
  • If it exists but it is not numeric, we perform a database query and return all the nodes whose titles contain that value
  • In any other case (which essentially means the lack of a parameter), we query the db and return the latest 10 nodes (just as an example)

Obviously this callback can be further improved and consolidated (error handling, etc), but for demonstration purposes, it will work just fine. Let’s now create a theme that uses a template file and a custom block that will render it:

/**
 * Implements hook_theme().
 */
function ang_theme($existing, $type, $theme, $path) {
  return array(
    'angular_listing' => array(
      'template' => 'angular-listing',
      'variables' => array()
    ),
  );
}

/**
 * Implements hook_block_info().
 */
function ang_block_info() {

  $blocks['angular_nodes'] = array(
    'info' => t('Node listing'),
  );

  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function ang_block_view($delta = '') {

  $block = array();

  switch ($delta) {
    case 'angular_nodes':
      $block['subject'] = t('Latest nodes');
      $block['content'] = array(
        '#theme' => 'angular_listing',
        '#attached' => array(
          'js' => array(
            'https://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.min.js',
            'https://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular-resource.js',
            drupal_get_path('module', 'ang') . '/lib/ngDialog/ngDialog.min.js',
            drupal_get_path('module', 'ang') . '/ang.js',
          ),
          'css' => array(
            drupal_get_path('module', 'ang') . '/lib/ngDialog/ngDialog.min.css',
            drupal_get_path('module', 'ang') . '/lib/ngDialog/ngDialog-theme-default.min.css',
          ),
        ),
      );
      break;
  }

  return $block;
}

/**
 * Implements template_preprocess_angular_listing().
 */
function ang_preprocess_angular_listing(&$vars) {
  // Can stay empty for now.
}

There are four simple functions here:

  1. Using hook_theme() we create our angular_listing theme that uses the angular-listing.tpl.php template file we will create soon.
  2. Inside the hook_block_info() we define our new block, the display of which is being controlled inside the next function.
  3. Using hook_block_view() we define the output of our block: a renderable array using the angular_listing theme and which has the respective javascript and css files attached. From the Google CDN we load the Angular.js library files, inside ang.js we will write our JavaScript logic and in the /lib/ngDialog folder we have the library for creating dialogs. It’s up to you to download the latter and place it in the module following the described structure. You can find the files either in the repository or on the library website.
  4. The last function is a template preprocessor for our template in order to make sure the variables are getting passed to it (even if we are actually not using any).

As you can see, this is standard boilerplate Drupal 7 code. Before enabling the module or trying out this code, let’s quickly create the template file so Drupal doesn’t error out. Inside a file called angular-listing.tpl.php, add the following:

        <div ng-app="nodeListing">
        
           <div ng-controller="ListController">
        
             <h3>Filter</h3>
             <input ng-model="search" ng-change="doSearch()">
        
              <ul>
                <li ng-repeat="node in nodes"><button ng-click="open(node.nid)">Open</button> {{ node.title }}</li>
              </ul>
        
             <script type="text/ng-template" id="loadedNodeTemplate">
             <h3>{{ loadedNode.title }}</h3>
             {{ loadedNode.body }}
             </script>
        
            </div>
        
        </div>

Here we have some simple HTML pimped up with Angular.js directives and expressions. Additionally, we have a <script> tag used by the ngDialog module as the template for the dialog. Before trying to explain this, let’s create also our ang.js file and add our javascript to it (since the two are so connected):

angular.module('nodeListing', ['ngResource', 'ngDialog'])

  // Factory for the ngResource service.
  .factory('Node', function($resource) {
    return $resource(Drupal.settings.basePath + 'api/node/:param', {}, {
      'search' : {method : 'GET', isArray : true}
    });
  })

  .controller('ListController', ['$scope', 'Node', 'ngDialog', function($scope, Node, ngDialog) {
    // Initial list of nodes.
    $scope.nodes = Node.query();

    // Callback for performing the search using a param from the textfield.
    $scope.doSearch = function() {
      $scope.nodes = Node.search({param: $scope.search});
    };

    // Callback to load the node info in the modal
    $scope.open = function(nid) {
      $scope.loadedNode = Node.get({param: nid});
      ngDialog.open({
        template: 'loadedNodeTemplate',
        scope: $scope
      });
    };

}]);

Alright. Now we have everything (make sure you also add the ngDialog files as requested in the #attached key of the renderable array we wrote above). You can enable the module and place the block somewhere prominent where you can see it. If all went well, you should get 10 node titles (if you have so many) and a search box above. Searching will make AJAX calls to the server to our endpoint and return other node titles. And clicking on them will open up a dialog with the node title and body on it. Sweet.

But let me explain what happens on the Angular.js side of things as well. First of all, we define an Angular.js app called nodeListing with the ngResource (the Angular.js service in charge communicating with the server) and ngDialog as its dependencies. This module is also declared in our template file as the main app, using the ng-app directive.

Inside this module, we create a factory for a new service called Node which returns a $resource. The latter is in fact a connection to our data on the server (the Drupal backend accessed through our endpoint). In addition to the default methods on it, we define another one called .search() that will make a GET request and return an array of results (we need a new one because the default .get() does not accept an array of results).

Below this factory, we define a controller called ListController (also declared in the template file using the ng-controller directive). This is our only controller and it’s scope will apply over all the template. There are a few things we do inside the controller:

  1. We load nodes from our resource using the query() method. We pass no parameters so we will get the latest 10 nodes on the site (if you remember our endpoint callback, the request will be made to /api/node). We attach the results to the scope in a variable called nodes. In our template, we loop through this array using the ng-repeat directive and list the node titles. Additionally, we create a button for each with an ng-click directive that triggers the callback open(node.nid) (more on this at point 3).
  2. Looking still at the template, above this listing, we have an input element whose value will be bound to the scope using the ng-model directive. But using the ng-change directive we call a function on the scope (doSearch()) every time a user types or removes something in that textfield. This function is defined inside the controller and is responsible for performing a search on our endpoint with the param the user has been typing in the textfield (the search variable). As the search is being performed, the results populate the template automatically.
  3. Lastly, for the the bonus part, we define the open() method which takes a node id as argument and requests the node from our endpoint. Pressing the button, this callback function opens the dialog that uses a template defined inside of the <script> tag with the id of loadedNodeTemplate and passes to it the current scope of the controller. And if we turn to the template file, we see that the dialog template simply outputs the title and the body of the node.

Conclusion

You can see for yourself the amount of code we wrote to accomplish this neat functionality. Most of it is actually boilerplate. A very fast node query block that delivers results asynchronously with all of its benefits. And if you know Angular.js, you can imagine the possibility of enhancing the Drupal experience further.

Now, are you interested to learn more about the love between Angular.js and Drupal? Would you have done anything differently? Let us know in the comments below!

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web