Angular nuggets

Helpful AngularJS code snippets.

Related: JavaScript nuggets

Visually arrange promise chains

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$http({ method: 'PUT', url: url }).success(function onArchivedIssue() {
console.debug('archived issue', issue);
$scope.issues = _.without($scope.issues, issue);
}).error(function onArchiveError(err) {
console.error('could not archive issue', issue, err);
});
// becomes
$http({
method: 'PUT',
url: url
}).success(function onArchivedIssue() {
...
}).error(function onArchiveError(err) {
...
});

Watch out for if-else-else

1
2
3
4
5
6
7
8
9
$scope.$watch('authState', function() {
var bodyClass = "";
if ($scope.authState === "authenticated") {
bodyClass = "app";
} else if ($scope.authState === "anonymous") {
bodyClass = "login";
}
$scope.bodyClass = bodyClass;
});

bodyClass = ""; is implicit second -else in this case.

Fragile

Two security threats in this case:

  • bodyClass should be properly initialized
  • $scope.authState should be properly initialized. Is it "" or "anonymous"?

Better

1
2
3
4
5
var stateToBody = {
authenticated: "app",
anonymous: "login"
};
$scope.bodyClass = stateToBody[$scope.authState] || "";

Works great for messages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var msg = "";
if(resultCode === 401){
msg = "User and password do not match";
}else if(resultCode === 403){
msg = "Access denied";
} else {
msg = "An exception occurred";
}
// becomes
var errorMessages = {
401: "User and password do not match",
403: "Access denied"
};
var msg = errorMessages[resultCode] ||
"An exception occurred";

Write custom filters

1
2
3
4
5
6
7
8
9
10
backlog.filter('issueIcon', function () {
return function (value) {
var issueIcons = {
bug: 'bug',
review: 'comments',
crash: 'exclamation-sign'
};
return issueIcons[value] || 'question';
};
});

use it like this ``

Pass scope to filters

1
2
3
4
5
6
7
8
9
10
backlog.filter('buildName', function () {
return function (value, scope) {
if (value) {
return value;
}
return Array.isArray(scope.versions) ?
scope.versions.length : 'All';
};
});
// use <button>Build {{selectedBuildId | buildName:this}}</button>

Avoid long chains

1
2
3
4
5
6
7
8
9
10
11
angular.module('App', [
'ui.route'
])
.config(function myAppConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/issues');
})
.run(function run($rootScope, titleService, $http, $location) {
titleService.setSuffix(' | products');
})
// chain has multiple levels,
// hard to see individual functions

Split

1
2
3
4
5
6
7
8
9
var app = angular.module('App', [
'ui.route'
]);
app.config(function myAppConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/issues');
});
app.run(function run($rootScope, titleService, $http, $location) {
titleService.setSuffix(' | products');
});

Don not leak variables

1
2
3
4
5
6
7
8
9
10
// issues.js
var issues = angular.module('issues', [
'ui.router',
'titleService',
'productDetails'
]);
issues.config( ... );
issues.controller('IssuesCtrl', ...);
// issues is now global variable!
// check in DevTools console: issues

Hide variables: IEFE

1
2
3
4
5
6
7
8
9
10
// issues.js
(function (angular) {
var issues = angular.module('issues', [
'ui.router',
'titleService',
'productDetails'
]);
}(angular));
// issues is local
// access to angular is probably faster

Wrap in IEFE on concat

1
2
3
4
5
6
7
8
9
10
11
12
// gruntfile.js
concat: {
src: [
'<%= vendor_files.js %>',
'module.prefix',
'<%= build_dir %>/src/**/*.js',
'<%= html2js.app.dest %>',
'<%= html2js.common.dest %>',
'module.suffix'
],
dest: '<%= compile_dir %>/assets/<%= pkg.name %>.js'
}

Module.prefix and suffix

1
2
3
4
5
// prefix
(function ( window, angular, undefined ) {
// all our angularjs source code here
// suffix
})( window, window.angular );

note: variables are still shared among individual files, unless each file is wrapped in IEFE.

Use inline annotation

Minification can break dependency injection by replacing $http with foo, so we need to write:

1
2
3
4
5
.controller('ProductIssuesCtrl',
['$scope', '$http' , '$stateParams', 'titleService',
function($scope, $http, $stateParams, titleService) {
...
}]);

Use inline annotations for all

Use inline annotations everywhere, not just in controllers

1
2
3
4
5
6
7
8
.config(function myAppConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/issues');
})
// becomes
.config(['$stateProvider, $urlRouterProvider',
function myAppConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/issues');
}]);

Split functions from inline annotations

to get back to elegant code

