Angular — New features in AngularJS 1.4

A review of new features included in the latest AngularJS release.

Kris Temmerman

AngularJS 1.4 was released few weeks ago. This is the biggest upgrade after AngularJS 1.3 last October.

Don’t be fooled, this is neither a maintenance nor a minor release, this release is packed with features (+30) and, best of all, it will improve your application performance with up to 30% faster digests due to changes in AngularJS compiler.

Let’s explore some of these new features:

  • Revamped ngAnimate
  • New $cookies service
  • Improved Forms
  • Miscelanea: angular.merge, changes to bindToController.

As previous versions, it is very easy to upgrade, and a list of breaking changes can be found here to guide you in the process.

Follow me on Twitter for latest updates @gerardsans.

Revamped ngAnimate

Introduction

Maybe you are not familiar with this module. ngAnimate provides support for CSS-based animations (transitions and keyframes) as well as JavaScript-based animations (callback hooks). CSS-based animations use pre-defined CSS classes (like .ng-enter and .ng-leave), while JS-based animations triggers animations registered by module.animation(). In both cases, the targeted HTML element requires a CSS class used by the animation.

Browser support for ngAnimate: all modern browsers but IE9 or lower.

New ngAnimate

The new ngAnimate has been totally refactored to be more flexible, reusable and performant. It certainly is a solid base for future features in Angular 1.5 and 2. At ng-conf the candidates were runners, custom drivers and reversible animations.

Regardless future plans, Angular 1.4 comes with some powerful features like imperative CSS-based animations, new callback and promises support and anchoring.

Another addition is the ability to use animation callbacks from controllers

// 1.4+
ngModule.controller('MyController', ['$animate', function($animate){
$animate.on('enter', ngViewElement, function() {
// the page is ready!
});
}])

As with any major release, some breaking changes apply. Find a detailed list here.

$animateCss

Starting with Angular 1.4, JavaScript and CSS animations can no longer be run in parallel. Instead, $animateCss service will take control of them.

By using its API, we can imperatively define what kind of animation we want. Then, only when we call start() it will figure it out how to translate our setup into an animation using the right CSS properties for the job. See an example below.

// 1.4+
myModule.animation('.slide', ['$animateCss', function($animateCss) {
return {
enter: function(element, doneFn) {
//setup animation
var animation = $animateCss(element, {
event: 'enter',
addClass: 'my-class',
from: { height: 0 },
to: { height: 200 }
});
//trigger animation
animation.start();
//setup done callback
animation.done(doneFn);
}
}
}]

Besides interacting with CSS properties, we can also add or remove CSS classes with addClass and removeClass. Check out all the available options here. Calling $animateCss returns an instance with two methods:

  • start(), triggers the animation and returns a promise.
  • done(), signals the end of the animation.

These leads us into the next section.

Callbacks and Promises support

The new syntax to interact with animations has been simplified. For example, you don’t have to wrap your code with $scope.$apply() any more. See an example of the evolution from previous versions below.

// 1.2+
element.addEvent('$animate:enter', function() {
// do something
});
// 1.4+
$animate.on('enter', container, function() {
// do something
});

Calls to $animate now return a promise that we can use to add some behaviour when the animation ends.

// 1.4+
$animate.enter(element, parent).then(function() {
//the animation has completed
});

Using promises with $animateCss comes with a performance penalty as using animation.then(), animation.catch() or animation.finally() will trigger a digest cycle. If you don’t need it, use animation.done(doneFn) instead.

Use this Plunker to practice with $animateCss and promises.

Anchoring

New ngAnimate allows us to cross-animate elements between views by using anchoring. Let’s take the example from the documentation using a home page with a list of record titles. When you click on a record a detail page will show the selected title in big letters. Run this demo to see it live.

In order to pair the elements we use ng-animate-ref with the same id. ngAnimate uses this information to progress from one element to the other. It automatically creates a transport element that transitions between the initial and the end states. To style this transition we can use .ng-anchor.

To pair the <a> and <div> elements we used the record.id in ng-animate-ref and the class .record as the transport class.

// 1.4+
<!-- index.html -->
<div class="view-container">
<div ng-view class="view"></div>
</div>

<!-- home.html -->
<a class="record"
ng-href="#/profile/{{record.id}}"
ng-animate-ref="{{record.id}}"
ng-repeat="record in home.records">
{{record.title}}
</a>

<!-- profile.html -->
<div class="profile record" ng-animate-ref="{{profile.id}}">
{{profile.title}}
</div>

.ng-anchor has two phases that we can style separately with .ng-anchor-in and .ng-anchor-out.

You need to use the .record class in both elements (a, div) for the animation to work.

When the user navigates to a profile page the element with ng-animate-ref will transition as specified by ng-anchor. All ngAnimate classes will be removed when the animation ends.

.record.ng-anchor {}
.record.ng-anchor-in {}
.record.ng-anchor-out {}

In the background, the view is still going through the stages below when the user navigates from the home page to the profile view. These animation classes are determined by ngAnimate and ng-view. Chek out this table for other directives.

(home view element) .view.ng-leave {}
(home view element) .view.ng-leave.ng-leave-active {}
(profile view element) .view.ng-enter {}
(profile view element) .view.ng-enter.ng-enter-active {}

Use this Plunker to familiarise with the previous example.

New $cookies service

In this release, ngCookies module replaces $cookieStore with the new $cookies service. The new interface is very easy to use and allows you to set cookie options for individual cookies as well as configuring defaults via the $cookiesProvider.

See below how to pass individual cookie options for put, putObject and remove.

// 1.4+
var config = {
path: '/a/b',
domain: 'www.mywebsite.com',
expires: new Date(),
secure: true
};
$cookies.put('name', 'value', config);
$cookies.putObject('name', 'value', config);
$cookies.remove('name', {path: '/a/b'});

To set the defaults for the whole application use $cookiesProvider during the configuration phase

// 1.4+
someModule.config(function($cookiesProvider) {
$cookiesProvider.defaults = {
domain: ‘www.mydomain.com’,
secure: true
};
});

Try different options using this Plunker.

Improved Forms

Dynamic ngMessages

If you need to apply dynamic validations this feature will come handy. Before we could only define a fixed set of ngMessages. Starting with 1.4 we can build ngMessages dynamically on our controller. In order to do that we can use ng-message-exp. Let’s see an example of ng-messages before 1.4.

// 1.3
<form name="form1">
<label>Email</label>
<input type="email" ng-model="email1" name="email1" required minlength="5" maxlength="15" pattern="[a-z]+[a-z0–9._]+@[a-z]+\.[a-z.]{2,5}" />
<div ng-messages="form1.email1.$error" class="validation">
<div ng-message="required">You did not enter your email address</div>
<div ng-message="minlength, maxlength">Your email must be between 5 and 15 characters long</div>
<div ng-message="pattern">Not a valid email</div>
</div>
</form>

To learn more about AngularJS 1.3 chek out this post.

Now we can use expressions to link model values to ngMessages and use structural directives to update the list accordingly (Eg: ng-repeat, ng-if, ng-switch). Avoid using these directives in the same element with ngMessage, instead use a parent element like in the code below:

// 1.4+
<div ng-messages="myForm.myEmail.$error">
<div ng-if="showRequiredError">
<div ng-message="required">Please enter something</div>
</div>
</div>

The initial example now can be translated to

// 1.4+
<form name="form3">
<label>Email</label>
<input type="email" ng-model="email3" name="email3" ng-minlength="5" ng-maxlength="15" required ng-pattern="[a-z]+[a-z0–9._]+@[a-z]+\.[a-z.]{2,5}" />
<div ng-messages="form3.email3.$error">
<div ng-repeat="m in messages">
<div ng-message-exp="m.expression">{{m.description}}</div>
</div>
</div>
</form>

Find a demo of the previous example here.

ngModelOptions timezone support

This is a nice addition to ngModelOptions introduced in AngularJS 1.3 to set up how your model updates are done.

// 1.3
<input type="text" ng-model="search" ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }">

