AngularJS: $onChanges component hook as solution for not ready bindings.

Photo credits: Miguel Á. Padriñán

In recent months I spent some time to figure out how to wait for bindings in Angular component. $onChanges was the solution — but meanwhile I found one ugly and one nice answer to the problem.

Use case

It is common case that you need to load some elements from server and display it in HTML. It’s also common case that you create directive/component for elements, to be able to reuse it in other places in application, and to separate future changes in way that element is displayed. For example, if you load user’s messages and decide that message title should be displayed with h1 tag instead h2, you make a change in component’s template — change occurs in exactly one place and other files remain untouched. Example code for parent component controller might look like this:

function MailboxController (MessagesService) { 
var $ctrl = this;
  $ctrl.$onInit = function () { 
MessagesService
.load()
.then(_bindMessages);
}

function _bindMessages (messages) {
$ctrl.messages = messages;
}
}

It’s a good practice to place any initialization logic always in $onInit — first, you avoid mess related to mixing declaration and usage of function (before components it was common to use activate function from John Papa’s Angular 1 Style Guide). Second, starting with Angular 1.6, you can rely on component’s bindings only in $onInit (from Todd Motto’s blog):

Using $onInit guarantees that the bindings are assigned before using them.

And third, there are cases where $onInit hook can significantly simplify unit tests. But returning back to the merits — messages are loaded and used in template in this way:

<messages 
elements="$ctrl.messages"
>
</messages>

And here is messages component, let’s say we want to display an alert when user has more than 10 unread messages:

function MessagesController () { 
var $ctrl = this;
  $ctrl.$onInit = function () { 
var unreadMessages = $ctrl.elements.filter(_isUnread);
$ctrl.showUnreadAlert = unreadMessages.length > 10;
}

function _isUnread (element) {
return !!(element.unread); // cast unread property to bool
}
}
function messagesComponent () { 
return {
bindings: {
elements: "<",
controller: MessagesController,
templateUrl: "messages.component.html" // not important
}
};
}
angular.module(“app”).component(messagesComponent());

It looks pretty nice, but in might not work.

Problem

Of course — problem is that messages may not be loaded yet when messages component is initialized. undefined is bound to elements input which causes error: filter method can not be called on undefined value. And exactly this problem I had in my 9–5 work.

First attempt to find a solution

The most obvious answer was to add additional ng-if to component usage, something like:

<messages 
elements="$ctrl.messages"
ng-if="$ctrl.messages"
>
</messages>

Which I didn’t find readable, so I added a boolean set to true when messages where loaded, which seemed to be a good answer:

<messages 
elements="$ctrl.messages"
ng-if="$ctrl.messagesLoaded"
>
</messages>
// And in MailboxController:
function _bindMessages (messages) {
$ctrl.messages = messages;
$ctrl.messagesLoaded = true;
}

Which, in retrospect, was even worse solution — I had easy to forget boolean and I had to always touch component’s controller to set it. And as it usually happens, bad solution spread out in entire application.

Nice solution found in the Internet

Few weeks later, on Daniel Niederberger’s blog, in article Wait for the Bindings of a Directive in Angular I found much clearer solution — just add a wrapping component! Just like:

<messages 
elements="$ctrl.messages"
>
</messages>

And then in messages template (If somebody dive into wrapping component and will be surprised why there is additional component, it can be explained with comment):

<!-- Needed because messages can be loaded asynchronously  -->
<messages-impl
elements="$ctrl.elements"
ng-if="$ctrl.messages !== undefined"
>
</messages-impl>

Easy to implement, easy to find out why it is done in this way even few months later. But today, with Angular 1.5.3 (and newer) and $onChanges component hook, even more cleaner way is possible.

Dealing with not ready bindings with $onChanges hook

Everyone who follows the changes in modern Angular development should be familiar with $onChanges component hook. Few months ago I wrote how $onChanges can be used to switch between Youtube and Vimeo players, and waiting for asynchronous bindings can be solved in the same way.

Inside $onChanges implementation we check elements are resolved, and if they are we calculate whether to display alert or not:

function MessagesController () { 
var $ctrl = this;
  $ctrl.$onInit = function () {}
  $ctrl.$onChanges = function (changes) { 
if (_areElementsResolved(changes)) {
var unreadMessages = $ctrl.elements.filter(_isUnread);
$ctrl.showUnreadAlert = unreadMessages.length > 10;
}
}
  // elements may be loaded asynchronously
function _areElementsResolved (changes) {
return changes.elements && Array.isArray(changes.elements.currentValue);
}

function _isUnread (element) {
return !!(element.unread); // cast unread property to bool
}
}

Additional function was introduced to check presence of elements changes — it makes it clearer to figure out what it do and it’s nice place to put comment — remember, there is no such thing as too much information.

$onChanges has two main advantages:

  1. It does not require wrapping component
  2. It allows component to react when, for example, elements are reloaded — with previous solutions component has to be recreated, by setting messagesLoaded flag to false or settings messages to undefined.

And I think is more readable than previous proposals. In my opinion,in general $onChanges is the most useful of all hooks and I will prefer $onChanges than $onInit — because binding which is synchronous today, can be asynchronous in future (requirements can change), and usage of $onChanges means less changes in code in future.

Conclusion

Guys from Angular team took a big step forward from 1.4.x to 1.5.x (and now 1.6.x) version. If you are not familiar with changes, I really encourage you to dive into new features, which make our life much easier.

Like what you read? Give Radek Anuszewski a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.