AngularJS Tips and Tricks [UPDATED]

These tips were developed in AngularJs v0.10.5 v1.0.1. I'll keep updating this post, so check back often!

I've compared a LOT of different javascript frameworks for my company's rewrite, and finally settled on AngularJS because of how rapidly I'm able to produce prototypes. In my opinion, although it's very alpha and fairly lacking on the graphical side, it's excellent for CRUD applications (meaning forms, tables and reports). I'm still trying to lean towards emphasizing reusable widgets and directives instead of just custom-coding everything for your own app. If you are still on the fence take a look at TodoMVC for an excellent unbaised comparison.

Most of these tips have been moved to AngularUI - Go check it out!

The companion suite to AngularJS, a collection of work by many AngularJS users with a plethora of useful utilities.

Table of Contents


Serializing the Form

No. Bad dog. You're doing it wrong.

If every form control on your page does not have an ng-model then you're doing it wrong.

Map the form to a large object (such as $scope.data={}) and specifying the keys you want each control to populate such as ng-model="data.name". Then when you're ready to save, all your data is in a nicely-accessible package in $scope.data.

Seriously. Stop trying to access the DOM from your controller.

Useful $scope Methods

You may at a point try to do ng-click="alert()" or ng-change="console.log()" and have found these don't work. That's because you need to remember every function is called on the $scope object. So alert() becomes $scope.alert() which is (usually) undefined. Here are some useful functions I like to throw on the $rootScope which is injected into my base module:

Another point to remember is that anything you set on $rootScope is available application-wide, so if you want to set global flags or app-wide view data this might be a good place to do it. However sometimes things might better fit into a service so don't get carried away with bloating your $scope.

/**
 * Easy access to route params
 * REMEMBER: this object is empty until the $routeChangeSuccess event is broadcasted 
 */
$rootScope.params = $routeParams;
/**
 * Wrapper for angular.isArray, isObject, etc checks for use in the view
 *
 * @param type {string} the name of the check (casing sensitive)
 * @param value {string} value to check
 */
$rootScope.is = function(type, value) {
	return angular['is'+type](value);
};
/**
 * Wrapper for $.isEmptyObject()
 *
 * @param value	{mixed} Value to be tested
 * @return boolean
 */
$rootScope.empty = function(value) {
	return $.isEmptyObject(value);
};
/**
 * Debugging Tools
 *
 * Allows you to execute debug functions from the view
 */
$rootScope.log = function(variable) {
	console.log(variable);
};
$rootScope.alert = function(text) {
	alert(text);
};

Don't Escape HTML

One of the first things people get stuck on. It used to be a filter, now it's a separate directive.

ng-bind-html-unsafe="{expression}"

Avoiding Flash Of Unstyled Content (FOUC)

You may notice when your app is loading up your templates and parts of your app you didn't want to be shown are briefly visible. Since AngularJS is entirely dependant on Javascript you can't really degrade gracefully, but you CAN avoid this behavior with two tools:

ngCloak

The ngCloak Directive does 1 thing: unhides the element when compiled (aka: when angular/js loads). Use this directive and the related CSS to hide content you don't want to render uncompiled (read the docs).

ngBind

Most of the time you'll use {{someVar}} but this may show up uncompiled on the initial page. For those situations, if you want to render the element, but you don't want to show the braces, use ngBind instead. Remember: only the initial html file content will load uncompiled. All subsequent directives, templates and partials that are injected by AngularJS will never show up uncompiled, so you don't need to worry about them!

Terse Class Toggling

It's much easier in 1.0 now. You can simply pass an object that handles multiple classes: ng-class="{'selected':item.isSelected,'enabled':isEnabled(item)}"

Routing Tricks

You may get a little frustrated because the controller/scope above your <ng-view> doesn't seem to have the $routeParams populated upon initialization. The reason for this is because until the <ng-view> tag is actually compiled, there will always be the possibility of more routes being added (via controllers in addition to the module). This means resolving the route (and route tokens) must be done asynchronously. Only a child controller of <ng-view> will have the params populated upon initialization.

Lucky for you there is a way to execute logic upon a route change. If you probe the $routeProvider API you will find that there is a $routeChangeSuccess event emitted on the scope that you can hook into for every route change (in addition to a $routeChangeStart event).

