18 Common Mistakes Made by AngularJS Developers

Building sophisticated single-page applications requires front-end developers to possess robust software engineering skills. While CSS and HTML remain important, they are no longer the sole focus. Today’s front-end developers grapple with a wider range of responsibilities, including managing XHRs, implementing application logic (models, views, controllers), optimizing performance, crafting animations, defining styles and structure, ensuring SEO effectiveness, and integrating external services. All these elements converge to shape the User Experience (UX), which should always remain the top priority.

AngularJS, a powerful framework and the third most starred repository on GitHub, simplifies the development process. However, harnessing its full potential demands a deep understanding of its underlying concepts. For instance, AngularJS developers must be mindful of memory consumption as navigation no longer triggers a reset. This shift signifies a new era in web development, and embracing it is crucial.

Common AngularJS mistakes

Common Mistake #1: Accessing The Scope Through The DOM

When preparing your AngularJS application for production, several optimization tweaks are recommended. One such tweak involves disabling debug info.

The DebugInfoEnabled setting, enabled by default, permits scope access directly through DOM nodes. To experiment with this in the JavaScript console, select a DOM element and retrieve its scope using:

1
angular.element(document.body).scope()

This technique proves helpful even without utilizing jQuery’s CSS selectors. However, it should be restricted to the console. The reason being, setting $compileProvider.debugInfoEnabled to false renders the .scope() method unusable on DOM nodes, returning undefined.

Disabling debug info is just one of many recommended production optimizations.

Note: Scope access through the console remains possible even in production. Executing angular.reloadWithDebugInfo() in the console reinstates this functionality.

Common Mistake #2: Not Having a Dot In There

You’ve likely encountered the advice that omitting a dot from your ng-model is incorrect. This holds true, particularly when inheritance comes into play. AngularJS scopes adhere to JavaScript’s prototypal inheritance model, and nested scopes are commonplace. Directives like ngRepeat, ngIf, and ngController all create child scopes. Consequently, when resolving a model, the lookup traverses from the current scope up through each parent scope, culminating in $rootScope.

However, setting a new value introduces nuances depending on the model type being modified. For primitive models, the child scope generates a new model. Conversely, if the change targets an object’s property, the lookup ascends parent scopes, locates the referenced object, and modifies its property directly. This prevents the creation of a new model on the current scope, thus avoiding masking:

1
2
3
4
5
6
7
function MainController($scope) {
  $scope.foo = 1;
  $scope.bar = {innerProperty: 2};
}

angular.module('myApp', [])
.controller('MainController', MainController);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div ng-controller="MainController">
  <p>OUTER SCOPE:</p>
  <p>{{ foo }}</p>
  <p>{{ bar.innerProperty }}</p>
  <div ng-if="foo"> <!— ng-if creates a new scope —>
    <p>INNER SCOPE</p>
    <p>{{ foo }}</p>
    <p>{{ bar.innerProperty }}</p>
    <button ng-click="foo = 2">Set primitive</button>
    <button ng-click="bar.innerProperty = 3">Mutate object</button>
  </div>
</div>

Clicking the “Set primitive” button sets foo to 2 within the inner scope but leaves the outer scope’s foo untouched.

Clicking the “Change object” button modifies the bar property of the parent scope. Since the inner scope lacks a corresponding variable, no shadowing occurs, and both scopes display bar as 3.

Another approach involves leveraging the fact that every scope references its parent scopes and the root scope. Utilizing the $parent and $root objects grants direct access to the parent scope and $rootScope respectively, directly from the view. While potent, this method introduces ambiguity when targeting specific scopes up the chain. The controllerAs syntax offers a cleaner alternative for setting and accessing scope-specific properties.

Common Mistake #3: Not Using controllerAs Syntax

