AngularJS component binding types — my experiences.

No one wants to waste time — photo credits: Eugene Shelestov.

Component bindings from Angular 1.5 had few difficulties. Although my problems may seem simple for experienced developers, I hope this post will help to protect somebody from unnecessary waste of time.

One-way (“<”) data bindings

Problem: passing value vs passing reference

One-way data bindings are quite well-known and really helpful with clear data flow (new data always come from parent), but it’s easy to mess up — when you pass object/array to binding you can unwillingly manipulate it’s values inside a component. This is JS behavior, not Angular fault — but it’s easy to think that Angular will take care about it, while it won’t.

What is real example of this situation? Imagine you have a select component which encapsulates common behavior like option translation, proper styling etc, and it takes array with options as binding:

<select-component 
options="$ctrl.options"
></select-component>

Solution: always copy and assign to new fields non-primitive inputs.

Just use angular.copy(). But it has to be done wisely.

Code like this won’t work:

function ComponentController ($window) { 
var $ctrl = this;
$ctrl.internalOptions = $window.angular.copy($ctrl.options);
}

Because bindings are not assigned before $onInit() hook beginning from Angular 1.6.

So, the second attempt may include $onInit:

function ComponentController ($window) { 
var $ctrl = this;

$ctrl.$onInit = function () {
$ctrl.internalOptions = $window.angular.copy($ctrl.options);
}
}

While $onInit has its use cases, it can’t deal with changes which come later. So what now? $onChanges hook should be used:

  1. It deals great with later changes, which occur after component is initialized, when input value changes.
  2. What is even more important, $onChanges is great solution when value can be loaded asynchronously, and bindings are empty even in $onInit.

So, final code:

function ComponentController ($window) { 
var $ctrl = this;

$ctrl.$onChanges = function (changes) {
if (changes.options) {
$ctrl.internalOptions = $window.angular.copy($ctrl.options);
}
}
}

(You can easily simulate asynchronous loading behavior with $q.when())
changes.options.currentValue can be used as source to copy as well, but inside $onChanges hook changes are also available in options input itself.
So, my way of avoiding problems with references passed to inputs is to copy them inside $onChanges hook.

Expressions (“&”) bindings

First problem: how the heck is it working at all?

For a long period of time, I have problem with understanding how expressions bindings works, with components and also, with directives — so you have an idea how long took me to understand it. So I used to pass it as simple, two-way binding:

var selectComponent = { 
controller: ComponentController,
templateUrl: 'select-component.html',
bindings: {
changeHook: '=',
},
};
angular.module("app").directive("selectComponent", selectComponent);
function ComponentController () { 
var $ctrl = this;

$ctrl.$onChange = function () {
$ctrl.changeHook($ctrl.name, $ctrl.value);
}
}
// And component usage
<select-component
change-hook="$ctrl.updateModel"
></select-component>

But I knew that this was ugly by-pass for my lack of knowledge, experience and lack of desire to read more about it.

Solution of first problem: really quick explanation

I read great article by Aviv Ben-Yosef: Understanding Angular’s & Binding and I saw a hope. Despite the fact that I won’t use expression binding in other way that as function binding (but maybe I will see things like on-change="$ctrl.value=value" in future, so thank you Aviv for explanation!), it’s as easy as that:

  1. You provide a callback like change-hook="$ctrl.updateModel(name, value)"
  2. If you want to call this callback inside component with internal component values, you provide an object with names of parameters as keys: $ctrl.changeHook({name: $ctrl.name, value: $ctrl.value}). Key thing is that key names should be exactly the same as in HTML where you provide callback to component — otherwise, for example when you call it like this: $ctrl.changeHook({name1: $ctrl.name, value1: $ctrl.value}) callback will be called with name as undefined and value as undefined. name1 and value1 values will be lost, cause they do not match parameter names. That’s how I think about it — if name is not specified in object, Angular looks for name variable on $scope (which still exists even in component world), but it is not defined so name is undefined.

I used to use the same names in HTML, where callback is provided: change-hook="$ctrl.updateModel(name, value)" and in component’s controller: $ctrl.updateModel = function (name, value){} . But, in retrospect, I don’t think this is a good idea-I’ll show you why in next paragraph.

Other ways to inject value to expression binding and how conflicts are resolved

We can just hard-code a value, which is useful when function takes more than one argument, and component does not inject it:

//HTML
<some-component
change-hook="$ctrl.lowerCaseAndUpdateCode('languageCode', value)"
>
</some-component>
//CALLBACK'S CALL INSIDE COMPONENT
$ctrl.changeHook({value: $ctrl.value});
//PARENT CODE
$ctrl.lowerCaseAndUpdateCode= function (name, value) {
$ctrl.codes[name] = _toLowerCase(value);
}

