Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
AngularJS Web application development Cookbook

You're reading from   AngularJS Web application development Cookbook Over 90 hands-on recipes to architect performant applications and implement best practices in AngularJS

Arrow left icon
Product type Paperback
Published in Dec 2014
Publisher Packt
ISBN-13 9781783283354
Length 346 pages
Edition 1st Edition
Arrow right icon
Author (1):
Arrow left icon
Matthew Frisbie Matthew Frisbie
Author Profile Icon Matthew Frisbie
Matthew Frisbie
Arrow right icon
View More author details
Toc

Table of Contents (12) Chapters Close

Preface 1. Maximizing AngularJS Directives 2. Expanding Your Toolkit with Filters and Service Types FREE CHAPTER 3. AngularJS Animations 4. Sculpting and Organizing your Application 5. Working with the Scope and Model 6. Testing in AngularJS 7. Screaming Fast AngularJS 8. Promises 9. What's New in AngularJS 1.3 10. AngularJS Hacks Index

Recursive directives

The power of directives can also be effectively applied when consuming data in a more unwieldy format. Consider the case in which you have a JavaScript object that exists in some sort of recursive tree structure. The view that you will generate for this object will also reflect its recursive nature and will have nested HTML elements that match the underlying data structure.

Getting ready

Suppose you had a recursive data object in your controller as follows:

(app.js)

angular.module('myApp', [])
.controller('MainCtrl', function($scope) {
  $scope.data = {
    text: 'Primates',
    items: [
      {
        text: 'Anthropoidea',
        items: [
          {
            text: 'New World Anthropoids'
          },
          {
            text: 'Old World Anthropoids',
            items: [
              {
                text: 'Apes',
                items: [
                  {
                    text: 'Lesser Apes'
                  },
                  {
                    text: 'Greater Apes'
                  }
                ]
              },
              {
                text: 'Monkeys'
              }
            ]
          }
        ]
      },
      {
        text: 'Prosimii'
      }
    ]
  };
});

How to do it…

As you might imagine, iteratively constructing a view or only partially using directives to accomplish this will become extremely messy very quickly. Instead, it would be better if you were able to create a directive that would seamlessly break apart the data recursively, and define and render the sub-HTML fragments cleanly. By cleverly using directives and the $compile service, this exact directive functionality is possible.

The ideal directive in this scenario will be able to handle the recursive object without any additional parameters or outside assistance in parsing and rendering the object. So, in the main view, your directive will look something like this:

<recursive value="nestedObject"></recursive>

The directive is accepting an isolate scope = binding to the parent scope object, which will remain structurally identical as the directive descends through the recursive object.

The $compile service

You will need to inject the $compile service in order to make the recursive directive work. The reason for this is that each level of the directive can instantiate directives inside it and convert them from an uncompiled template to real DOM material.

The angular.element() method

The angular.element() method can be thought of as the jQuery $() equivalent. It accepts a string template or DOM fragment and returns a jqLite object that can be modified, inserted, or compiled for your purposes. If the jQuery library is present when the application is initialized, AngularJS will use that instead of jqLite. If you use the AngularJS template cache, retrieved templates will already exist as if you had called the angular.element() method on the template text.

The $templateCache

Inside a directive, it's possible to create a template using angular.element() and a string of HTML similar to an underscore.js template. However, it's completely unnecessary and quite unwieldy to use compared to AngularJS templates. When you declare a template and register it with AngularJS, it can be accessed through the injected $templateCache, which acts as a key-value store for your templates.

The recursive template is as follows:

<script type="text/ng-template" id="recursive.html">
  <span>{{ val.text }}</span>
  <button ng-click="delSubtree()">delete</button>
  <ul ng-if="isParent" style="margin-left:30px">
    <li ng-repeat="item in val.items">
      <tree val="item" parent-data="val.items"></tree>
    </li>
  </ul>
</script>

The <span> and <button> elements are present at each instance of a node, and they present the data at that node as well as an interface to the click event (which we will define in a moment) that will destroy it and all its children.

Following these, the conditional <ul> element renders only if the isParent flag is set in the scope, and it repeats through the items array, recursing the child data and creating new instances of the directive. Here, you can see the full template definition of the directive:

<tree val="item" parent-data="val.items"></tree>

Not only does the directive take a val attribute for the local node data, but you can also see its parent-data attribute, which is the point of scope indirection that allows the tree structure. To make more sense of this, examine the following directive code:

(app.js)