A more efficient and less confusing approach involves assigning models using a controller object instead of injecting $scope directly. This allows for model definitions like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function MainController($scope) {
  this.foo = 1;
  var that = this;
  var setBar = function () {
    // that.bar = {someProperty: 2};
    this.bar = {someProperty: 2};
  };
  setBar.call(this);
  // there are other conventions: 
  // var MC = this;
  // setBar.call(this); when using 'this' inside setBar()
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div>
  <p>OUTER SCOPE:</p>
  <p>{{ MC.foo }}</p>
  <p>{{ MC.bar.someProperty }}</p>
  <div ng-if="test1">
    <p>INNER SCOPE</p>
    <p>{{ MC.foo }}</p>
    <p>{{ MC.bar.someProperty }}</p>
    <button ng-click="MC.foo = 3">Change MC.foo</button>
    <button ng-click="MC.bar.someProperty = 5">Change MC.bar.someProperty</button>
  </div>
</div>

This approach significantly enhances clarity, especially within deeply nested scopes, as often seen with nested states.

However, the controllerAs syntax offers even more benefits.

Common Mistake #4: Not Fully Utilising The controllerAs Syntax

The exposure of the controller object comes with a few subtleties. Essentially, it’s an object residing on the controller’s scope, akin to any other model.

Watching a controller object’s property can be achieved by watching a function, although it’s not mandatory. For instance:

1
2
3
4
5
6
7
8
function MainController($scope) {
  this.title = 'Some title';
  $scope.$watch(angular.bind(this, function () {
    return this.title;
  }), function (newVal, oldVal) {
    // handle changes
  });
}

A simpler alternative exists:

1
2
3
4
5
6
function MainController($scope) {
  this.title = 'Some title';
  $scope.$watch('MC.title', function (newVal, oldVal) {
    // handle changes
  });
}

This approach enables accessing MC from a child controller further down the scope chain:

1
2
3
4
5
function NestedController($scope) {
  if ($scope.MC && $scope.MC.title === 'Some title') {
    $scope.MC.title = 'New title';
  }
}

Consistency in the chosen controllerAs acronym is vital. Let’s examine the different ways to set it. We’ve already encountered the first method:

1
2
3
<div ng-controller="MainController as MC">
</div>

However, with ui-router, this method becomes error-prone when defining controllers for states. In such cases, specifying the controller within the state configuration is preferred:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
angular.module('myApp', [])
.config(function ($stateProvider) {
  $stateProvider
  .state('main', {
    url: '/',
    controller: 'MainController as MC',
    templateUrl: '/path/to/template.html'
  })
}).
controller('MainController', function () {  });

Alternatively, annotations can be employed:

1
2
3
4
5
6
7
()
.state('main', {
  url: '/',
  controller: 'MainController',
  controllerAs: 'MC',
  templateUrl: '/path/to/template.html'
})

This annotation approach applies to directives as well:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function AnotherController() {
  this.text = 'abc';
}

function testForToptal() {
  return {
    controller: 'AnotherController as AC',
    template: '<p>{{ AC.text }}</p>'
  };
}

angular.module('myApp', [])
.controller('AnotherController', AnotherController)
.directive('testForToptal', testForToptal);

While the alternative annotation syntax remains valid, it lacks conciseness:

1
2
3
4
5
6
7
function testForToptal() {
  return {
    controller: 'AnotherController',
    controllerAs: 'AC',
    template: '<p>{{ AC.text }}</p>'
  };
}

Common Mistake #5: Not Using Named Views With UI-ROUTER For Power”

ui-router has emerged as the de facto routing solution for AngularJS, surpassing the basic ngRoute module that was removed from core.

Although a new NgRouter is in development, it’s not yet production-ready. As of this writing, AngularJS 1.3.15 remains stable, and ui-router continues to excel.

Its key strengths include:

  • Robust state nesting
  • Route abstraction
  • Support for optional and required parameters

Let’s delve into state nesting to understand how it helps avoid AngularJS errors.

Imagine a typical yet complex scenario: an app with a homepage view and a product view. The product view comprises three sections: intro, widget, and content. The requirement is to persist the widget without reloading when transitioning between states, while the content should reload.

Consider the following HTML structure for the product index page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<body>
  <header>
    <!-- SOME STATIC HEADER CONTENT -->
  </header>

  <section class="main">
    <div class="page-content">

    <div class="row">
      <div class="col-xs-12">
        <section class="intro">
          <h2>SOME PRODUCT SPECIFIC INTRO</h2>
        </section>
      </div>
    </div>

    <div class="row">
      <div class="col-xs-3">
        <section class="widget">
          <!-- some widget, which should never reload -->
        </section>
      </div>
      <div class="col-xs-9">
        <section class="content">
          <div class="product-content">
          <h2>Product title</h2>
          <span>Context-specific content</span>
        </div>
        </section>
      </div>
    </div>

  </div>

  </section>

  <footer>
    <!-- SOME STATIC HEADER CONTENT -->
  </footer>

</body>

This structure might be provided by an HTML developer. Our task is to separate it into files and states. I advocate for an abstract MAIN state to house global data, replacing $rootScope, and static HTML shared across all pages. This keeps index.html clean.

1
2
3
4
<!— index.html —>
<body>
  <div ui-view></div>
</body>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!— main.html —>
<header>
  <!-- SOME STATIC HEADER CONTENT -->
</header>

<section class="main">
  <div ui-view></div>
</section>

<footer>
  <!-- SOME STATIC HEADER CONTENT -->
</footer>

Now, let’s examine the product index page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="page-content">

  <div class="row">
    <div class="col-xs-12">
      <section class="intro">
        <div ui-view="intro"></div>
      </section>
    </div>
  </div>

  <div class="row">
    <div class="col-xs-3">
      <section class="widget">
        <div ui-view="widget"></div>
      </section>
    </div>
    <div class="col-xs-9">
      <section class="content">
        <div ui-view="content"></div>
      </section>
    </div>
  </div>

</div>

Here, the product index page utilizes three named views: one each for the intro, widget, and product content, fulfilling our requirements. Now, let’s configure the routing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function config($stateProvider) {
  $stateProvider
    // MAIN ABSTRACT STATE, ALWAYS ON
    .state('main', {
      abstract: true,
      url: '/',
      controller: 'MainController as MC',
      templateUrl: '/routing-demo/main.html'
    })
    // A SIMPLE HOMEPAGE
    .state('main.homepage', {
      url: '',
      controller: 'HomepageController as HC',
      templateUrl: '/routing-demo/homepage.html'
    })
    // THE ABOVE IS ALL GOOD, HERE IS TROUBLE
    // A COMPLEX PRODUCT PAGE
    .state('main.product', {
      abstract: true,
      url: ':id',
      controller: 'ProductController as PC',
      templateUrl: '/routing-demo/product.html',
    })
    // PRODUCT DEFAULT SUBSTATE
    .state('main.product.index', {
      url: '',
      views: {
        'widget': {
          controller: 'WidgetController as PWC',
          templateUrl: '/routing-demo/widget.html' 
        },
        'intro': {
          controller: 'IntroController as PIC',
          templateUrl: '/routing-demo/intro.html' 
        },
        'content': {
          controller: 'ContentController as PCC',
          templateUrl: '/routing-demo/content.html'
        }
      }
    })
    // PRODUCT DETAILS SUBSTATE
    .state('main.product.details', {
      url: '/details',
      views: {
        'widget': {
          controller: 'WidgetController as PWC',
          templateUrl: '/routing-demo/widget.html' 
        },
        'content': {
          controller: 'ContentController as PCC',
          templateUrl: '/routing-demo/content.html'
        }
      }
    });
}

angular.module('articleApp', [
  'ui.router'
])
.config(config);

While this initial approach works, transitioning between main.product.index and main.product.details reloads both the content and the widget. Ideally, we only want to reload the content. This challenge led to the development of routers supporting “sticky views.” Fortunately, ui-router provides this functionality out of the box through “absolute named view targeting.”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// A COMPLEX PRODUCT PAGE
// WITH NO MORE TROUBLE
.state('main.product', {
  abstract: true,
  url: ':id',
  views: {
    // TARGETING THE UNNAMED VIEW IN MAIN.HTML
    '@main': {
      controller: 'ProductController as PC',
      templateUrl: '/routing-demo/product.html' 
    },
    // TARGETING THE WIDGET VIEW IN PRODUCT.HTML
    // BY DEFINING A CHILD VIEW ALREADY HERE, WE ENSURE IT DOES NOT RELOAD ON CHILD STATE CHANGE
    'widget@main.product': {
      controller: 'WidgetController as PWC',
      templateUrl: '/routing-demo/widget.html' 
    }
  }
})
// PRODUCT DEFAULT SUBSTATE
.state('main.product.index', {
  url: '',
  views: {
    'intro': {
      controller: 'IntroController as PIC',
      templateUrl: '/routing-demo/intro.html' 
    },
    'content': {
      controller: 'ContentController as PCC',
      templateUrl: '/routing-demo/content.html'
    }
  }
})
// PRODUCT DETAILS SUBSTATE
.state('main.product.details', {
  url: '/details',
  views: {
    'content': {
      controller: 'ContentController as PCC',
      templateUrl: '/routing-demo/content.html'
    }
  }
});

By relocating the state definition to the parent view (which is also abstract), we prevent the child view from reloading when switching URLs that would typically affect its siblings. While the widget could be a simple directive, it demonstrates how complex nested states can be preserved.

An alternative approach using $urlRouterProvider.deferIntercept() exists. However, I find state configuration to be cleaner. For those interested in route interception, I’ve written a tutorial available on StackOverflow.

Common Mistake #6: Declaring Everything In The Angular World Using Anonymous Functions

This “mistake” leans more towards style than causing AngularJS errors. You may have observed my preference for defining functions explicitly before passing them to AngularJS internals, rather than relying heavily on anonymous functions.

This practice extends beyond just functions. I adopted it after studying style guides, particularly Airbnb’s and Todd Motto’s. The advantages are numerous, with minimal drawbacks.

Firstly, assigning functions and objects to variables enhances their manipulation and mutation. Secondly, this approach promotes cleaner code that can be easily modularized into separate files, improving maintainability. Wrapping each file in an IIFE prevents global namespace pollution. Thirdly, testability is greatly enhanced. Consider the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'use strict';

function yoda() {

  var privateMethod = function () {
    // this function is not exposed
  };

  var publicMethod1 = function () {
    // this function is exposed, but it's internals are not exposed
    // some logic...
  };

  var publicMethod2 = function (arg) {
    // THE BELOW CALL CANNOT BE SPIED ON WITH JASMINE
    publicMethod1('someArgument');
  };

  // IF THE LITERAL IS RETURNED THIS WAY, IT CAN'T BE REFERRED TO FROM INSIDE
  return {
    publicMethod1: function () {
      return publicMethod1();
    },
    publicMethod2: function (arg) {
      return publicMethod2(arg);
    }
  };
}

angular.module('app', [])
.factory('yoda', yoda);

Mocking publicMethod1 is an option, but its exposed nature makes it simpler to spy on the existing method. However, the method itself is a thin wrapper function. Let’s explore an alternative:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function yoda() {

  var privateMethod = function () {
    // this function is not exposed
  };

  var publicMethod1 = function () {
    // this function is exposed, but it's internals are not exposed
    // some logic...
  };

  var publicMethod2 = function (arg) {
    // the below call cannot be spied on
    publicMethod1('someArgument');

    // BUT THIS ONE CAN!
    hostObject.publicMethod1('aBetterArgument');
  };

  var hostObject = {
    publicMethod1: function () {
      return publicMethod1();
    },
    publicMethod2: function (arg) {
      return publicMethod2(arg);
    }
  };

  return hostObject;
}

This goes beyond mere style, resulting in more reusable, idiomatic code with greater expressiveness. Splitting code into self-contained blocks enhances clarity and maintainability.

Common Mistake #7: Doing Heavy Processing In Angular AKA Using Workers

Certain scenarios necessitate processing large arrays of complex objects, applying filters, decorators, and sorting algorithms. Offline functionality or performance-critical data display are prime examples. JavaScript’s single-threaded nature makes it susceptible to browser freezes.

Web workers offer a solution. While no widely adopted libraries specifically address this in AngularJS, implementing it is straightforward.

First, set up the service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function scoringService($q) {
  
  var scoreItems = function (items, weights) {
    var deferred = $q.defer();
    var worker = new Worker('/worker-demo/scoring.worker.js');
    var orders = {
      items: items,
      weights: weights
    };
    worker.postMessage(orders);
    worker.onmessage = function (e) {
      if (e.data && e.data.ready) {
        deferred.resolve(e.data.items);
      }
    };

    return deferred.promise;
  };
  var hostObject = {
    scoreItems: function (items, weights) {
      return scoreItems(items, weights);
    }
  };

  return hostObject;

}

angular.module('app.worker')
.factory('scoringService', scoringService);

Next, create the worker:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'use strict';

function scoringFunction(items, weights) {
  var itemsArray = [];
  for (var i = 0; i < items.length; i++) {
    // some heavy processing
    // itemsArray is populated, etc.
  }

  itemsArray.sort(function (a, b) {
    if (a.sum > b.sum) {
      return -1;
    } else if (a.sum < b.sum) {
      return 1;
    } else {
      return 0;
    }
  });

  return itemsArray;
}

self.addEventListener('message', function (e) {
  var reply = {
    ready: true
  };
  if (e.data && e.data.items && e.data.items.length) {
    reply.items = scoringFunction(e.data.items, e.data.weights);
  }
  self.postMessage(reply);
}, false);

Now, inject the service as usual. Treat scoringService.scoreItems() like any other service method returning a promise. The heavy processing occurs on a separate thread, preserving UX responsiveness.

Considerations:

  • The optimal number of workers varies. Some suggest 8, but using an online calculator is recommended.
  • Ensure compatibility with older browsers.
  • Passing the number 0 from the service to the worker might require converting it to a string using .toString().

Common Mistake #8: Overusing And Misunderstanding Resolves

Resolves introduce additional loading time for views. Prioritizing front-end performance means minimizing any unnecessary delays. Rendering parts of the view while awaiting API data should be the norm.

Consider the following setup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function resolve(index, timeout) {
  return {
    data: function($q, $timeout) {
      var deferred = $q.defer();
      $timeout(function () {
        deferred.resolve(console.log('Data resolve called ' + index));
      }, timeout);
      return deferred.promise;
    }
  };
}

function configResolves($stateProvide) {
  $stateProvider
    // MAIN ABSTRACT STATE, ALWAYS ON
    .state('main', {
      url: '/',
      controller: 'MainController as MC',
      templateUrl: '/routing-demo/main.html',
      resolve: resolve(1, 1597)
    })
    // A COMPLEX PRODUCT PAGE
    .state('main.product', {
      url: ':id',  
      controller: 'ProductController as PC',
      templateUrl: '/routing-demo/product.html',
      resolve: resolve(2, 2584)
    })
    // PRODUCT DEFAULT SUBSTATE
    .state('main.product.index', {
      url: '',
      views: {
        'intro': {
          controller: 'IntroController as PIC',
          templateUrl: '/routing-demo/intro.html'
        },
        'content': {
          controller: 'ContentController as PCC',
          templateUrl: '/routing-demo/content.html'
        }
      },
      resolve: resolve(3, 987)
    });
}

The console output reveals:

1
2
3
4
5
6
Data resolve called 3
Data resolve called 1
Data resolve called 2
Main Controller executed
Product Controller executed
Intro Controller executed

Key takeaways:

  • Resolve execution is asynchronous.
  • Execution order is not guaranteed (and requires significant effort to enforce).
  • All states, even non-abstract ones, are blocked until all resolves complete.

This means users face a delay before seeing any output, waiting for all dependencies to resolve. If data is absolutely critical before rendering, use a .run() block. Otherwise, make service calls from the controller and handle the partially loaded state gracefully. Displaying progress (which is possible as the controller has already executed) is preferable to a stalled app.

Common Mistake #9: Not Optimizing The App - Three Examples

a) Causing too many digest loops, such as attaching sliders to models

