Wrapping AngularJS modules in Drupal CTools plugins

by Joe FenderMarch 18, 2015

A crucial requirement for the MSNBC's recent online TV revamp was a highly interactive user interface that didn't interfere with the site's all-important video player. In our previous work on the main MSNBC site, we had used AngularJS to decouple complex front-end UI behaviors from the Drupal-powered backend. With that experience, using AngularJS for the new requirements was a no-brainer.

MSNBC video

The features we needed to build all followed a similar pattern: we needed to pass data from Drupal to AngularJS, then process and present it on the front-end. Since the site's primary layout was implemented using the Panels module, we decided to implement these small units of AngularJS functionality as CTools plugins. That would allow us to easily integrate the AngularJS features into existing layouts. In this article, I'll take you through the method that we used: creating a CTools plugin, an AngularJS module, and finally an API resource for the AngularJS module to interact with.

For tips on getting started with AngularJS check out Move logic to the front end with AngularJS by Juampy. I also recommend reading about the AngularJS Style Guide as that is what we follow for our naming conventions. For the source code used in this article, check out this repository on Github.

Creating a CTools Plugin

There are multiple pages that make up MSNBC online TV such as msnbcTV, shift and on demand video. Although they may look similar, they each offer very different functionality and content. However, the one thing that all the pages have in common is that they are powered by CTools plugins and Panels. If you’re interested in how to create CTools plugins, check out this article by Ivan Zugec.

For demonstration purposes, let’s pretend that we want to display a message when a video becomes out of date. Here is what our CTools plugin would look like:


$plugin = array(
  'title' => t('Old Video Warning'),
  'description' => t('Displays a warning message when the current video is out of date.'),
  'single' => TRUE,
  'render callback' => 'mymodule_old_video_content_type_render',
  'required context' => new ctools_context_required(t('Node'), 'node'),
  'category' => t('My Module'),
);

function mymodule_old_video_content_type_render($subtype, $conf, $args, $context) {
  $settings = array(
    'nid' => $context->data->nid,
    'changed' => $context->data->changed,
  );
  drupal_add_js(array('MyModuleOldVideo' => $settings), array('type' => 'setting'));
  drupal_add_js(drupal_get_path('module', 'mymodule') . '/plugins/content_types/old_video.js');

  $block = new stdClass();
  $block->content = theme('mymodule_old_video', array('node' => $context->data));
  return $block;
}
  

We’ve declared the Old Video Warning plugin which requires a Node context. Once you’ve created the plugin, head over to the CTools Page Manager admin UI, located at admin/structure/pages, enable the Node template page and add the pane to the content area.

Implementing the AngularJS Module

In the render function of our plugin we include some Drupal settings using drupal_add_js() which will allow us to access them from JavaScript. We also included our custom JS file, old_video.js:


if (typeof angular !== 'undefined') {
  angular.module('MyModuleOldVideo', ['ngResource'])
  .factory('videoFactory', ['$resource', function($resource) {
    return $resource('/d7/api/1.0/videos.json', { nid: '@nid' });
  }])
  .controller('MyModuleOldVideoController', ['$scope', '$interval', 'videoFactory', function($scope, $interval, videoFactory) {
    var nid = Drupal.settings.MyModuleOldVideo.nid;
    var changed = Drupal.settings.MyModuleOldVideo.changed;

    $interval(function() {
      videoFactory.get({ nid: nid }, function(data) {
        if (data.videos.length) {
          var video = data.videos[0].video;
          if (video.changed > changed) {
            changed = video.changed;
            $scope.showMessage = true;
          }
        }
      });
    }, 30000);
  }]);
}
  

This script declares the MyModuleOldVideo AngularJS module. Make sure that you’re including AngularJS, bootstrapping your application and injecting this module for it to work.

Using a combination of ngResource and $interval, the MyModuleOldVideoController controller sends a GET request every 30 seconds to our API to fetch the latest video node information. We check to see if the latest node changed timestamp is newer than the original value and if so, set the showMessage scope boolean to true.

For all this to work, we need to know the current video node ID and changed timestamp on page load. Passing values through Drupal.settings is a great way to do this because Drupal handles variable conversion to JSON for us. On MSNBC, we make a point of not altering any values in Drupal.settings on the client-side as they are an indicator of the page state (or in this case, node state) on page load.

Here is the template file markup for our mymodule_old_video theme hook:


<div data-ng-controller="MyModuleOldVideoController">
  <h2 data-ng-show="showMessage" data-ng-cloak><?php print t('This video is out of date!'); ?></h2>
</div>
  

Create an API endpoint to expose nodes

The last piece of the puzzle is to set up the video API endpoint. On MSNBC, we use the CreateAPI module by our very own Sally Young. Simply install the module and add this hook to your custom module:


/**
 * Implements hook_createapi_content_types().
 */
function mymodule_createapi_content_types() {
  return array(
    'video' => array(
      'version' => '1.0',
      'path' => 'videos.json',
      'wrapper' => 'videos',
      'row' => 'video',
      'data' => array(
        'properties' => array(
          'nid' => 'nid',
          'title' => 'title',
          'changed' => 'changed',
        ),
        'path' => 'path',
      ),
      'filters' => array(
        'properties' => array(
          'nid' => 'nid',
        ),
      ),
    ),
  );
}
  

We’ve declared the videos.json path that will return video nodes in JSON format. You probably won’t have a video node content type so you’ll need to create that first or change the values in the hook above to match a content type you already have. Calling this endpoint will return something similar to the following:


{  
   "videos":[  
      {  
         "video":{  
            "nid":"4",
            "title":"My Video",
            "changed":"1419949322",
            "path":"node\/4"
         }
      }
   ]
}
  

Summary

Bob’s your uncle! Try it out by opening a video node page and in another tab edit and save that same node. Within 30 seconds you will see the message pop up in the original tab. Feel free to ask questions and discuss below in the comments section!

If you enjoyed reading my article and want to learn more about front-end technologies, check out Front-End Fundamentals of which I co-authored with Carwin Young.

newsletter-bot