tcmoore tcmoore - 6 months ago 282
AngularJS Question

How to wait for binding in Angular 1.5 component (without $scope.$watch)

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... sot hat 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?

Answer

Update:

The $onChanges() lifecycle hook has been backported from 2 as of Angular 1.5.3. $onChanges() will fire on any change to one-way ('<') or interpolation ('@') bindings, which essentially takes care of exactly this case.

Check it out: http://blog.thoughtram.io/angularjs/2016/03/29/exploring-angular-1.5-lifecycle-hooks.html

Past Update:

I was wrong. $onInit doesn't wait for the bindings to get stabilized.

I find this to be one of the most accurate $onInit definition:

$onInit lifecycle hook: Introducing a new lifecycle hook for directive controllers, called after all required controllers have been constructed. This enables access to required controllers from a directive's controller, without having to rely on the linking function.

In the documentation of $compile there are some instances where the words "bindings" and "initialized" are used together. In this context, it simply means that all of the directive's bindings have had their watchers set up.

To answer this question, as Angular 1.5.0, there isn't any way to wait for the directive's bindings to get stabilized without using a watcher. This isn't a bad thing as you'll have greater control while defining your watchers. The following example showcases how a directive may wait for its bindings to get stabilized:

angular
  .module('app', [])
  .controller('MainCtrl', ['$timeout',
    function($timeout) {
      var vm = this;

      $timeout(function() {
        vm.stringBinding = 'stringy';

        vm.numberBinding = 'this will be ignored';
      }, 1000);

      $timeout(function() {
        vm.numberBinding = 123;
        vm.arrayBinding = [1, 2, 3];
      }, 1500);

      $timeout(function() {
        vm.localBinding = 'something';
      }, 2000);
    }
  ])
  .component('myComponent', {
    bindings: {
      stringBinding: '=',
      numberBinding: '=',
      arrayBinding: '=',
      localBinding: '@'
    },
    controller: ['$scope',
      function($scope) {
        var $ctrl = this
        var neededBindings = 4;

        $ctrl.bindingsAreStabilized = false;

        var stringBindingDeReg = $scope.$watch('$ctrl.stringBinding',
          function(newValue) {
            if (angular.isString(newValue)) {
              stringBindingDeReg();
              neededBindings -= 1;

              onBindingsStabilize();
            }
          });

        var numberBindingDeReg = $scope.$watch('$ctrl.numberBinding', function(newValue) {
          if (angular.isNumber(newValue)) {
            numberBindingDeReg();
            neededBindings -= 1;

            onBindingsStabilize();
          }
        });

        var arrayBindingDeReg = $scope.$watch('$ctrl.arrayBinding', function(newValue) {
          if (angular.isArray(newValue)) {
            arrayBindingDeReg();
            neededBindings -= 1;

            onBindingsStabilize();
          }
        });

        var localBindingDeReg = $scope.$watch('$ctrl.localBinding', function(newValue) {
          // @ bindings are always a string
          if (angular.isString(newValue) && newValue.length > 0) {
            localBindingDeReg();
            neededBindings -= 1;

            onBindingsStabilize();
          }
        });

        function onBindingsStabilize() {
          if (neededBindings === 0) {
            console.log('everything is ready!');

            $ctrl.bindingsAreStabilized = true;
          } else {
            console.log(neededBindings + ' more bindings need to stabilize until onBindingsStabilize gets called');
          }
        }
      }
    ],
    template: '{{ $ctrl.bindingsAreStabilized ? "all bindings have been stabilized" : "waiting for required bindings to get stabilized"}}'
  });
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>

<div ng-app="app">
  <div ng-controller="MainCtrl as vm">
    <my-component string-binding="vm.stringBinding" number-binding="vm.numberBinding" array-binding="vm.arrayBinding" local-binding="{{ vm.localBinding }}"></my-component>
  </div>
</div>

I've deleted my old answer since it was incorrect.