While this issue can arise in various contexts, let’s examine it through the lens of sliders. Using the “angular range slider” library for its extended functionality, consider its minimal usage:

1
2
3
4
5
6
7
8
9
<body ng-controller="MainController as MC">
  <div range-slider 
    min="0" 
    max="MC.maxPrice" 
    pin-handle="min" 
    model-max="MC.price"
  >
  </div>
</body>

Now, let’s analyze the controller code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
this.maxPrice = '100';
this.price = '55';

$scope.$watch('MC.price', function (newVal) {
  if (newVal || newVal === 0) {
    for (var i = 0; i < 987; i++) {
      console.log('ALL YOUR BASE ARE BELONG TO US');
    }
  }
});

This implementation can lead to performance bottlenecks. A common workaround involves introducing a timeout on the input. However, this isn’t always ideal and might introduce unwanted delays in model updates.

A more elegant solution involves a temporary model bound to the slider, updating the working model on timeout:

1
2
3
4
5
6
7
8
9
<body ng-controller="MainController as MC">
  <div range-slider 
    min="0" 
    max="MC.maxPrice" 
    pin-handle="min" 
    model-max="MC.priceTemporary"
  >
  </div>
</body>

And in the controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
this.maxPrice = '100';
this.price = '55';
this.priceTemporary = '55';

