AngularJS 1.6 was just released! Here’s the low down on what to expect for the component method changes as well as $http
Promise method deprecations and the amazing new ngModelOptions
inheritance feature.
Table of contents
The purpose of this article is to focus on the most important changes,
$onInit
andngModelOptions
inheritance.
The official documentation guides you through some of the smaller nitty gritty stuff that likely won’t affect you as much.
Component and $onInit
Let’s begin with the most important update and recommendation if you’re using AngularJS 1.5 components and their lifecycle hooks.
Initialisation logic
When we initialise a particular model on a component’s controller instance, we’d typically do something like this:
const mortgageForm = {
template: `
<form>
{{ $ctrl.applicant | json }}
</form>
`,
controller() {
this.applicant = {
name: 'Todd Motto',
email: '[email protected]'
};
}
};
angular.module('app').component('mortgageForm', mortgageForm);
Note: here we are not using
$onInit
, but can set an Object on a controller, pay attention to this next bit
However, when we switch this to using input bindings
- which is likely the case in a real world application - 1.6 introduces a new change. Let’s assume this:
const mortgageForm = {
bindings: {
applicant: '<'
},
template: `
<form>{{ $ctrl.applicant | json }}</form>
`,
controller() {
console.log(this.applicant);
}
};
The template
will of course render out the Object passed into the binding, however - the this.applicant
log inside the controller will NOT be available. To “fix” this (it’s not essentially a fix though), you’ll need to use $onInit
:
const mortgageForm = {
bindings: {
applicant: '<'
},
template: `
<form>{{ $ctrl.applicant | json }}</form>
`,
controller() {
this.$onInit = () => {
console.log(this.applicant);
};
}
};
Better yet, use an ES6 class or Typescript:
const mortgageForm = {
bindings: {
applicant: '<'
},
template: `
<form>{{ $ctrl.applicant | json }}</form>
`,
controller: class MortgageComponent {
constructor() {}
$onInit() {
console.log(this.applicant);
}
}
};
Using $onInit
guarantees that the bindings
are assigned before using them.
The reasoning behind this change is that it is not idiomatic JavaScript to bind properties to an Object before its constructor has been called - which also prevents people from using native ES6 classes as controllers.
$onInit and ngOnInit
Let’s look at an AngularJS 1.x and Angular v2+ comparison momentarily before we step back to 1.6 as this will add some reason behind this change.
Assume our AngularJS component using an ES6 class:
const mortgageForm = {
...
template: `<form>...</form>`
controller: class MortgageComponent {
constructor(MortgageService) {
this.mortgageService = MortgageService;
}
$onInit() {
this.mortgageService.doSomethingWithUser(this.applicant);
}
}
};
In Angular, we’d do the following:
@Component({
selector: 'mortgage-form',
template: `<form>...</form>`
})
class MortgageComponent implements OnInit {
constructor(private mortgageService: MortgageService) {}
ngOnInit() {
this.mortgageService.doSomethingWithUser(this.applicant);
}
}
This approach treats the constructor
function as the “wiring” of dependencies to be injected, be it AngularJS or Angular. If you’re planning to upgrade your codebase, then adopting this strategy as early as possible is a huge win.
Re-enabling auto-bindings
If you’re upgrading to 1.6 and can’t change your entire codebase at once, you can drop in this configuration to enable the bindings back so they work outside $onInit
:
.config(function($compileProvider) {
$compileProvider.preAssignBindingsEnabled(true);
});
This should really be done as a temporary solution whilst you’re switching things across to $onInit
. Once you’re done making necessary changes, remove the above configuration.
Note: the configuration will be application-wide, so keep this in mind.
Recommendation
Always use $onInit
, even if you’re not accepting bindings
. Never put anything, besides public methods on your controller
.
A small example to demonstrate what will no longer work in 1.6, and will require $onInit
:
const mortgageForm = {
bindings: {
applicant: '<'
},
template: `<form>...</form>`
controller: function () {
if (this.applicant) {
// not accessible, needs $onInit
this.applicant.name = 'Tom Delonge';
}
}
};
Remember: this change means bindings inside the
constructor
, are undefined and now require$onInit
A fuller example with bindings and public methods to illustrate (however the bindings could be omitted here and your properties must exist inside $onInit
):
const mortgageForm = {
bindings: {
applicant: '<'
},
template: `<form>...</form>`
controller: class MortgageComponent {
constructor(MortgageService) {
this.mortgageService = MortgageService;
}
$onInit() {
// initialisation state props
this.submitted = false;
this.resolvedApplicant = {};
// service call
this
.mortgageService
.getApplication(this.applicant)
.then(response => {
this.resolvedApplicant = response;
});
}
updateApplication(event) {
this.submitted = true;
this.mortgageService.updateApplication(event);
}
}
};
That brings us to the end of $onInit
and the changes you need to consider when migrating and the reasons for doing so.
$http success() and error()
The legacy .success()
and .error()
methods have finally been removed - please upgrade your applications to align with the new Promise API.
Refactoring to then()
You’ll potentially have code that looks like this:
function MortgageService() {
function handleSuccess(data, status, headers, config) {
// use data, status, headers, config
return data;
}
function handleError(data, status, headers, config) {
// handle errors
}
return {
getApplication(applicant) {
return $http
.get(`/api/${applicant.id}`)
.success(handleSuccess)
.error(handleError);
}
}
}
The Promise
would then be returned for a typical then()
callback somewhere in your component’s controller. The success and error methods are deprecated and have been removed.
You’ll need this instead (note everything is contained in the response
argument instead):
function MortgageService() {
function handleSuccess(response) {
// use response
// response: { data, status, statusText, headers, config }
return response.data;
}
function handleError(response) {
// handle errors
}
return {
getApplication(applicant) {
return $http
.get(`/api/${applicant.id}`)
.then(handleSuccess)
.catch(handleError);
}
}
}
Or some ES6 destructuring fun:
function MortgageService() {
// argument destructuring
function handleSuccess({data}) {
return data;
}
function handleError(response) {
// assignment destructuring
const { data, status } = response;
// use data or status variables
}
...
}
Let’s move onto the new ngModelOptions
feature.
ngModelOptions and inheritance
Using ngModelOptions
allows you to specify how a particular model updates, any debounce on the model setters (which in turns forces a $digest
) and what events you’d like to listen to.
An example:
<input
type="text"
name="fullname"
ng-model="$ctrl.applicant.name"
ng-model-options="{
'updateOn': 'default blur',
'debounce': {
'default': 200,
'blur': 0
}
}">
This tells ng-model
to update the models on the default
events, such as paste
and input
. It also specifies a debounce
to delay model setting. On the default
event, this delay is set to 200
milliseconds - meaning you are not forcing a $digest
every key stroke, but waiting until 200ms
after the user has finished the operation. This also ties nicely with backend API requests, as the model will not be set - therefore no request is made every keystroke to the API.
Similarly, the blur
is set to 0
, meaning we want to ensure the models are changed as soon as a blur occurs, which works nicely with things like validation errors or hitting a backend API.
Repetition problem
Using ngModelOptions
is the best performance enhancement you can give your inputs, but when you have multiple <input>
nodes, you end up with something like this:
<input
type="text"
name="fullname"
ng-model="$ctrl.applicant.name"
ng-model-options="{
'updateOn': 'default blur',
'debounce': {
'default': 200,
'blur': 0
}
}">
<input
type="email"
name="email"
ng-model="$ctrl.applicant.email"
ng-model-options="{
'updateOn': 'default blur',
'debounce': {
'default': 200,
'blur': 0
}
}">
<input
type="text"
name="postcode"
ng-model="$ctrl.applicant.postcode"
ng-model-options="{
'updateOn': 'default blur',
'debounce': {
'default': 200,
'blur': 0
}
}">
At which point, it becomes messy very quickly.
Using inheritance
This is something I’ve been waiting for in 1.x for too long. Scoped inheritance for ngModelOptions
!
Let’s take a look at that first example and refactor it, as we’re using the same options everywhere:
<form ng-submit="$ctrl.onSubmit()" ng-model-options="{
'updateOn': 'default blur',
'debounce': { 'default': 200, 'blur': 0 }
}">
<input
type="text"
name="fullname"
ng-model="$ctrl.applicant.name">
<input
type="email"
name="email"
ng-model="$ctrl.applicant.email">
<input
type="text"
name="postcode"
ng-model="$ctrl.applicant.postcode">
</form>
ngModel
was always able to inherit from an ancestorngModelOptions
, howeverngModelOptions
can now also inherit from an ancestorngModelOptions
Let’s look at ngModelOptions
inheriting from ngModelOptions
.
Control level optional inheritance
We also have the ability to override specific options, whilst inheriting others using $inherit
.
Let’s assume our entire form is going to use the same options, however what we want is to only update our postcode
input on the blur
event:
<form ng-submit="$ctrl.onSubmit()" ng-model-options="{
'allowInvalid': true,
'updateOn': 'default blur',
'debounce': { 'default': 200, 'blur': 0 }
}">
<!-- omitted other inputs for brevity -->
<input
type="text"
name="postcode"
ng-model="$ctrl.applicant.postcode"
ng-model-options="{
'*': '$inherit',
'updateOn': 'blur'
}">
</form>
The above '*'
uses the wildcard to tell ngModelOptions
to inherit all options from the parent - so you don’t have to keep repeating them but can fine-tune individual inputs. This is extremely powerful and productive.
We can also optionally choose to fallback to ngModelOptions
default values (not the ones specified on the parent container) if we omit the wildcard $inherit
. For example:
<form ng-submit="$ctrl.onSubmit()" ng-model-options="{ 'allowInvalid': true, 'updateOn': 'default blur', 'debounce': { 'default': 200, 'blur': 0 } }"> <!-- omitted other inputs for brevity --> <input type="text" name="postcode" ng-model="$ctrl.applicant.postcode" ng-model-options="{ 'updateOn': '$inherit' }"> </form>
This new ngModelOptions
binding will in fact override the entire inheritance chain for that particular input - however it does inherit the updateOn
property.
You can obviously mix and match these for your specific use case, but these three examples give you global inheritance, omitted value inheritance and single property inheritance.
Check out the documentation for
ngModelOptions
for any further information
Migration / changelog
Check the Angular 1.6 migration guide for some of the other noteworthy enhancements (such as the improve input[type=range]
support) - for me these were the big ones to ensure you’re writing your AngularJS applications with the most up-to-date practices.
Huge shout out to Pete Bacon Darwin - lead AngularJS developer - and other collaborators that have been working on this release for so long. Angular 1.x is very much alive and it’s direction is being steered not only towards Angular practices, but better practices in general.