Things I Wish I had Known about AngularJS

Written by Austin Keeley

Your first AngularJS app is going to suck.

And so will your second. And third. And probably every single one until you finally understand the framework at an intimate level. Angular is challenging; it will take weeks of development just to learn the basics and years of building non-trivial applications to actually become comfortable with it. There’s a joke that everyone loves to use Angular for new applications, but no one wants to maintain them. Because Angular is so new, I feel like a lot of Angular codebases are people’s first projects. And we’ve already established those are going to suck.

Over the past few years, I’ve been compiling a list of tips and tricks I’ve painfully learned when writing AngularJS code. My hope is that one day I’ll be able to hop in a time machine and slap my younger self with this before I unleash more terrible AngularJS code to humanity. Even if that never happens, you too can benefit from it!

Imgur Oh good, they found someone to maintain your first AngularJS codebase.

Keep controllers lightweight

The most common newbie mistake I run across is seeing controllers that contain thousands of lines of code. Most AngularJS tutorials start off by explaining how a controller allow you to put variables in scope and link them to your view, so the new developer begins thinking “okay, this must be where I put all my application logic”.

There’s a few problems with this approach:

  • Controllers are not as re-usable as you might think. They typically have only one place to live in the application’s architecture and they are fairly tightly coupled to that.
  • Controllers are slightly more difficult to unit test and they usually involve mocking out the $scope.
  • Controllers can be nested which means the executing environment might change; the downside is now your $scope is different and you could be looking at re-writing a lot of those aforementioned unit tests.

The solution is pretty simple: just use the service and factory AngularJS components and inject them into your controllers. They’re designed for this kind of thing. A well-written factory or service is very easy to unit test and is far more re-usable than controller code.

Here’s a good example. Let’s say you have a really basic web app that grabs a blog feed (as JSON), filters it down by date (passed via $routeParams), and renders it to a web page.

Bad version:

angular.module('blog-app', [])

.controller('BlogCtrl', function($scope, $http, $routeParams) {
  // Grab the start and end date from the route
  var start = $routeParams.start,
    end = $routeParams.end,
    range = moment().range(start, end);

  $http.get('/entries').then(function(entries) {
     entries.forEach(function(entry) {
       if (range.contains(entry.date)) {
         $scope.entries.push(entry);
       }
    });
  });
});

In this example, the logic for grabbing the entries and filtering them is tightly coupled to the controller, so we’re not really building reusable code. In addition, the code is tightly coupled to both a hard coded endpoint URL string and the route parameters. For small trivial apps, this isn’t a big deal, but as your web apps grow larger, this becomes awful to maintain. As a general rule of thumb, I never inject $http into controllers. Instead, bundle all of that interaction up into a factory:

Better version:

angular.module('blog-app', [])

.factory('EntryEndpoint', function($http) {
  return function(endpointURL) {
    this.getEntries = function(start, end) {
      var range = moment().range(start, end);
      var defer = $q.defer();
      var result = [];
      $http.get('/entries').then(function(entries) {
        entries = entries.map(function(entry) {
          return range.contains(entry.date);
        });
        defer.resolve(result);
      });
      return defer.promise;
    };
  };
})

.controller('BlogCtrl', function($scope, $routeParams, EntryEndpoint) {
  var start = $routeParams.start,
    end = $routeParams.end;

  var endpoint = new EntryEndpoint('/entries');
  endpoint.getEntries(start, end).then(function(entries) {
    $scope.entries = entries;
  });
});

This “better version” is a lot longer and appears more complicated, but the payoff is that we now have a clean separation between our components; the controller hands off the logic to factory which doesn’t care about a lot of the other things (like the route parameters or the current scope). We can also re-use this code in other controllers in other places.

Keep the logic out of your templates

One of the reasons I despise PHP is that it encourages you to mix your logic with your HTML code. This is basically just setting you up to write big monolithic and unmaintainable applications. AngularJS’s templating functionality, while being very good, can also lead you to load up your templates with a lot of logic that really doesn’t belong there. The ngIf directive in particular is a beast because if you rely on it too much, you might just have a huge chain of ngIf directives in your templates, which isn’t clean or fun to maintain. Angular also lets you manipulate the scope variables (or create new ones) from within the template code which is also something I’m not a fan of.

Here’s some template code that I’m used to seeing…

Bad version:

<ul>
  <li ng-repeat="entry in entries">
    <div ng-if="entry.attribute === 'valueOne'">   <!-- do something --> </div>
    <div ng-if="entry.attribute === 'valueTwo'">   <!-- do something else --> </div>
    <div ng-if="entry.attribute === 'valueThree'"> <!-- do yet another thing --> </div>
  </li>
</ul>

This is a really simple case, so it doesn’ts look too bad, but if you continue adding logic here, you’ll eventually get into nested ng-if statements or complicated boolean expressions that could otherwise be elegantly expressed in your controller code.

So how do you avoid junking up your templates with logic? I have a few methods I like:

  • If you find yourself using a lot of logic to build out a view stack as your application state changes, then you probably should be using ngRoutes to cleanly define how your view changes between application states.
  • Define your JavaScript objects to adhere to an interface and keep your view code from needing to know the implementation; this is just good development anyway.
  • Angular directives allow you to couple together your logic and view together (when it makes sense to) into a maintainable package. When you use directives with ngTransclude, you end up with a very powerful way to write clean code that is also highly flexible. More on that later.