$scope.$watch('MC.price', function (newVal) {
  if (!isNaN(newVal)) {
    for (var i = 0; i < 987; i++) {
      console.log('ALL YOUR BASE ARE BELONG TO US');
    }
  }
});

var timeoutInstance;
$scope.$watch('MC.priceTemporary', function (newVal) {
  if (!isNaN(newVal)) {
    if (timeoutInstance) {
      $timeout.cancel(timeoutInstance);
    }

    timeoutInstance = $timeout(function () {
      $scope.MC.price = newVal;
    }, 144);
    
  }
});

b) Not using $applyAsync

AngularJS lacks a polling mechanism for $digest(). Its execution relies on directives (ng-click, input), services ($timeout, $http), and methods ($watch) that evaluate code and trigger a digest cycle.

.$applyAsync() defers expression resolution to the next $digest() cycle, initiated after a ~10ms timeout.

There are two ways to leverage applyAsync: automated for $http requests and manual for other scenarios.

For batched $http request resolution within a single digest cycle:

1
2
3
mymodule.config(function ($httpProvider) {
  $httpProvider.useApplyAsync(true);
});

The manual approach provides insight into its inner workings. Consider a function invoked by a vanilla JS event listener, jQuery’s .click(), or an external library. If this function modifies models outside an $apply() wrapper, calling $scope.$root.$digest() ($rootScope.$digest()), or at least $scope.$digest(), becomes necessary to reflect the changes.