.directive('tree', function($compile, $templateCache) {
  return {
    restrict: 'E',
    scope: {
      val: '=',
      parentData: '='
    },
    link: function(scope, el, attrs) {
      scope.isParent = angular.isArray(scope.val.items)
      scope.delSubtree = function() {
        if(scope.parentData) {   
          scope.parentData.splice(
            scope.parentData.indexOf(scope.val),
            1
          );
        }
        scope.val={};
      }   
      el.replaceWith(
        $compile(
          $templateCache.get('recursive.html')
        )(scope)
      );    
    }
  };
});

With all of this, if you provide the recursive directive with the data object provided at the beginning of this recipe, it will result in the following (presented here without the auto-added AngularJS comments and directives):

(index.html – uncompiled)

<div ng-app="myApp">
  <div ng-controller="MainCtrl">
    <tree val="data"></tree>
  </div>
    
  <script type="text/ng-template" id="recursive.html">
    <span>{{ val.text }}</span>
    <button ng-click="deleteSubtree()">delete</button>
    <ul ng-if="isParent" style="margin-left:30px">
      <li ng-repeat="item in val.items">
        <tree val="item" parent-data="val.items"></tree>
      </li>
    </ul>
  </script>
</div>

The recursive nature of the directive templates enables nesting, and when compiled using the recursive data object located in the wrapping controller, it will compile into the following HTML:

(index.html - compiled)

<div ng-controller="MainController"> <span>Primates</span>
  <button ng-click="delSubtree()">delete</button>
  <ul ng-if="isParent" style="margin-left:30px">
    <li ng-repeat="item in val.items">
      <span>Anthropoidea</span>
      <button ng-click="delSubtree()">delete</button>
      <ul ng-if="isParent" style="margin-left:30px">
        <li ng-repeat="item in val.items">
          <span>New World Anthropoids</span>
          <button ng-click="delSubtree()">delete</button>
        </li>
        <li ng-repeat="item in val.items">
          <span>Old World Anthropoids</span>
          <button ng-click="delSubtree()">delete</button>
          <ul ng-if="isParent" style="margin-left:30px">
            <li ng-repeat="item in val.items">
              <span>Apes</span>
              <button ng-click="delSubtree()">delete</button>
              <ul ng-if="isParent" style="margin-left:30px">
                <li ng-repeat="item in val.items">
                  <span>Lesser Apes</span>
                  <button ng-click="delSubtree()">delete</button>
                </li>
                <li ng-repeat="item in val.items">
                  <span>Greater Apes</span>
                  <button ng-click="delSubtree()">delete</button>
                </li>
              </ul>
            </li>
            <li ng-repeat="item in val.items">
              <span>Monkeys</span>
              <button ng-click="delSubtree()">delete</button>
            </li>
          </ul>
        </li>
      </ul>
    </li>
    <li ng-repeat="item in val.items">
      <span>Prosimii</span>
      <button ng-click="delSubtree()">delete</button>
    </li>
  </ul>
</div>

How it works…

The definition of the isolate scope through the nested directives described in the previous section allows all or part of the recursive objects to be bound through parentData to the appropriate directive instance, all the while maintaining the nested connectedness afforded by the directive hierarchy. When a parent node is deleted, the lower directives are still bound to the data object and the removal propagates through cleanly.

The meatiest and most important part of this directive is, of course, the link function. Here, the link function determines whether the node has any children (which simply checks for the existence of an array in the local data node) and declares the deleting method, which simply removes the relevant portion from the recursive object and cleans up the local node. Up until this point, there haven't been any recursive calls, and there shouldn't need to be. If your directive is constructed correctly, AngularJS data binding and inherent template management will take care of the template cleanup for you. This, of course, leads into the final line of the link function, which is broken up here for readability:

el.replaceWith(
  $compile(
    $templateCache.get('recursive.html')
  )(scope)
);

Recall that in a link function, the second parameter is the jqLite-wrapped DOM object that the directive is linking—here, the <tree> element. This exposes to you a subset of jQuery object methods, including replaceWith(), which you will use here. The top-level instance of the directive will be replaced by the recursively-defined template, and this will carry down through the tree.

At this point, you should have an idea of how the recursive structure is coming together. The element parameter needs to be replaced with a recursively-compiled template, and for this, you will employ the $compile service. This service accepts a template as a parameter and returns a function that you will invoke with the current scope inside the directive's link function. The template is retrieved from $templateCache by the recursive.html key, and then it's compiled. When the compiler reaches the nested <tree> directive, the recursive directive is realized all the way down through the data in the recursive object.

There's more…

This recipe demonstrates the power of constructing a directive to convert a complex data object into a large DOM object. Relevant portions can be broken into individual templates, handled with distributed directive logic, and combined together in an elegant fashion to maximize modularity and reusability.

See also

  • The Optional nested directive controllers recipe covers vertical communication between directives through their controller objects
lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime