Lesson 4: Sculpting and Organizing your Application

In this Lesson, we will cover the following recipes:

  • Manually bootstrapping an application
  • Using safe $apply
  • Application file and module organization
  • Hiding AngularJS from the user
  • Managing application templates
  • The "Controller as" syntax

Introduction

In this Lesson, you will discover strategies to keep your application clean—visually, structurally, and organizationally.

Manually bootstrapping an application

When initializing an AngularJS application, very frequently you will allow the framework to do it transparently with the ng-app directive. When attached to a DOM node, the application will be automatically initialized upon the DOMContentLoaded event, or when the framework script is evaluated and the document.readyState === 'complete ' statement becomes true. The application parses the DOM for the ng-app directive, which becomes the root element of the application. It will then begin initializing itself and compiling the application template. However, in some scenarios, you will want more control over when this initialization occurs, and AngularJS provides you with the ability to do this with angular.bootstrap(). Some examples of this include the following:

  • Your application uses script loaders
  • You want to modify the template before AngularJS begins compilation
  • You want to use multiple AngularJS applications on the same page

Getting ready

When manually bootstrapping, the application will no longer use the ng-app directive. Suppose that this is your application template:

(index.html)

<!doctype html>
<html>
  <body>
    <div ng-controller="Ctrl">
      {{ mydata }}
    </div>
    <script src="angular.js"></script>
    <script src="app.js"></script>
  </body>
</html>

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope) {
  $scope.mydata = 'Some scope data';
});

How to do it…

The AngularJS initialization needs to be triggered by an event after the angular.js file is loaded, and it must be directed to a DOM element to be used as the root of the application. This can be accomplished in the following way:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope) {
  $scope.mydata = 'Some scope data';
});

angular.element(document).ready(function() {
  angular.bootstrap(document, ['myApp']);
});

How it works…

The angular.bootstrap() method is used to link an existing application module to the designated DOM root node. In this example, the jqLite ready() method is passed a callback, which indicates that the browser's document object should be used as the root node of the myApp application module. If you were to use ng-app to auto-bootstrap, the following would roughly be the equivalent:

(index.html)

<!doctype html>
<html ng-app="myApp">
  <body>
    <div ng-controller="Ctrl">
      {{ mydata }}
    </div>
    <script src="angular.js"></script>
    <script src="app.js"></script>
  </body>
</html>

There's more…

By no means are you required to use the <html> element as the root of your application. You can just as easily attach the application to an inner DOM element if your application only needed to manage a subset of the DOM. This can be done as follows:

(index.html)

<!doctype html>
<html ng-app="myApp">
  <body>
    <div id="child">
      <div ng-controller="Ctrl">
        {{ mydata }}
      </div>
    </div>
    <script src="angular.js"></script>
    <script src="app.js"></script>
  </body>
</html>

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope) {
  $scope.mydata = 'Some scope data';
});

angular.element(document).ready(function() {
  angular.bootstrap(document.getElementById('child'), ['myApp']);
});

Using safe $apply

In the course of developing AngularJS applications, you will become very familiar with $apply() and its implications. The $apply() function cannot be invoked while the $apply() phase is already in progress without causing AngularJS to raise an exception. While in simpler applications, this problem can be solved by being careful and methodical about where you invoke $apply(); however, this becomes increasingly more difficult when applications incorporate third-party extensions with high DOM event density. The resulting problem is one where the necessity of invoking $apply is indeterminate.

As it is entirely possible to ascertain the state of the application when $apply() might need to be invoked, you can create a wrapper for $apply() to ascertain the state of the application, and conditionally invoke $apply() only when not in the $apply phase, essentially creating an idempotent $apply() method.

Note

This recipe contains content that the AngularJS wiki considers an anti-pattern, but it proffers an interesting discussion on the application life cycle as well as architecting scope utilities. As consolation, it includes a solution that is more idiomatic.

Getting ready

Suppose that this is your application:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="increment()">Increment</button>
    {{ val }}
  </div>
</div>

(app.js)

angular.module('myApp',[])
.controller('MainController', function($scope) {
  $scope.val = 0;

  $scope.increment = function() {
    $scope.val++;
  };

  setInterval(function() {
    $scope.increment();
  }, 1000);
});

Note