Want to set flags or arbitrary data on each route (such as setting an active tab in your view)? You can actually add arbitrary properties to your route definitions. You can then access these properties in the $routeChangeSuccess event as the second callback parameter. Look inside routeData.$route (shown above).

$routeProvider.when({ ... step: 'first' });
...
$scope.$on('$routeChangeSuccess', function(event, routeData){
  // Your $routeParams-dependent logic goes here
  $scope.step = routeData.$route.step;
});
...
Step 1

Now lets say you want to delay a switch to a new route until after you've loaded the relevant resources. Luckily $routeProvider allows you to specify to work with promises and resolve to get this done. Rather than dive into it here, go check out this discussion or this video if you're feeling lazy.

Working with Templates

You may not have been aware, but you don't have to rely on remote html partials for your <ng-include> tags. You can actually use AngularJS templates in script tags.

<script type="text/ng-template" id="someId">{{name}}</script>

<ng-include src=" 'someId' "></ng-include>

A cool trick about this is that you can 'intercept' AJAX requests by using a matching id:

<script type="text/ng-template" id="banner.htm">...</script>
<ng-include src=" 'banner.htm' "></ng-include>

But what if you want to leverage templates elsewhere in your code? You could simply do elm.find('someId') but then you would fail to benefit from Angular's automatic caching.

Instead, take a look at the $templateCache service and its horribly lacking documentation. If you want to retrieve a template, inject it into the DOM and compile it.

// inject $templateCache and $compile

// Retrieve the HTML (uncompiled)
var html = $templateCache.get('someId');
// Inject it into the DOM before compiling
elm.html(html);
// Compile / Bind the DOM
$compile(elm.contents())($scope);

elm and $scope are passed to your directive's linking function. I assume you're creating a directive because of course you're not doing this from your controller, right?

But did you know you can use partials in your $http requests too? The advantage to this is that once again, you can specify normal URLs and use intelligent id names to intercept!

$http.get('someId', { cache: $templateCache, ... })

Working with Directives

Few helpful tips when you decide to finally take a crack at directives:

  • You can require multiple controllers by doing require: ['^pane', '^panes']
  • There are various ways to define your directive:
    • return function(){}; is a shortcut for just returning the linking function
    • If you specify 1 linking function, it will be a post-linking function
    • Anywhere you can specify the linking function you can alternatively specify { pre: function(){}, post: function(){} } instead (if you can even figure out how to use it)
    • If you create a compile:function(){} it must be used to return your linking function (instead of the link key)
  • By default, restrict: 'A' which means unless you specify otherwise, they can only be used as attributes
  • It's safer to modify original DOM at compile time, but none of the values will be interpolated yet
  • If you (or a plugin) injects DOM outside (after) the current element at linking it will break the compiler's cache of DOM positions
  • Linking is done in order from parent to child, top to bottom. This means that the children elements will not be linked until the parent is done linking (commonly problematic when counting ng-repeated children)
  • You should understand when each function is executed
    • Only one container directive function is instantiated upon app load. Variables declared here will be shared across all instances of the directive
    • One compile function is instantiated for every original DOM element triggering the directive. Variables declared here will be shared across all clones.
    • One linking function is instantiated for every clone/instance (refer to ui-if or ng-repeat). Variables declared here will be unique per clone.
    • Remember when setting object references how this might cause data to bleed across clones

Working with Resources

  • ngResource is not included in the AngularJS core. You must remember to include a separate resource file.
  • ngResource module must be listed as a dependency when declaring your module.
  • You can specify a port but the : (colon) must be escaped: $resource('mydomain.com\\:8080/people')
  • At this moment you can't have access to the $promise object. This may change, or go checkout my AngularUI proposal.? I think it's a property of the object.
  • $scope.$watch() not working with $resource? That's because you need to pass the true parameter: $scope.$watch('someModel', fn(){}, true). It has to do with watching for changes by reference vs value (for performance reasons).
  • Do you want to have a file extension and support endpoints like /people/34.json and /people.json? Too bad.

Nuances of ngDisabled

You may have encountered some bits of confusion when it came to disabled form inputs. People will occasionally try doing something like <input disabled="{{someVar}}"> but this just won't behave the way they expected it to. There is actually an ngDisabled attribute that you can leverage for dynamically changing the disabled state of a form control. However, there may still be some confusion as to the different states and values that represent disabled or enabled, so to help clarify I made this simple demo:

Easy Table Sorting

I'm still coming up with an elegant way to change the classes of the current sort, but here's the logic:

...
<tr>
	<th><a ng-click="sort='status';reverse=!reverse">Status</a></th>
	<th><a ng-click="sort='updated';reverse=!reverse">Updated</a></th>
	<th><a ng-click="sort='candidates';reverse=!reverse">Candidates</a></th>
</tr>
<tr ng-repeat="items in items|orderBy:sort:reverse" ng-click="item.selected = !item.selected" ng-class="{selected:item.selected}">
 ...

I really want to create a reusable widget or directive that will handle worrying about scope of your sort/reverse variables and add classes, but alas this seems to be a pain in Angular.

I have been having a discussion on a convenient wrapper for table sorting functionality. After a bit of discussion this excellent example was created by Dan Doyon:

Efficient storing groups of checkboxes

I was building a form generator, and for every field in my array, I was using a shared 'value' slot. For radio, selects, checkboxes, text, textareas that's all fine and dandy. But what if you have a group of related checkboxes (like multiselect)? This was how I implemented storing multiple values to one slot. Note that the placeholder must be declared as an object literal beforehand for this to work. In the form generator, if the field is set to 'checkboxes' I would do this programmatically, otherwise doing nothing.

Checkout the demo:

Forcefully Compile HTML Partials

While working on some integration with existing code, we wanted to have our main app behave normally, and 1 tab work with the new Angular framework. When you click on a link, it would load the code (in all its angular glory) into the page, however how do you get angular to kick in? Originally we tried including script tags into the partial (including a reference to the angular lib itself with ng-autobind) but these appear to be completely ignored by the browser. It appears browsers don't parse async-loaded script tags, need to research and confirm this though.

Instead, we kept the angular script tag in the parent document (to preserve the ng-autobind attribute). When the partial is loaded, we then include the rest of the scripts and call compile() and $apply() to make angular 'kick in' when it otherwise wouldn't automatically. Keep in mind somePartial.htm has the ng-controller attribute. This is the equivalent (seemingly) to using <ng-include> outside of Angular.

// $container is a common jQuery variable naming convention for jQuery-wrapped DOM selectors
var $container = $('#someContainer');
$.ajax({
	type: 'GET',
	url: 'somePartial.htm',
	success: function(data){
		$container.html(data);
		// Make AngularJS kick in!
		angular.compile($container)().$apply();
	}
});

Note: if you are calling this inside an Angular controller, you can explicitly pass the scope to the compiler: angular.compile($container)(scope).$apply(); assuming that you did var scope = this;

Set a $scope variable's value from a Directive

One of the complex scenarios encountered is trying to set a value onto a $scope model. The problem is that people may specify a model using syntax such as myDirective="data.person.name". The problem with this is you'd have to dissect the expression so that you could do something similar to $scope[attrs.myDirective]. Or chances are this is exactly what you tried and it didn't work for complex expression.

Rather than manually parsing the expression, you can use the AngularJS $parse service to do this make life easier. Here is an example:

// This object is a 'parsed' version of the model, such as $scope.myData (aka: ng-model="myData")
var ngModel = $parse('myData');
// This lets you SET the value of the 'parsed' model
ngModel.assign(scope, 'newValue');
// This lets you GET the value of the 'parsed' model
ngModel(scope);

Refreshing Partials and <ng-include>

Due to stupid reasons, I was forced to refresh an already-loaded partial. Doing this with ng-include proved tricky, but Andy Joslin was nice enough to help me out so I thought I would share.

Open your inspector and watch the network or console if you're logging AJAX requests. Every time you hit refresh, it will reload the same url. What is going on is that we are manually wiping out the cache for the url by injecting $templateCache and then calling $templateCache.remove('url/passed/to/ng-include'). Then we set the url for ng-include to something else (false) and setup an immediate $timeout to put it back after the $apply() is complete. Try poking around the code.

jQuery Plugins not triggering ng-model updates?

If you've worked with form inputs containing ng-model, you may at a certain point start to wonder why some plugins or even $.fn.trigger('change') isn't updating the model. After some digging through the source, we found that Angular doesn't actually watch the change event by default. Instead it watches the new input event. Read up about the differences here.

A simple hack we did for the AngularUI passthrough directive was to bind to the change event and manually fire an input event, however I recommend instead trying to leverage the new input event where you can instead.

