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 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:
| |
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:
| |
| |
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:
| |
| |
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:
| |
A simpler alternative exists:
| |
This approach enables accessing MC from a child controller further down the scope chain:
| |
Consistency in the chosen controllerAs acronym is vital. Let’s examine the different ways to set it. We’ve already encountered the first method:
| |
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:
| |
Alternatively, annotations can be employed:
| |
This annotation approach applies to directives as well:
| |
While the alternative annotation syntax remains valid, it lacks conciseness:
| |
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:
| |
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.
| |
| |
Now, let’s examine the product index page:
| |
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:
| |
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.”
| |
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:
| |
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:
| |
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:
| |
Next, create the worker:
| |
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:
| |
The console output reveals:
| |
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:
| |
Now, let’s analyze the controller code:
| |
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:
| |
And in the controller:
| |
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:
| |
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):
| |
| |
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:
| |
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:
- Applying multiple isolated scope directives to a single element is not possible.
- Nesting, inheritance, and event processing, particularly transclusion, can behave unexpectedly.
Therefore, this code snippet would fail:
| |
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.
| |
Let’s address two questions:
- How can same-scope directives process parent scope models?
- How can new model values be instantiated?
Both involve passing values through attributes. Consider this MainController:
| |
This controller manages the following view:
| |
Note that watch-attribute is not interpolated. Here’s the directive definition:
| |
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
$destroyevent)
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:
| |
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.
| |
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:
| |
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:
| |
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:
| |
Or, after selecting an element in the inspector:
| |
Even with debugInfo disabled:
| |
This access persists after reload.
To inject and interact with a service:
| |
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:
| |
Surprisingly concise, isn’t it? The sixth line holds the key.
Let’s compare it to ng-show:
| |
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:
| |