AngularJS has its own $interval service that would ameliorate the problem with this code, but this recipe is trying to demonstrate a scenario where safeApply() might come in handy.

How to do it…

In this example, the use of setInterval() means that a DOM event is occurring and AngularJS is not paying attention to it or what it does. The model is correctly being modified, but AngularJS's data binding is not propagating that change to the view. The button click, however, is using a directive that starts the $apply phase. This would be fine; however, as it presently exists, clicking the button will update the DOM, but the setInterval() callback will not.

Worse yet, incorporating a call to $scope.$apply() inside the increment() method does not solve the problem. This is because when the button is clicked, the method will attempt to invoke $apply() while already in the $apply phase, which as mentioned before, will cause an exception to be raised. The setInterval() callback, however, will function properly.

The ideal solution is one where you are able to reuse the same method for both events, but $apply() will be conditionally invoked only when it is needed. The most trivial and straightforward method of achieving this is to attach a safeApply() method to the parent controller scope of the application and let inheritance propagate it throughout your application. This can be done as follows:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope) {
  $scope.safeApply = function (func) {
    var currentPhase = this.$root.$$phase;

    // determine if already in $apply/$digest phase
    if (currentPhase === '$apply' || 
      currentPhase === '$digest') {
      // already inside $apply/$digest phase

      // if safeApply() was passed a function, invoke it
      if (typeof func === 'function') {
        func();
      }
    } else {
      // not inside $apply/$digest phase, safe to invoke $apply
      this.$apply(func);
    }
  };

  $scope.val = 0;

  // method that may or may not be called from somewhere
  // that will not trigger a $digest
  $scope.increment = function () {
    $scope.val++;
    $scope.safeApply();
  };

  // application component that modifies the model without
  // triggering a $digest
  setInterval(function () {
    $scope.increment();
  }, 1000);
});

How it works…

The current phase of the application can be determined by reading the $$phase attribute of the root scope of the application. If it is either in the $apply or $digest phase, it should not invoke $apply(). The reason for this is that $scope.$digest() is the actual method that will check to see whether any binding values have changed, but this should only be called after the non-AngularJS events have occurred. The $scope.$apply() method does this for you, and it will invoke $digest() only after evaluating any function passed to it. Thus, inside the safeApply() method, it should only invoke $apply() if the application is not in either of these phases.

There's more…

The preceding example will work fine as long as all scopes that want to use safeApply() inherit from the controller scope on which it is defined. Even so, controllers are initialized relatively late in the application's bootstrap process, so safeApply() cannot be invoked until this point. On top of this, defining something like safeApply() inside a controller introduces a bit of code smell, as you would ideally like a method of this persuasion to be implicitly available throughout the entire application without relegating it to a specific controller.

A much more robust way of doing this is to decorate $rootScope of the application with the method during the config phase. This ensures that it will be available to any services, controllers, or directives that try to use it. This can be accomplished in the following fashion:

(app.js)

angular.module('myApp', [])
.config(function($provide) {
  // define decorator for $rootScope service
  return $provide.decorator('$rootScope', function($delegate) {
    // $delegate acts as the $rootScope instance
    $delegate.safeApply = function(func) {
      var currentPhase = $delegate.$$phase;
      
      // determine if already in $apply/$digest phase
      if (currentPhase === "$apply" || 
        currentPhase === "$digest") {
        // already inside $apply/$digest phase
        
        // if safeApply() was passed a function, invoke it
        if (typeof func === 'function') {
          func();
        }
      }
      else {
        // not inside $apply/$digest phase, 
        // safe to invoke $apply
        $delegate.$apply(func);
      }
    };
    return $delegate;
  });
})
.controller('Ctrl', function ($scope) {
  $scope.val = 0;
  
  // method that may or may not be called from somewhere
  // that will not trigger a $digest
  $scope.increment = function () {
    $scope.val++;
    $scope.safeApply();
  };
  
  // application component that modifies the model without
  // triggering a $digest
  setInterval(function () {
    $scope.increment();
  }, 1000);
});

Anti-pattern awareness

The AngularJS wiki notes that if your application needs to use a construct such as safeApply(), then the location where you are invoking $scope.$apply() isn't high enough in the call stack. This is true, and if you can avoid using safeApply(), you should do so. That being said, it is easy to think up a number of scenarios similar to this recipe's example where using safeApply() allows your code to remain DRY and concise, and for smaller applications, perhaps this is acceptable.

