Wait for the Bindings of a Directive in Angular

If you’ve worked with angular for a while, you’ve probably found yourself having a directive like

<my-directive my-prop="myProp"></my-directive>

where not having myProp ready yet (e.g. it’s retrieved from the server asynchronously) lead to problems. For example if the first thing you do in your directive is to call myProp.sayHello(), your app will crash saying cannot read property sayHello of undefined. Or maybe the displayed directive just will look wrong without myProp.

So you recalled yourself “ah well, ng-if prevents things from rendering, so I’ll just put an ng-if there”. So your code looked like that

<my-directive my-prop="myProp" ng-if="myProp"></my-directive>

Every once in a while myProp is not an object, but a number or a boolean instead, which breaks the desired behaviour so you’ll have to fix the bug by something like

<my-directive my-prop="myProp" ng-if="myProp !== undefined"></my-directive>

Phew, problem dodged. But then the next time you use your directive in another place, you’ll forget to put the ng-if there again! You’ll have to remember the ng-if each time you use your directive. How do you communicate to another user (e.g. team member) of your directive, that the ng-if is required? Do you write this

{
  myProp: "=" //this is very important property, so put ng-if=myProp on the directive when you use it
}

in the source of the directive? As you can see, this is not ideal.

So what can you do to alleviate the pain? I think it would be best if there were a flag by angular, something like

{
 myProp: "=r"
}

(where r stands for required). Of course it’s a bit confusing since there is also the flag "=?" which means that you can actually leave away the binding (i.e. <my-directive><my-directive> is allowed). I guess what angular intended to do is saying “you specified "=" and not "=?" so anyone using the directive will know it’s a required property. So if anyone passes in undefined because a property is received asynchronously it’s their fault.”. The problem with this line of thinking is that it can cause a lot of bugs. Let’s say several directives make use of myProp which is loaded synchronously in the view-controller. But then for one reason or another you’ll make myProp asynchronous. First of all you probably won’t remember that you need to put ng-if’s everywhere. Second, you probably don’t know what’s all depending on myProp. And third and worst, your app might run fine locally because the local server responds fast enough, but then crash in production!

So since Angular doesn’t provide you with a flag for actually waiting for bindings (until they’re not undefined anymore), is there another way than the ng-if? The only thing I could come up with so far is wrapping your directive in another directive to abstract away the necessity of the ng-if for users of the directive. This could go like so:

<my-directive-implementation ng-if="myProp"
                             my-prop="myProp">
</my-directive-implementation>

where the controller has to bind the property

// controller of my-directive
...
{
  myProp: "="
}

Then, in my-directive-implementation, you can put the code you originally intended for my-directive. Another possibility would be to just put the ng-if on the top level node of your directive

<div ng-if="myProp">
  ...directive stuff
</div>

and in the the directive-controller watch the property and only do stuff once the property is not undefined anymore:

scope.$watch('myProp', function(newValue, oldValue){
   ...initialize the controller
}

Of course this is quite a lot of noise for something that could have just been achieved with a flag provided by angular. I mean, as mentioned it’s reasonable to let the invoker make sure that a property is non-null, but since it’s quite a common use-case to retrieve myProp asynchronously, I think it would have been nice if they provided a way to only render directives once props are not undefined anymore. If you have other solutions or inputs for this, please let me know in the comments!

Leave a Reply

Your email address will not be published.