Favor Factories over Services

This may be more of a personal preference than a hard rule, but I think Factories are underused when it compared to Services. The big difference between the two is that Factories can return a constructor definition which can be instantiated with new. We can, of course, pass parameters into the constructor, to custom tailor the object to the current situation, rather than just writing a giant service that tries to be one-size-fits-all.

This isn’t a 100% of the time rule (since Services are nice for simple interactions), but it’s really clean in the long run when you’re trying to build out your app’s domain objects.

Mixing AngularJS and jQuery

Here’s a fun piece of trivia: AngularJS includes a copy of jqLite and it will bind it to the global $ variable. If you include jQuery before you include the AngularJS library, it will use that instead. Because jqLite isn’t completely compatible with jQuery, I make sure that I include jQuery first so it doesn’t have any adverse effects on other plugins.

While on the topic of jQuery, it should be noted that AngularJS and jQuery aren’t incompatible libraries, however I really don’t like mixing jQuery DOM manipulation code with my AngularJS code. I see a ton of this in AngularJS code written by people who are new to the framework because they have exerience with jQuery and they used to solving all their problems with it. DOM manipulation within your Angular code tends to be bad since it couples your DOM view code to your logic.

There is one good exception, however: Angular directives give you access to the DOM element in the link function. Go crazy with DOM manipulation there. Other than that, I avoid it. If you absolutely must do some jQuery magic to manipulate the DOM, at least restrict it to controller code, which already has a tighter coupling to the view code anyway.

One module per file.

Prepare your hate mail! I am not a fan of splitting a module up into multiple files. Most novice AngularJS developers don’t even know this is possible, but it’s perfectly fine to have a file called first.js with:

angular.module('my-module', []).controller('MyController', function() {});

and another file called second.js with:

angular.module('my-module').controller('AnotherController', function() { });

A lot of people probably like this; it keeps your files small and normally that’s a good thing. The nastiness comes from the fact that you need to be absolutely sure that the first.js file is loaded before the second.js file, otherwise you’ll get a dreaded $injector:nomod error. You see, that first call to angular.module has two arguments, so we’re defining the module there. The second call just allows us to continue the module. If you’re including your .js files in a page, it’s not too bad to make sure this happens, but if you’re using something like grunt, you need to be sure your modules are being concat’d in the right order (the default is alphabetical). Some people get around this by creating “meta-modules” that just exist as dependency holders. This isn’t terrible, but I think it’s just going to bloat up your project structure. My personal preference is to keep things as one module per file.

Use ngRoutes

Someone once told me they didn’t use routes because it made their URLs look ugly. A small part of me died that day.

Routes are incredibly useful for building out your application states. I like to design my routes as early as possible in the application design since it’s much tougher to add them later. Even though the ngRoutes module isn’t part of the core AngularJS library, you should be using it.

Also, if you’re still hung up on ugly URLs, you can always switch it to HTML5 mode or opt for the Angular-UI-Route 3rd party library.

Don’t inject $routeParams

…into anything other than controllers. It’s really tempting to just inject that thing anywhere you need it, but injecting that provider into, say, a factory or a service, just tightly couples that component to your specific route.

Inject $routeParams into your controller code and have that pass the route variables to your individual objects in other ways (like in a Factory constructor, because those are a good idea, right?).

Don’t manipulate things in $rootScope

This is fairly obvious. The $rootScope is basically a global variable space that gets inherited for all child scopes. Putting things in the $rootScope isn’t too terrible (as long as you’re only doing it in one centralized place), but changing those variables will now change them for all scopes. You’re basically dealing with global variables at this point. Nested Controllers make things more complicated

Being able to nest controllers is pretty damn cool. I’m okay with it. In fact, you probably can’t write a modern web application without doing it. Just realize that doing this is going to increase the complexity of your application. If you’re nesting controllers four or five levels deep, then you probably need to think about your web app’s architecture.

Nested controllers tend to be coupled together, which is bad for all the obvious reasons.

Try to avoid using $scope.$parent

A newbie mistake that I see is trying to manipulate a variable in the current scope without realizing that things like ngRepeat create a new child scope. The less-than-ideal solution is to manipulate it with $scope.$parent. This is not an elegant solution, particularly if this scope is more deeply nested (then you get things like $scope.$parent.$parent.$parent).

My preferred solution is to wrap these variables in an outer object so that the child scope inherits a copy of that object which has the pointer to the actual variable. This is not the most intuitive thing to understand, but it’s far more maintainable than a family tree of parent scopes.

Directives are really cool

They are also incredibly complicated and difficult to learn, but if you can master them, you basically can have a deeper appreciation for how AngularJS works. One aspect of directives is particularly cool: ngTransclude. While directives are usually self-contained and tightly encapsulated components of your application, transclusion allows you to elegantly add on to the directive to tailor it to your specific use case. It’s too detailed to try to explain here, but it’s seriously a pretty cool way to build some sweet directives.

Conclusions

AngularJS is a beast. The learning curve on this library is insane. I feel that by learning it, however, you will become a better developer and you’ll curate your own lists like this. Hopefully you can pass your own experiences along to others.

Photo Credit: Landfill, by the Wisconsin Department of Natural Resources.

Posted July 06, 2015