In the example above, search model will be updated for default events (Eg: keydown) and on blur with a debounce of 500ms and 0ms respectively.

Now you can also set a specific timezone. If none is specified or recognised, it will default to the browser’s timezone.

// 1.4+
<input type="datetime" ng-model="meetingDateTime" ng-model-options="{timezone: '+0100'}">

A use case for this would be a global application with dates applying only to a specific timezone. So user input will be set to that timezone regardless of user’s browser local time. See demo in Plunker.

Miscelanea

angular.merge(destination, source)

angular.merge complements previous angular.copy and angular.extend. It does a deep copy of all properties from source to destination preserving properties in child objects. Note how we can also use multiple source objects that will be merged in order.

// 1.4+
angular.merge(destination, source);
angular.merge(destination, source [, source]);

Preserve source objects by using this notation shown below

// 1.4+
var merged = angular.merge({}, object1, object2)
//angular.merge(merged, object1, object2) // equivalent

Let’s see the difference between previous angular.extend and angular.merge with one example. Let’s take two object literals person1 and person2

// 1.4+
var person1 = {
name: 'John',
address : {
description: 'Oxford Street'
}
}
var person2 = {
id: 1,
address : {
postcode: 'SW1'
}
}
var extended = angular.extend(person1, person2);
var merged = angular.merge(person1, person2);

The extended object will look like

{
id: 1,
name: 'John',
address : {
postcode: 'SW1'
}
}

Notice the id property, and how we have lost the description property. This is because angular.extend copied person2.address into person1.address.

Let’s see what happened to the merged object

{
id: 1,
name: 'John',
address : {
description: 'Oxford Street',
postcode: 'SW1'
}
}

Here all child properties in address are copied over. Try other combinations in this Plunker.

Changes to bindToController

Usage has been improved allowing a more intuitive bindToController syntax using scope attributes.

<my-records hash="A3F9" records="ctrl.records" close="ctrl.close()"></my-records>
// 1.3
bindToController: true,
scope: {
hash: '@',
records: '=',
close: '&'
}
// 1.4+
bindToController: {
hash: '@',
records: '=',
close: '&'
},
scope: {}

These were some of the new features on AngularJS 1.4. Hope you like them!

Think I missed some features? Contact me on @gerardsans or gerard.sans_at_gmail.com. Thanks!

Resources