Multiple such calls within a short timeframe can impact performance. Employing $scope.$applyAsync() instead consolidates them into a single digest cycle.

c) Doing heavy processing of images

Chrome Developer Tools’ Timeline proves invaluable for pinpointing performance bottlenecks. Green dominance in the recorded timeline graph often signifies image processing issues. While not strictly AngularJS-related, this can exacerbate existing AngularJS performance problems (typically yellow in the graph). As front-end engineers, a holistic view is crucial.

Assess your application for:

  • Parallax effects
  • Multiple overlapping content layers
  • Frequent image repositioning
  • Image scaling (e.g., using background-size)
  • Image resizing within loops, potentially triggering digest cycles

If three or more of these resonate, consider optimizations. Serving multiple image sizes eliminates resizing altogether. The transform: translateZ(0) hack leverages GPU acceleration. Utilizing requestAnimationFrame for handlers optimizes animation performance.

Common Mistake #10: jQuerying It - Detached DOM Tree

The advice to avoid or minimize jQuery usage in AngularJS is prevalent. Understanding the rationale behind this is key. While three primary reasons exist, none are insurmountable obstacles.

Reason 1: jQuery code execution often necessitates manual $digest() calls. AngularJS frequently offers tailored alternatives (e.g., ng-click or its event system) better suited for its ecosystem.

Reason 2: Single-page apps demand careful memory management, unlike traditional websites that reload on navigation. Failing to clean up can lead to performance degradation for users over prolonged sessions.

Reason 3: Cleanup itself poses challenges. Lacking a direct garbage collector invocation from within browser scripts, detached DOM trees can linger. Consider the following example (assuming jQuery is loaded):

1
2
3
4
<section>
  <test-for-toptal></test-for-toptal>
  <button ng-click="MC.removeDirective()">remove directive</button>
</section>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function MainController($rootScope, $scope) {
  this.removeDirective = function () {
    $rootScope.$emit('destroyDirective');
  };
}

function testForToptal($rootScope, $timeout) {
  return {
    link: function (scope, element, attributes) {

      var destroyListener = $rootScope.$on('destroyDirective', function () {
        scope.$destroy();
      });

      // adding a timeout for the DOM to get ready
      $timeout(function () {
        scope.toBeDetached = element.find('p');
      });

      scope.$on('$destroy', function () {
        destroyListener();
        element.remove();
      });
    },
    template: '<div><p>I AM DIRECTIVE</p></div>'
  };
}

angular.module('app', [])
.controller('MainController', MainController)
.directive('testForToptal', testForToptal);

This simple directive outputs text with a button to manually destroy it.

