36

I'm writing an Angular 1.5 directive and I'm running into an obnoxious issue with trying to manipulate bound data before it exists.

Here's my code:

app.component('formSelector', {
  bindings: {
    forms: '='
  },
  controller: function(FormSvc) {

    var ctrl = this
    this.favorites = []

    FormSvc.GetFavorites()
    .then(function(results) {
    ctrl.favorites = results
    for (var i = 0; i < ctrl.favorites.length; i++) {
      for (var j = 0; j < ctrl.forms.length; j++) {
          if (ctrl.favorites[i].id == ctrl.newForms[j].id) ctrl.forms[j].favorite = true
      }
     }
    })
}
...

As you can see, I'm making an AJAX call to get favorites and then checking it against my bound list of forms.

The problem is, the promise is being fulfilled even before the binding is populated... so that by the time I run the loop, ctrl.forms is still undefined!

Without using a $scope.$watch (which is part of the appeal of 1.5 components) how do I wait for the binding to be completed?

gitsitgo
  • 6,589
  • 3
  • 33
  • 45
tcmoore
  • 1,129
  • 1
  • 12
  • 29

4 Answers4

34

I had a similar issue, I did this to avoid calling the component until the value I am going to send is ready:

<form-selector ng-if="asyncValue" forms="asyncValue" ></form-selector>
Ced
  • 721
  • 1
  • 11
  • 21
28

You could use the new lifecycle hooks, specifically $onChanges, to detect the first change of a binding by calling the isFirstChange method. Read more about this here.

Here's an example:

<div ng-app="app" ng-controller="MyCtrl as $ctrl">
  <my-component binding="$ctrl.binding"></my-component>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.4/angular.js"></script>
<script>
  angular
    .module('app', [])
    .controller('MyCtrl', function($timeout) {
      $timeout(() => {
        this.binding = 'first value';
      }, 750);

      $timeout(() => {
        this.binding = 'second value';
      }, 1500);
    })
    .component('myComponent', {
      bindings: {
        binding: '<'
      },
      controller: function() {
        // Use es6 destructuring to extract exactly what we need
        this.$onChanges = function({binding}) {
          if (angular.isDefined(binding)) {
            console.log({
              currentValue: binding.currentValue, 
              isFirstChange: binding.isFirstChange()
            });
          }
        }
      }
    });
</script>
Cosmin Ababei
  • 7,003
  • 2
  • 20
  • 34
  • 6
    Awesome! Thank you so much. Angular's in a weird place right now; 1.5 is so cool and a really great option for apps that started in 1.3 or 1.4... but when you search for help on 1.5 components it's hard to sort through all the Angular 2 community chatter! – tcmoore Feb 26 '16 at 00:11
  • @HristoEnev Thanks! Can you tell me exactly what helped you? I'm thinking of refining the answer a bit, but I'm not sure what to emphasize: my custom method or the `$onChanges()` hook. – Cosmin Ababei May 18 '16 at 10:03
  • Your custom method. I have a two way binding and I had no idea why it is undefined on $onInit so I was wondering how to handle it's binding stabilization. This works just fine. – Hristo Enev May 18 '16 at 10:30
  • What happens if we don't know the type of the binding? – Hristo Enev Jun 14 '16 at 09:47
  • And what about if the binding is optional? – Hristo Enev Jun 14 '16 at 10:16
  • 1
    @HristoEnev if you don't know its type, then [angular.isUndefined](https://docs.angularjs.org/api/ng/function/angular.isUndefined) would work just fine. – Cosmin Ababei Jun 16 '16 at 08:32
  • @HristoEnev Regarding your second question, an optional binding should only alter the functionality if it's *defined*. If it's *undefined* it mustn't break the functionality. With this in mind, once all your required bindings get stabilized, check if the optional binding is ready or not, and perform the appropriate action. – Cosmin Ababei Jun 16 '16 at 08:32
  • Thanks! Wish I would have found this sooner. I ran into issues attempting to use a resolve function on the routes. – PPJN Dec 29 '17 at 04:35
7

The original poster said :

the promise is being fulfilled even before the binding is populated... sot hat by the time I run the loop, ctrl.forms is still undefined

Ever since AngularJS 1.5.3, we have lifecycle hooks and to satisfy the OP's question, you just need to move the code that is depending on the bindings being satisfied inside $onInit():

$onInit() - Called on each controller after all the controllers on an element have been constructed and had their bindings initialized (and before the pre & post linking functions for the directives on this element). This is a good place to put initialization code for your controller.

So in the example:

app.component('formSelector', {
  bindings: {
    forms: '='
  },
  controller: function(FormSvc) {
    var ctrl = this;
    this.favorites = [];

    this.$onInit = function() {
      // At this point, bindings have been resolved.
      FormSvc
          .GetFavorites()
          .then(function(results) {
            ctrl.favorites = results;
            for (var i = 0; i < ctrl.favorites.length; i++) {
              for (var j = 0; j < ctrl.forms.length; j++) {
                if (ctrl.favorites[i].id == ctrl.newForms[j].id) {
                  ctrl.forms[j].favorite = true;
                }
              }
            }
          });
    }
}

So yes there is a $onChanges(changesObj), but $onInit() specifically addresses the original question of when can we get a guarantee that bindings have been resolved.

Oli
  • 1,031
  • 8
  • 20
  • I needed to get a
    from the component's template, but within the $onInit() method the form is undefined. If I access it inside a $timeout (100ms), then I can get it properly. I do not like this approach, but accessing the form inside $onInit() is not working for me as it seems not all the bindings are resolved.
    – Francesco Aug 22 '16 at 20:03
  • onInit does not guarantee resolved bindings. See here: http://javascript.qahowto.com/How-to-wait-for-binding-in-Angular-15-component-without-scopewatch-javascript-angularjs-components-21fd08b which looks to be the same question. OnChanges is the way to go, which means if you need your code to wait for valid bindings, then you will need to check that in onChanges. – Adam Tolley Aug 28 '17 at 21:45
  • 1
    I think this answers is true for 1.6 not for 1.5. $onInit does not guarantee that bindings have been resolved in 1.5 whereas 1.6 does. – toxaq Apr 07 '18 at 02:44
1

I had a similar problem and I found this article very helpful. http://blog.thoughtram.io/angularjs/2016/03/29/exploring-angular-1.5-lifecycle-hooks.html

I have an ajax call that hits the server on page load and my component needs the ajax return value to properly load. I implemented it this way:

this.$onChanges = function (newObj) {
      if (newObj.returnValFromAJAX)
        this.returnValFromAJAX = newObj.returnValFromAJAX;
    };

Now my component works perfectly. For reference I am using Angular 1.5.6

tperdue321
  • 11
  • 3