1
2
3
4
5
6
var app = ...
function appConfig($stateProvider, $urlRouterProvider) {
// use any parameter names, no DI here
$urlRouterProvider.otherwise('/issues');
}
app.config(['$stateProvider, $urlRouterProvider', appConfig]);

Count parameters

Do not waste dependency injection

1
2
3
4
5
6
7
.run(function run($rootScope, titleService, $http, $location) {
titleService.setSuffix(' | products');
})
// becomes
.run(function run(titleService) {
titleService.setSuffix(' | products');
})

Use $http shortcuts

1
2
3
4
5
6
7
8
9
$http({ method: 'GET', url: productsUrl }).success(...)
// better
$http({
method: 'GET',
url: productsUrl
})
.success(...)
// best
$http.get(productsUrl).success(...)

Inject dependencies in correct places

When writing AngularJs, dynamic dependency injection is everywhere: in controller functions, modules, filters. While injecting into controller function is obvious, it is unclear how to inject dependencies into link or filter functions. You can do the injection in these cases like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// directive and controller
angular.module('MyApp', [])
.directive(['$scope', 'MyDependency', function ($scope, MyDependency) {
// injected variables to be used inside link function
return {
restrict: 'E',
scope: true,
templateUrl: 'study-line-chart/study-line-chart.tpl.html',
controller: ['$scope', 'utils', function ($scope, utils) {
// separate injection if necessary for controller
}],
link: function (scope, element, attrs) {
// scope === $scope and MyDependency injected variables
}
};
}])
.filter('formattedName', ['MyUtils', function (myUtils) {
// myUtils is injected into filter
return function (name) {
return myUtils.formatName(name);
};
}]);

Prevent silent module / controller / filter overrides

Angularjs allows overriding modules at any time. Good for testing, awful for working as a team. I wrote stop-angular-overrides script that throws an Error on any override attempt. More examples in this blog post.

In general Angular is silent on errors, but you can detect problems yourself, see Avoiding silent angular failures.

Working with Angular forms

Working with angular forms is much simpler if you use meaningful input names that follow dot notation.

1
2
3
4
5
6
7
8
9
10
11
12
<form name="myForm" novalidate>
<div class="col-md-4 temp-var form-group">
<label for="start_day">Start Day</label>
<input type="number"
id="start_day"
name="start_day"
class="form-control"
ng-model="myVars.start_day"
placeholder="Start Day (-1)"
required/>
</div>
</form>

We gave form and input fields names that are valid JavaScript identifiers. This gives us an advantage when we query the state of the form from the browser console

1
2
3
4
5
6
7
8
9
10
11
12
13
// grab element's angular wrapper
var el = angular.element('form[name="myForm"]');
var scope = el.scope();
// get the form structure, automatically created by angular
var form = scope.myForm;
// has user touched start day input?
form.start_date.$pristine;
// has user entered something in start date?
form.start_date.$dirty;
// is start date input valid?
form.start_date.$valid;
// which validation fails for start_date input?
form.start_date.$error

Because form data is placed in the scope, we can watch it just as any other scope expression

1
2
3
$scope.$watch('myForm.start_date.$dirty', function (newStartDate) {
...
});

Finally, the entire form has $valid, $dirty, etc properties on the scope that you can use to enable / disable the submit button. The angular form automatically computes these properties from the children inputs.

1
<button type="submit" ng-disabled="myForm.$invalid">Submit</button>

Using Angular services outside Angular application

You do not need to run the entire Angular application to use separate services. For example if you need just promises, you can inject $q service and use it in your non-Angular code.

1
2
3
4
5
6
7
function asyncFoo() {
var $q = angular.injector(['ng']).get('$q');
return $q.when('foo');
}
asyncFoo().then(function (value) {
// value is 'foo'
});

Defaults object with Angular

Often we need to combine options object passed into a function with defaults object. If there is the same propety in both objects, the options object should overwrite the defaults. This can be done using a composition of two angular built-in functions

1
2
3
4
5
6
7
8
9
10
11
12
function defaults(_defaults, opts) {
opts = opts || {};
return angular.extend(angular.copy(_defaults), opts);
}
function foo(options) {
var _defaults = {
bar: 'baz'
};
options = defaults(_defaults, options);
}
foo({ bar: 'my bar' }); // options is { bar: 'my bar' }
foo(); // options is { bar: 'baz' }

Print a message every time digest cycle runs

1
2
3
4
5
var $rootScope = injector.get('$rootScope');
function dummy() {
console.log('digest cycle');
}
window.stopWatching = $rootScope.$watch(dummy);

If you want to stop print the message, just call window.stopWatching() to remove the watched dummy function.

Extra

  • ng-nuggets has excellent advanced stuff. Highly recommended.