Removing the directive leaves a reference to the DOM tree within scope.toBeDetached. Chrome dev tools’ “Profiles” tab, specifically the “Take Heap Snapshot” functionality, reveals:

A few such instances might be tolerable, but a large number, especially when stored on the scope, poses problems. Each digest cycle evaluates the entire DOM, including the detached tree with 4 nodes. Here’s the solution:

1
2
3
4
5
6
7
8
9
scope.$on('$destroy', function () {

  // setting this model to null
  // will solve the problem.
  scope.toBeDetached = null;

  destroyListener();
  element.remove();
});

The problematic detached DOM tree is gone!

This example, for demonstration purposes, uses the same scope and stores the DOM element on it. Real-world scenarios might differ, storing it in a variable instead. However, memory consumption persists if any closure referencing that variable or others within the same function scope remains alive.

Common Mistake #11: Overusing Isolated Scope

When creating directives intended for single-use scenarios or environments where conflicts are unlikely, isolated scope is often unnecessary. The trend of reusable components might suggest otherwise, but even core AngularJS directives refrain from using it.

Two primary reasons stand out:

  1. Applying multiple isolated scope directives to a single element is not possible.
  2. Nesting, inheritance, and event processing, particularly transclusion, can behave unexpectedly.

Therefore, this code snippet would fail:

1
<p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"></p>

Even with a single directive, neither isolated scope models nor events broadcasted within isolatedScopeDirective would be accessible to AnotherController. While transclusion offers workarounds, in most cases, isolation is unnecessary.

1
2
3
4
5
<p isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux">
  <div ng-controller="AnotherController">
    … the isolated scope is not available here, look: {{ isolatedModel }}
  </div>
</p>

Let’s address two questions:

  1. How can same-scope directives process parent scope models?
  2. How can new model values be instantiated?

Both involve passing values through attributes. Consider this MainController:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function MainController($interval) {
  this.foo = {
    bar: 1
  };
  this.baz = 1;
  var that = this;
  $interval(function () {
    that.foo.bar++;
  }, 144);

  $interval(function () {
    that.baz++;
  }, 144);

  this.quux = [1,2,3];
}

This controller manages the following view:

1
2
3
4
5
6
7
8
<body ng-controller="MainController as MC">

  <div class="cyan-surface">
    <h1 style="font-size: 21px;">Attributes test</h1>
    <test-directive watch-attribute="MC.foo" observe-attribute="current index: {{ MC.baz }}"></test-directive>
  </div>

</body>

Note that watch-attribute is not interpolated. Here’s the directive definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function testDirective() {
  var postLink = function (scope, element, attrs) {
    scope.$watch(attrs.watchAttribute, function (newVal) {
      if (newVal) {
        // take a look in the console
        // we can't use the attribute directly
        console.log(attrs.watchAttribute);

        // the newVal is evaluated, and it can be used
        scope.modifiedFooBar = newVal.bar * 10;
      }
    }, true);

    attrs.$observe('observeAttribute', function (newVal) {
      scope.observed = newVal;
    });
  };

  return {
    link: postLink,
    templateUrl: '/attributes-demo/test-directive.html'
  };
}

The magic lies in passing attrs.watchAttribute to scope.$watch() without quotation marks. This effectively passes the string MC.foo! It works because any string passed to $watch() undergoes evaluation against the scope, and MC.foo is accessible within that scope. This approach mirrors how core AngularJS directives typically watch attributes.

The complete template code is available on GitHub. Exploring $parse and $eval unlocks even more powerful techniques.

Common Mistake #12: Not Cleaning Up After Yourself - Watchers, Intervals, Timeouts And Variables

AngularJS handles some cleanup tasks, but others require manual intervention:

  • Watchers not bound to the current scope (e.g., those bound to $rootScope)
  • Intervals
  • Timeouts
  • Variables referencing DOM elements within directives
  • Problematic jQuery plugins (those lacking handlers for the JavaScript $destroy event)

Failing to perform this manual cleanup invites unexpected behavior and memory leaks, often manifesting gradually rather than immediately, adhering to Murphy’s law.