Form Builder Demo (v0.10.5)

When I was considering AngularJs, my company pointed out that we needed a form-generator (like Wufoo or Google Docs Form) but I told them I could slap one together in about an hour using angular. This is the fruit of that labor, it's a basic demo and could use drag-n-drop sorting and a few other enhancements but I thought it's pretty cool none-the-less:

Form Validating & Validating Too Early?

First of all, form validation at the moment is kind of lame. Yes I said it. AngularUI is trying to address this. Here is how you do it:

<form name="person" ng-submit="save()">
    <input name="name" ng-model="data.name" required>
    <input name="age" ng-model="data.age" type="number">
    <input type="submit" value="Save">
</form>
Form Level:
{{ person.$error }}
Individual Input Level:
{{ person.name.$error }}
{{ person.age.$error }}

Yes, we're using the name attributes, not the ng-model attribute. Makes sense if you think about it, you might have multiple inputs on the page all associated to the same model. What we're toying with in AngularUI is having the name, ID and label all generated automatically off of the ng-model attribute to make life easier (along with some other magic).

But wait! My error messages and styles are showing up before I even touched the form!

Ma'am, please remain calm. If you are annoyed with the page loading and all your validation rules spitting out messages, take a look at this page on pristine and dirty classes. These are classes Angular sets for you to hook into. Scraped from this thread.

Why isn't my form validation / styling kicking in when I submit?

If you are on a self-respecting browser, chances are things like the required attribute or other types of validation are starting to finally be supported by the browser natively. Only problem is that these native validation checks supersede AngularJS, and don't actually fire any JS until they pass. This means that those shiny CSS classes and error notices you worked so hard on will never actually be shown.

Lucky for you there's a way to disable that! Simply use a <form novalidate> attribute and the browser will let you do the heavy lifting. Chances are you may want to do this for every form tag on your application just to be safe.

Animating the creation of new elements (10.6)

You CAN in fact animate new content being injected into the page in CSS3. Also, you can animate state changes (or class toggles) in CSS3 using transitions. Check out the fiddle:

Want a CSS Sandbox? (webkit only)

Someone came up with this awesome trick you can do to create an iframe that's still tied into the parent window's scope. This is great if you need to reset your CSS. Keep in mind it only works on webkit browsers currently. Compliments of jrowny

QUnit Testing Examples

So Testacular is awesome but your companies forcing you to stick with QUnit? Christopher Hiller was nice enough to provide a handy example of some tests done in QUnit.

Debugging

Here is a collection of common caveats you may run into during your development to help smooth the process. And before you get started, checkout the video on how to debug the scope through the HTML

The List:

  • Scope Inheritence: Initializing a scope variable in a child controller (such as scope.data={}) will cause it to separate from the parent variable. You will then have 2 variables of the same name in both scopes that are not identical. Make sure you only initialize variables in the top-most scope, and optionally check if the variable is undeclared before initializing.
  • "$digest already in progress" error: If you get this error it's because you called $scope.$apply() from inside an already-firing $apply() (aka $digest cycle). If it's impossible to refactor to avoid this accidental recursion (recommended), then use this check: if (!$scope.$$phase) $scope.$apply()
  • Bug with ng-options: (may be fixed soon) If you have an object-literal that contains something like {id1: 'Label1', id2: 'Label2'} and try to use this as the key and value, the select it will break. View example above.
  • Show doesn't work for ng-show/hide: Turns out these directives do NOT set an element to display:block/inline-block. They only set display:none and remove it, so make sure your CSS isn't hidden by default.
  • Strict comparisons in expressions: Several (if not all) directives do not understand === comparisons in evaluated expressions. Instead use ==.
  • Useful Util Functions: We leverage controller inheritence and have a global AppCtrl. I recommend adding a scope.log() and scope.alert() function as wrappers for alert() and console.log() to make debugging from the view a little bit easier.
  • Radio inputs not grouping: If you set the model to a simple variable like ng-model="radoiVal" it won't work. Apparently radio inputs only work with an object or array attribute, so use something like ng-model="radios.value".
  • Google Chrome Plugin: Check out the new AngularJS inspector toolbar for Google Chrome called Batarang!

Useful Resources

Here is a collection of useful reading material you should checkout if you find you're getting stumped with certain concepts:

Comments