But, if we have this situation:

//ALMOST THE SAME CODE, BUT SEE BOLD CODE
//HTML
<some-component
change-hook="$ctrl.lowerCaseAndUpdateCode('languageCode', value)"
>
</some-component>
//CALLBACK'S CALL INSIDE COMPONENT
$ctrl.changeHook({name: $ctrl.name, value: $ctrl.value});
//PARENT CODE
$ctrl.lowerCaseAndUpdateCode = function (name, value) {
$ctrl.codes[name] = _toLowerCase(value);
}

For me, it could create some confusion — component injects name, but there is no name in expression provided in HTML. On the other hand, first parameter of callback declaration has name as its first argument. 
What will happen?
If you do not forget that object should be provided to callback’s call inside component, and object keys should be the same as parameter names in HTML where callback is provided, you now what will happen — name from $ctrl.changeHook({name: $ctrl.name, value: $ctrl.value}); is ignored and languageCode is provided as value for first argument of lowerCaseAndUpdateCode function. That’s why, from a perspective of time, different names should be provided for HTML expression binding and for callback declaration in parent code:

//HTML
<some-component
change-hook="$ctrl.lowerCaseAndUpdateCode('languageCode', value)"
>
</some-component>
//CALLBACK'S CALL INSIDE COMPONENT
$ctrl.changeHook({name: $ctrl.name, value: $ctrl.value});
//PARENT CODE
$ctrl.lowerCaseAndUpdateCode = function (parameterName, parameterValue) {
$ctrl.codes[parameterName] = _toLowerCase(parameterValue);
}

It’s more readable, at least for me.

Evaluated DOM attribute (“@”) bindings

First problem: rely on attribute binding for for boolean-like values

How it works? It takes what is written written and makes a string:

var someComponent = { 
controller: ComponentController,
templateUrl: 'some-component.html',
bindings: {
required: '@',
},
};
angular.module("app").directive("someComponent", someComponent);
<some-component 
required="true"
></some component

So, when you use it like this:

<some-component 
required="false"
></some component

And inside component’s template (some-component.html) you have:

<span 
ng-if=”$ctrl.required”
>
<span aria-hidden="true"></span>
<span class="sr-only">Field is required</span>
</span>

You will be really surprised, and not in positive way.
What the heck happened?
As I wrote, it took attribute value:

// false is attribute value - "false" as string, not boolean!
<some-component
required="false"
></some component

And make a string, so required value is string, not boolean. So in code ng-if="$ctrl.required” it casts to boolean true. You can check it now, without any effort — just open Developer Console in your browser, type !!(“false”) and you will get true as value of this expression. Nothing wrong in this behavior, any JS string with length greater than zero casts to true — I’m pretty sure that it happens for every language with dynamic types. But personally I like the semantic of required="true", its readability and I don’t want to lose it.

Solution: create boolean value based on attribute binding value

The way to solve this problem is to transform value from string to boolean and assign result to new value:

function ComponentController ($window) { 
var $ctrl = this;

$ctrl.$onInit = function () {
$ctrl.isRequired = $ctrl.required === 'true';
}
}

And in view:

<span 
ng-if=”$ctrl.isRequired”
>
<span aria-hidden="true"></span>
<span class="sr-only">Field is required</span>
</span>

Now, until true is provided, $ctrl.isRequired is always set to false. Is $onInit good for this transformation? Yes, but as I mentioned earlier it can’t deal with changes after component is initialized or with situation when binding is loaded asynchronously, so it’s better to use $onChanges:

$ctrl.$onChanges = function (changes) { 
if (changes.required) {
$ctrl.isRequired = $ctrl.required === true;
}
}

Now, component is ready for parent’s data reload or for future value changes. But how to provide value when it’s changed?

Second problem: dynamic values for evaluate DOM attribute binding

First attempt may look like this:

<some-component 
required="$ctrl.isRequired"
></some component

At first glance, it should work: every change should be grabbed in $onChanges hook. But it won’t — because attribute binding will take attribute as it is, and real value of required binding is… $ctrl.isRequired. What we can do about it? We can evaluate it:

<some-component 
required="{{$ctrl.isRequired}}"
></some component

And it may evaluated as boolean true or false, null or undefined —we are ready for this situation, cause internal component’s variable is set to true only when true literal is value of attribute.

Important warning about attribute binding

Cause it’s take attribute as it is, string 'required' will evaluate to "'required'", and ="required" !== "'required'". So you have to be really careful to not provide attributes with quotes!

Conclusion

Intentionally I omitted two-way binding (=), as it should be avoided if possible. I hope this post will help somebody to avoid wasting time, especially at the beginning of work with Angular JS component input bindings.