By the same token, the rigorous developer will not be satisfied with this and will desire an idiomatic solution to this problem aside from laborious code refactoring. One solution is to use $timeout, as shown here:

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function ($scope, $timeout) {
  $scope.val = 0;
  
  // method that may or may not be called from somewhere
  // that will not trigger a $digest
  $scope.increment = function () {
    // wraps model modification in $timeout promise
    $timeout(function() {
      $scope.val++;
    });
  };
  
  // application component that modifies the model without
  // triggering a $digest
  setInterval(function () {
    $scope.increment();
  }, 1000);
});

The $timeout wrapper is the AngularJS wrapper for window.setTimeout. What this does is effectively schedule the model modification inside a promise that will be resolved as soon as possible and when $apply can be invoked without consequence. In most cases, this solution is acceptable as long as the deferred $apply phase does not affect other portions of the application.

Application file and module organization

Few things are less enjoyable than working on a project where the organization of the application files and modules is garbage, especially if the application is written by people other than you. Keeping your application file tree and module hierarchy clean and tidy will save you and whoever is reading and using your code lots of time in the long run.

Getting ready

Assume that an application you are working on is a generic e-commerce site, with many users who can view and purchase products, leave reviews, and so on.

How to do it…

There are several guidelines that can be followed to yield extremely tight and clean applications that are able to scale without bloating.

One module, one file, and one name

This might seem obvious, but the benefits of following the one module, one file, and one name approach are plentiful:

  • Keep only one module per file. A module can be extended in other files in the subfiles and subdirectories as necessary, but angular.module('my-module') should only ever appear once. A file should not contain all or part of the two different modules.
  • Name your files after your modules. It should be easy to figure out what to expect when opening inventory-controller.js.
  • Module names should reflect the hierarchy in which it exists. The module in /inventory/inventory-controller.js should reflect its location in the hierarchy by being named something along the lines of inventory.controller.

Keep your related files close, keep your unit tests closer

Proper locality and organization of test files is not always obvious. Rigorously following this style guide is not mandatory, but choosing a unified naming and organization convention will save you a lot of headaches later on. This approach entails the following:

  • Name your unit test files by appending _test to whatever module file it is testing. The inventory-controller.js module will have its unit tests located in inventory-controller_test.js.
  • Keep unit tests in the same folder as the JS file they are testing. This will encourage you to write your tests as you develop the application. Additionally, you won't need to spend time mirroring your test directory structure to that of your application directory (see Lesson 6, Testing in AngularJS, for more information on testing procedures).

Group by feature, not by component type

Applications that group by component type (all directives in one place and all controllers in another) will scale poorly. The file and module locality should reflect that which appears in AngularJS dependencies. This includes the following:

  • Grouping by feature allows your file and module structure to imitate how the application code is connected. As the application begins to scale, it is cleaner and makes more sense for code that is more closely related in execution to have matching spatial locality.
  • Feature grouping also allows nested directories of functionality within larger features.

Don't fight reusability

Some parts of your application will be used almost everywhere and some parts will only be used once. Your application structure should reflect this. This approach includes the following:

  • Keep common unspecialized components that are used throughout the application inside a components/ directory. This directory can also hold common asset files and other shared application pieces.
  • Directives, services, and filters are all application components that can potentially see a lot of reuse. Don't hesitate to house them in the components/ directory if it makes sense to do so.

An example directory structure

With the tips mentioned in the preceding section, the e-commerce application will look something like this:

ng-commerce/
  index.html
  app.js
  app-controller.js
  app-controller_test.js
  components/
    login/
      login.js
      login-controller.js
      login-controller_test.js
      login-directive.js
      login-directive_test.js
      login.css
      login.tpl.html
    search/
      search.js
      search-directive.js
      search-directive_test.js
      search-filter.js
      search-filter_test.js
      search.css
      search.tpl.html
  shopping-cart/
    checkout/
      checkout.js
      checkout-conroller.js
      checkout-controller_test.js
      checkout-directive.js
      checkout-directive_test.js
      checkout.tpl.html
      checkout.css
    shopping-cart.js
    shopping-cart-controller.js
    shopping-cart-controller_test.js
    shopping-cart.tpl.html
    shopping-cart.css

