I'm Todd, I teach the world Angular through @UltimateAngular. Conference speaker and Developer Expert at Google.

Angular 1.6 is here, this is what you need to know

Angular 1.6 just got 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.

The purpose of this article is to focus on the most important changes, $onInit and ngModelOptions inheritance.

The official documentation guides you through some of the smaller nitty gritty stuff that likely won’t affect you as much.

Table of contents

Component and $onInit

Let’s begin with the most important update and recommendation if you’re using Angular 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 Angular 1.x and Angular 2 comparison momentarily before we step back to 1.6 as this will add some reason behind this change.

Assume our Angular 1.x 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 2, 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 Angular 1.x or Angular 2. 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 ancestor ngModelOptions, however ngModelOptions can now also inherit from an ancestor ngModelOptions

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 1.x applications with the most up-to-date practices.

Huge shout out to Pete Bacon Darwin - lead Angular 1.x 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 2 practices, but better practices in general.

Todd Motto

I'm Todd, I teach the world Angular through @UltimateAngular. Conference speaker and Developer Expert at Google.