AngularJS provides convenient tools to address these concerns:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function cleanMeUp($interval, $rootScope, $timeout) {
  var postLink = function (scope, element, attrs) {
    var rootModelListener = $rootScope.$watch('someModel', function () {
      // do something
    });

    var myInterval = $interval(function () {
      // do something in intervals
    }, 2584);

    var myTimeout = $timeout(function () {
      // defer some action here
    }, 1597);

    scope.domElement = element;

    $timeout(function () {
      // calling $destroy manually for testing purposes
      scope.$destroy();
    }, 987);

    // here is where the cleanup happens
    scope.$on('$destroy', function () {
      // disable the listener
      rootModelListener();

      // cancel the interval and timeout
      $interval.cancel(myInterval);
      $timeout.cancel(myTimeout);

      // nullify the DOM-bound model
      scope.domElement = null;
    });

    element.on('$destroy', function () {
      // this is a jQuery event
      // clean up all vanilla JavaScript / jQuery artifacts here

      // respectful jQuery plugins have $destroy handlers,
      // that is the reason why this event is emitted...
      // follow the standards.
    });

  };

Note the jQuery $destroy event. While named similarly to its AngularJS counterpart, it’s handled separately. Scope $watchers won’t react to the jQuery event.

Common Mistake #13: Keeping Too Many Watchers

This relates to the earlier discussions on $digest(). Each binding ({{ someModel }}) in AngularJS creates a watcher. The $digest phase evaluates every binding, comparing it to its previous value—a process called dirty-checking. If a change is detected, the associated watcher callback is executed. If this callback modifies a scope variable, a new $digest cycle is triggered (up to a maximum of 10 before throwing an exception).

Modern browsers handle thousands of bindings without breaking a sweat, unless the expressions involved are overly complex. A general rule of thumb is to aim for no more than 2000 watchers.

Limiting watchers involves strategically choosing when to watch scope models. AngularJS 1.3 introduced one-time bindings, simplifying this process.

1
<li ng-repeat="item in ::vastArray">{{ ::item.velocity }}</li>

Once vastArray and item.velocity are initially evaluated, they are no longer tracked for changes. Filters applied to the array continue to function as expected. The key difference is that the array itself isn’t reevaluated, leading to performance gains in many scenarios.

Common Mistake #14: Misunderstanding The Digest

Mistakes 9.b and 13 touched upon this aspect. Here’s a more in-depth explanation. AngularJS DOM updates are driven by watcher callback functions. Every binding, represented by the {{ someModel }} directive, sets up these watchers. Other directives like ng-if and ng-repeat do so as well. A peek into the source code (which is surprisingly readable) reveals this. Watchers can also be established manually, a practice you’ve likely employed.

$watch()ers are associated with scopes. They accept strings evaluated against their bound scope or functions to evaluate. Crucially, they also accept callbacks. When $rootScope.$digest() is invoked, it evaluates all registered models (scope variables) and compares them to their previous values. Mismatches trigger the associated $watch() callback.

It’s vital to understand that even if a model’s value changes, the callback is deferred until the next digest phase. This “phase” can encompass multiple digest cycles. A single watcher modifying a scope model triggers an additional cycle.

However, $digest() is not polled. Core directives, services, methods, and the like are responsible for invoking it. Modifying a model within a custom function that doesn’t call .$apply, .$applyAsync, .$evalAsync, or another method that ultimately triggers $digest() won’t update the bindings.

Interestingly, the source code for $digest(), while complex, is worth exploring for its humorous warnings.

Common Mistake #15: Not Relying On Automation, Or Relying On It Too Much

Embracing automation is natural for efficiency-minded developers. Managing dependencies, processing files, and constantly reloading the browser can be tedious.

Tools like Bower, npm, Grunt, Gulp, Brunch, and even bash scripts streamline these tasks. Yeoman generators further expedite project setup.

However, it’s crucial to understand the inner workings of this infrastructure. Do you truly need all the bells and whistles, especially if you’ve just spent hours troubleshooting livereload issues with your Connect web server?

Take a step back and assess your actual requirements. These tools are meant to assist, not complicate. Many experienced developers advocate for simplicity.

Common Mistake #16: Not Running The Unit Tests In TDD Mode

While tests won’t magically eliminate all AngularJS errors, they provide a safety net against regressions.

The focus here is on unit tests due to their speed advantage over end-to-end (e2e) tests. The process I’m about to describe is surprisingly enjoyable.

Test Driven Development (TDD), often implemented with runners like gulp-karma, involves running all unit tests on every file save. I prefer starting with empty assertions:

1
2
3
4
5
6
describe('some module', function () {
  it('should call the name-it service…', function () {
    // leave this empty for now
  });
  ...
});

Next, I write or refactor the actual code. Finally, I return to the tests, fleshing out the assertions.

Having a TDD task running in a terminal significantly accelerates development. Unit tests execute swiftly, even with a large suite. Saving a test file triggers the runner, which evaluates the tests and provides instant feedback.

e2e tests, being slower, benefit from dividing them into suites, running one at a time. Protractor supports this. Here’s my preferred setup using Gulp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
'use strict';

var gulp = require('gulp');
var args = require('yargs').argv;
var browserSync = require('browser-sync');
var karma = require('gulp-karma');
var protractor = require('gulp-protractor').protractor;
var webdriverUpdate = require('gulp-protractor').webdriver_update;

function test() {
  // Be sure to return the stream
  // NOTE: Using the fake './foobar' so as to run the files
  // listed in karma.conf.js INSTEAD of what was passed to
  // gulp.src !
  return gulp.src('./foobar')
    .pipe(karma({
      configFile: 'test/karma.conf.js',
      action: 'run'
    }))
    .on('error', function(err) {
      // Make sure failed tests cause gulp to exit non-zero
      // console.log(err);
      this.emit('end'); //instead of erroring the stream, end it
    });
}

function tdd() {
  return gulp.src('./foobar')
    .pipe(karma({
      configFile: 'test/karma.conf.js',
      action: 'start'
    }))
    .on('error', function(err) {
      // Make sure failed tests cause gulp to exit non-zero
      // console.log(err);
      // this.emit('end'); // not ending the stream here
    });
}

function runProtractor () {

  var argument = args.suite || 'all';
  
  // NOTE: Using the fake './foobar' so as to run the files
  // listed in protractor.conf.js, instead of what was passed to
  // gulp.src
  return gulp.src('./foobar')
    .pipe(protractor({
      configFile: 'test/protractor.conf.js',
      args: ['--suite', argument]
    }))
    .on('error', function (err) {
      // Make sure failed tests cause gulp to exit non-zero
      throw err;
    })
    .on('end', function () {
      // Close browser sync server
      browserSync.exit();
    });
}

gulp.task('tdd', tdd);
gulp.task('test', test);
gulp.task('test-e2e', ['webdriver-update'], runProtractor);
gulp.task('webdriver-update', webdriverUpdate);

Common Mistake #17: Not Using The Available Tools

A - Chrome Breakpoints

Chrome dev tools empower you to pause code execution at specific points within any loaded file, granting access to variables, the call stack, stack traces, and more. This functionality requires no code modifications, operating entirely within dev tools.

While console.log() offers similar runtime insights, breakpoints are far more powerful, even supporting minified files. For a deeper dive, refer to here.

AngularJS provides additional console-based debugging capabilities. With debugInfo enabled, scopes can be accessed through DOM elements. Injecting services is also possible. Consider these examples:

1
$(document.body).scope().$root

Or, after selecting an element in the inspector:

1
$($0).scope()

Even with debugInfo disabled:

1
angular.reloadWithDebugInfo()

This access persists after reload.

To inject and interact with a service:

1
2
var injector = $(document.body).injector();
var someService = injector.get('someService');

B - Chrome Timeline

The Timeline tool within dev tools allows for recording and analyzing your app’s runtime performance. Metrics like memory usage, frame rate, and CPU activity breakdown (loading, scripting, rendering, painting) are visualized.

Suspect performance degradation? The Timeline tab is your friend. Record the actions leading to the issue and analyze the results. Excessive watchers manifest as prominent yellow bars. Memory leaks are evident from the memory consumption graph over time.

A comprehensive guide can be found at https://developer.chrome.com/devtools/docs/timeline.

C - Inspecting Apps Remotely on iOS and Android

Developing hybrid or responsive web apps often involves debugging on physical devices. Both Chrome and Safari dev tools allow accessing the console, DOM tree, and other tools for iOS and Android devices, including WebViews.

First, configure your web server to listen on 0.0.0.0 for network accessibility. Enable web inspector in your device settings. Connect your device to your development machine and access the development page using your machine’s IP address instead of localhost. Your device should now be accessible from your desktop browser’s dev tools.

Detailed instructions for Android can be found at Here. For iOS, unofficial guides are readily available online.

Recently, I’ve had positive experiences with browserSync. Functioning like livereload, it also synchronizes user interactions (scrolling, clicks) across multiple browsers viewing the same page. I was able to control an iOS app from my desktop while monitoring its log output—quite impressive!

Common Mistake #18: Not Reading The Source Code On The NG-INIT Example

ng-init might seem analogous to ng-if and ng-repeat. However, a closer look at its documentation reveals a curious comment advising against its use. This might seem counterintuitive, given its name suggests model initialization, which it does, but with a subtle difference. It doesn’t watch the attribute value. Here’s the relevant AngularJS source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var ngInitDirective = ngDirective({
  priority: 450,
  compile: function() {
    return {
      pre: function(scope, element, attrs) {
        scope.$eval(attrs.ngInit);
      }
    };
  }
});

Surprisingly concise, isn’t it? The sixth line holds the key.

Let’s compare it to ng-show:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var ngShowDirective = ['$animate', function($animate) {
  return {
    restrict: 'A',
    multiElement: true,
    link: function(scope, element, attr) {
      scope.$watch(attr.ngShow, function ngShowWatchAction(value) {
        // we're adding a temporary, animation-specific class for ng-hide since this way
        // we can control when the element is actually displayed on screen without having
        // to have a global/greedy CSS selector that breaks when other animations are run.
        // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845
        $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, {
          tempClasses: NG_HIDE_IN_PROGRESS_CLASS
        });
      });
    }
  };
}];

Again, the sixth line stands out. The presence of $watch introduces dynamism. Much of the AngularJS source code consists of comments elaborating on otherwise readable code. It’s a goldmine for learning.

Conclusion

This guide, significantly longer than its predecessors, highlights the breadth and depth of knowledge required for modern front-end development. The demand for skilled JavaScript engineers, particularly those proficient in AngularJS, remains high. With AngularJS 2.0 on the horizon, its dominance seems assured.

Front-end development is inherently rewarding. Our creations are directly experienced by users, providing instant gratification. Investing time in JavaScript, the language of the web, is paramount. The competition is fierce, and user experience reigns supreme.

Source code for the examples provided can be downloaded from GitHub. Feel free to experiment and adapt it to your needs.

I’d like to acknowledge the following developers whose work has inspired me:

Thanks to the communities on FreeNode’s #angularjs and #javascript channels for the insightful discussions and unwavering support.

Remember:

1
// when in doubt, comment it out! :)
Licensed under CC BY-NC-SA 4.0