The app.js file is the top-level configuration file, complete with route definitions and initialization logic. JS files matching their directory names are the combinatorial files that bind all the directory modules together.

CSS files provide styling that is only used by that component in that directory. Templates also follow this convention.

Hiding AngularJS from the user

As unique and elegant as AngularJS is, the reality of the situation is that it is a framework that lives inside asynchronously executed client-side code, and this requires some considerations. One of these considerations is the first-time delivery initialization latency. Especially when your application JS files are located at the end of the page, you might experience a phenomenon called "template flashing," where the uncompiled template is presented to the user before AngularJS bootstraps and compiles the page. This can be elegantly prevented using ng-cloak.

Getting ready

Suppose that this is your application:

(index.html)

<body>
  {{ youShouldntSeeThisBecauseItIsUndefined }}
</body>

How to do it…

The solution is to simply declare sections of the DOM that the browser should treat as hidden until AngularJS tells it otherwise. This can be accomplished with the ng-cloak directive, as follows:

(app.css)

/* this css rule is provided in the angular.js file, but 
if AngularJS is not included in <head>, you must
define this style yourself */

[ng:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
  display: none !important;
}

(index.html)

<body ng-cloak>
  {{ youShouldntSeeThisBecauseItIsUndefined }}
</body>

How it works…

Any section with ng-cloak initially applied to it will be hidden by the browser. AngularJS will delete the ng-cloak directive when it begins to compile the application template, so the page will only be revealed once compilation is complete, effectively shielding the user from the uncompiled template. In this case, as the entire <body> element has the ng-cloak directive, the user will be presented with a blank page until AngularJS is initialized and compiles the page.

There's more…

It might not behoove you to cloak the entire application until it's ready. First, if you only need to compile a subset or subsets of a page, you should take advantage of that by compartmentalizing ng-cloak to those sections. Often, it's better to present the user with something while the page is being assembled than with a blank screen. Second, breaking ng-cloak apart into multiple locations will allow the page to progressively render each component it must compile. This will probably give the feeling of a faster load as you are presenting compiled pieces of the view as they become available instead of waiting for everything to be ready.

Managing application templates

As is to be expected with a single-page application, you will be managing a large number of templates in your application. AngularJS has several template management solutions baked into it, which offer a range of ways for your application to handle template delivery.

Getting ready

Suppose you are using the following template in your application:

<div class="btn-group">
  #{{ player.number }} {{ player.name }}
</div>

The content of the template is unimportant; it is merely to demonstrate that this template has HTML and uncompiled AngularJS content inside it.

Additionally, assume you have the following directive that is trying to use the preceding template:

(app.js)

angular.module('myApp', [])
.directive('playerBox', function() {
  return {
    link: function(scope) {
      scope.player = {
        name: 'Jimmy Butler',
        number: 21
      };
    }
  };
});

The top-level template will look as follows:

(index.html)

<div ng-app="myApp">
  <player-box></player-box>
</div>

How to do it…

There are four primary ways to provide the directive with the template's HTML. All of these will feed the template into $templateCache, which is where the directive and other components tasked with locating a template will search first.

The string template

AngularJS is capable of generating a template from a string of uncompiled HTML. This can be accomplished as follows:

(app.js)

angular.module('myApp', [])
.directive('playerBox', function() {
  return {
    template: '<div>' +
              '  #{{ player.number }} {{ player.name }}' + 
              '</div>',
    link: function(scope) {
      scope.player = {
        name: 'Jimmy Butler',
        number: 21
      };
    }
  };
});

Remote server templates

When the component cannot find a template in $templateCache, it will make a request to the corresponding location on the server. This template will then receive an entry in $templateCache, which can be used as follows:

(app.js)

angular.module('myApp', [])
.directive('playerBox', function() {
  return {
    // will attempt to acquire the template at this relative URL
    templateUrl: '/static/js/templates/player-box.html',
    link: function(scope) {
      scope.player = {
        name: 'Jimmy Butler',
        number: 21
      };
    }
  };
});

On the server, your file directory structure will look something like the following:

yourApp/
  static/
    js/
      templates/
        player-box.html

Inline templates using ng-template

It is also possible to serve and register the templates along with another template. HTML inside <script> tags with type="text/ng-template" and the id attribute set to the key for $templateCache will be registered and available in your application. This can be done as follows:

(app.js)

angular.module('myApp', [])
.directive('playerBox', function() {
  return {
    templateUrl: 'player-box.html',
    link: function(scope) {
      scope.player = {
        name: 'Jimmy Butler',
        number: 21
      };
    }
  };
});

(index.html)

<div ng-app="myApp">
  <player-box></player-box>
  
  <script type="text/ng-template" id="player-box.html">
    <div>
      #{{ player.number }} {{ player.name }}
    </div>
  </script>
</div>

Pre-defined templates in the cache

Even cleaner is the ability to directly insert your templates into $templateCache on application startup. This can be done as follows:

(app.js)

angular.module('myApp', [])
.run(function($templateCache) {
  $templateCache.put(
    // the template key
    'player-box.html',
    // the template markup
    '<div>' +
    '  #{{ player.number }} {{ player.name }}' +
    '</div>'
  );
})
.directive('playerBox', function() {
  return {
    templateUrl: 'player-box.html',
    link: function(scope) {
      scope.player = {
        name: 'Jimmy Butler',
        number: 21
      };
    }
  };
});

How it works…

All these denominations of template definitions are different flavors of the same thing: uncompiled templates are accumulated and served from within $templateCache. The only real decision to be made is how you want it to affect your development flow and where you want to expose the latency.

Accessing the templates from a remote server ensures that you aren't delivering content to the user that they won't need, but when different pieces of the application are rendering, they will all need to generate requests for templates from the server. This can make your application sluggish at times. On the other hand, delivering all the templates with the initial application load can slow things down quite a bit, so it's important to make informed decisions on which part of your application flow is more latency-tolerant.

There's more…

The last method of defining templates is provided in a popular Grunt extension, called grunt-angular-templates. During the application build, this extension will automatically locate your templates and interpolate them into your index.html file as JavaScript string templates, registering them in $templateCache. Managing your application with build tools such as Grunt has huge and obvious benefits, and this recipe is no exception.

The "Controller as" syntax

AngularJS 1.2 introduced the ability to namespace your controller methods using the "controller as" syntax. This allows you to abstract $scope in controllers and provide more contextual information in the template.

Getting ready

Suppose you had a simple application set up as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    {{ data }}
  </div>
</div>

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function($scope) {
  $scope.data = "This is string data";
});

How to do it…

The simplest way to take advantage of the "controller as" syntax is inside the ng-controller directive in a template. This allows you to namespace pieces of data in the view, which should feel good to you as more declarative views are the AngularJS way. The initial example can be refactored to appear as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl as MyCtrl">
    {{ MyCtrl.data }}
  </div>
</div>

(app.js)

angular.module('myApp', [])
.controller('Ctrl', function() {
  this.data = "This is string data";
});

Note that there is no longer a need to inject $scope, as you are instead attaching the string attribute to the controller object.

This syntax can also be extended for use in directives. Suppose the application was retooled to exist as follows:

(index.html)

<div ng-app="myApp">
  <foo-directive></foo-directive>
</div>

(app.js)

angular.module('myApp', [])
.directive('fooDirective', function() {
  return {
    restrict: 'E',
    template: '<div>{{ data }}</div>',
    controller: function($scope) {
      $scope.data = 'This is controller scope data';
    }
  };
});

This works, but the "controller as" syntactic sugar can be applied here to make the content of the directive template a little less ambiguous:

(app.js)

angular.module('myApp', [])
.directive('fooDirective', function() {
  return {
    restrict: 'E',
    template: '<div>{{ fooController.data }}</div>',
    controller: function() {
      this.data = 'This is controller data';
    },
    controllerAs: 'fooController'
  } 
});

How it works…

Using the "controller as" syntax allows you to directly reference the controller object within the template. By doing this, you are able to assign attributes to the controller object itself rather than to $scope.

There's more…

There are a couple of main benefits of using this style, which are as follows:

  • You get more information in the view. By using this syntax, you are able to directly infer the source of the object from only the template, which is something you could not do before.
  • You are able to define directive controllers anonymously and define them where you choose. Being able to rebrand a function object in a directive allows a lot of flexibility in the application structure and locality of definition.
  • Testing is easier. Controllers defined in this way by nature are easier to set up, as injecting $scope into controllers means that unit tests need some boilerplate initialization.
There's more…
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.4.44