Using AngularJS with Drupal + Retrospective
I decided to start working on how to effectively meld these tools together, AngularJS and Drupal, after going through an overview course on AngularJS.
After spending the weekend going through a rather excellent (I thought) video overview course on AngularJS, my head was spinning with the possibilities when it came to building Drupal sites that were much more responsive in terms of loading and displaying content.
So, I decided to start working on some proof-of-concept modules that would help me better understand how to effectively meld these two tools together.
Using AngularJS to load information
I worked up my first example module where I use AngularJS to load information in a block on a page. The information is a listing of Articles, which I display as an unordered list.
Each list item has a button that, when clicked, opens the article title and teaser, along with a thumbnail of the article's image, in a dialog, with a link to the full node page at the bottom of the dialog.
My starting point was an example I found using my google-fu, which is published on SitePoint. While it is a decent working example that utilizes a few of the Angular concepts, I decided to refactor it to make it more practical as a working Drupal example and to also leverage Angular's capabilities more effectively.
So, I did the following:
- I cloned the author's sample code from Github, and installed it as a module on a sandbox Drupal site.
- I created an Article content type, and used the Devel module to generate some sample data with which to work.
- I also moved the ngDialog scripts, included with the author's sample code, into the module folder (in a more practical application I would place these in sites/all/libraries, but I wanted to keep all the code I was using together in one place for now).
- I tweaked his code to reference that location.
I had to make a few more minor tweaks to the author's code to use the specific field names that I chose when I created my content type, but other than it was pretty simple to get up and running.
The author's code generates a block that is available at admin/structure/blocks for use on the site. I placed this block so that it would display on the on the /user page on my sandbox site, verified that my article listing was showing up and that the Filter was working.
Now, time to refactor.
Want to learn more? Check: Promet Training Schedule.
Refactor 1: Install the Views Datasource module and create some Views returning JSON data
The first thing I did in preparation for refactoring the original module code was to install the Views Datasource module. This module has a submodule, Views JSON, that will allow you to turn any Views page into a RESTful service, returning JSON data at the endpoint. Cool!
Next, I constructed a couple of views which I am using to replace the author's not so elegant database queries. This allows me to leverage Drupal's built-in permissions as well as the power of Views to query my data and return my results.
I built two views: one pulling an entire list of all articles, and another that returned the contents of a single article using a contextual filter on the node ID.
The author had three database queries total in his module; however, I decided to leverage Angular's data binding and filtering capabilities to filter my data instead, so these two JSON feeds were all that were needed to replace the author's database queries.
Learn how to develop your own Drupal modules!
Refactor 2: Change the API callback to use my Views results
The author had written a function into this module, ang_node_api(), that took a single optional parameter.
This function is a page callback for a hook_menu() item and, based on the parameter passed in, runs one of three database queries that return node information that is used by the Angular app included with his code.
I refactored the code in this function to instead make a drupal_http_request() to the Views page URLs from my two views, which are serving as my RESTful endpoints.
I removed the author's "elseif" component of his "if" statement since my plan is to replace that completely with an AngularJS directive.
Now, I'm using json_decode() on the results returned by the drupal_http_request(), in my code currently, but a future exercise might be to write an Angular service to consume that data instead (an exercise for a later date).
Refactor 3: Change the Angular app code
Surprisingly, the Angular app code that the author originally included in his example code needed very little changing.
I did two things:
- I wrapped the code in an anonymous function to get it out of the global scope.
- I removed the callback to perform the search when the user types in the text to filter on the article title.
We will be setting up our template to leverage Angular for that search/filter capability instead.
Refactor 4: Change angular-listing.tpl.php to leverage Angular's data binding and filtering capabilities
My last refactoring was to change angular-listing.tpl.php to bind to our results data and filter our data via Angular instead of querying the database via Drupal.
I basically used ng-model to bind my input type to my list item (which is looped through and repeated using Angular's ng-repeat directive), and further changed the list items to order the list by node title, and to limit the displayed results to 150.
I also changed the dialog box to display the article image and added a link to the node page for the article after the display of the node body teaser.
That's it!
I placed my block on the /user page, cleared the cache, navigated to the user page and Wah Lah! There's my filterable article listing, complete with dialog popup (using Angular's ng-template directive and ngDialog).
I posted my module code out on GitHub. I created features containing the Article content type and the Views and included those as well so that you have everything if you are interested in downloading and playing with the building blocks I have so far.
Overall, I am excited about the way that Angular works when melded with Drupal, and I see so many possibilities for making client projects better.
Now, let's do a retrospective. Let's start with the template file.
A brief tour of the template file
The template file uses a number of "directives", which is a core feature of AngularJS. A directive is effectively a marker on a DOM element that Angular uses to attach a behavior to that element. Angular directives are prefixed with "ng-", so they are pretty easy to identify.
Our template file uses the following directives:
- ng-app
- ng-controller
- ng-model
- ng-cloak
- ng-repeat
- ng-click
- ng-template
- ng-src
ng-app
The ng-app directive is the mother of all directives. This directive tells AngularJS which part of the DOM it is managing. Everything inside the DOM element marked with this directive is under the purview of AngularJS. You will have one, and only one, "ng-app" directive on any given page.
ng-controller
In AngularJS, areas of a page are managed by JavaScript classes called controllers. The ng-controller directive allows you to specify which particular controller is managing a particular area. In our case, we have specified that the "ListController" is managing everything inside the first nested "div" in our template file. The code for that controller resides in our ang.js file.
ng-model
The ng-model directive creates a data binding between the DOM element it's attached to (in our case the "input" text field) and the specified element (in our case the "nodeFilter", which i'll cover under the "ng-repeat" directive.
ng-cloak
ng-cloak prevents the AngularJS template (in our case the the contents of angular-listing.tpl.php) from displaying in its raw state.
This is to prevent the "flickering" that occurs while the DOM element expressions wrapped in binding markup managed by AngularJS are being populated. It works in conjunction with CSS rules embedded in the main angular framework script.
ng-repeat
The ng-repeat directive instructs Angular to repeat this DOM element, in our case the "li" tag and its contents, for each item in the specified javascript array object (in our case "nodes"), substituting the specified property of each instance of "node" for our expressions, which are wrapped in binding markup designated by double curly brackets {{ }}.
We also have a few other features on this element specific to the ng-repeat directive, which are:
- filter: Works with the directive we specified earlier on our "input" tag, ng-model, to bind our input element to filter our results, listed here in our unordered list, by the specified input from the user. In our case, we are using this to filter down our results list based on the node title.
- orderBy: specifies an item to sort any results by (in our case, the node title).
- limitTo: specifies the number of results items we want displayed on the page.
A note here: even though we are displaying 150 items, we are able to filter the entire result set (in my case, 5000 nodes), returning the first 150 items that match our filter contents, and updating that result dynamically as you type in more characters in the filter input.
ng-click
ng-click attaches a behavior to an element that is triggered by a "click" event.
ng-template
ng-template specifies that the contents of the script tag where this is attached are to be loaded into the $templateCache for use by other directives (in our case, the ngDialog directive in our application script).
ng-src
The ng-src directive is used in place of the "src" attribute in an image tag to allow the use of AngularJS expressions (specified by {{ }}) within our "src" attribute).
That covers all of the Angular directives. You can look at the documentation (or use your google-fu) to find out more about each one specifically. Now for our Angular app code, which resides in ang.js.
ang.js
Our Angular application code is contained in ang.js, and is as follows: Our angular module, called "nodeListing" (note that this corresponds to our "ng-app" directive in our template file), contains two components: a "factory" class and a "controller" class.
Controllers are an integral part of an AngularJS project, since the precept of Angular is MVC/MVVM based. Controllers manage the interactions between the "Model" and the "View", and act as the manager for different components of the Angular application.
Controllers interact with the View through the use of the $scope object; both Views and Controllers have access to the $scope object. $scope is used to store data that is needed by the View to render output.
Fellow Drupalers, please note: the View we speak of here is not a Drupal View, as provided by the Views module, but an Angular View.
Factories (and Services and Providers) serve a special purpose within an Angular application. They follow the Singleton design pattern, in that only one instance of a specific Factory or Service or Provider exists per Angular application instance.
Their role is to store the more complex logic of the application, such as business rules and calculations, and to serve as or interact with data stores (Models) (so your ajax calls would typically reside in one of these).
Any task that might be re-usable throughout your application should be placed in a Factory or Service.
Tyler McGinnis has a blog post that has a very good explanation and illustration of the differences between Factories, Services and Providers. Please read up on it there, as he does a great job explaining it much better than I.
AngularJS provides a number of services out of the box; we use the $resource factory in our application. $resource provides an object that lets us easily interact with RESTful server-side data sources and is the tool we use to interact with our JSON output provided via the (Drupal) Views Datasource module and our custom (Drupal) views.
The $resource object is injected into our Node factory and handles our requests to our service endpoints. Our Controller, ListController, handles the interactions with our Angular View (template) and the interactions with our Factory object, Node.
ListController is responsible for taking the data requested from and returned by the Node factory and adds that data to the $scope object so that it is available to our Angular View.
ListController also handles launching our dialog box, by interacting with our dialog box template, loadedNodeTemplate, which has been embedded in our View template wrapped in script tags with the attribute 'type=text/ng-template' specified in it.
Our entire app script, with whitespace for readability, is 27 lines long.
How beautiful is that?
Looking for a Drupal development company for your website? Contact us today.
Other Insights & Resources you may like
Get our newsletter
Alright, so, software ate the world. That happened. Technology is now at the heart of every modern company, and as far as we can tell that isn’t changing. That’s the sitch. Our job is to make it more human.