Yaniv
Yaniv

Reputation: 1908

race condition with angularjs directive

I have a directive that has 2 attributes:
1. a title object passed from the controller.
2. a onchange function passed from the controller.

scope: {
  title: '=',
  change: '&'
}


//on the link function I update the object, and call the function.

link: function(scope) {
    scope.setValue = function() {
      scope.title = "Yaniv"
      scope.change();
    };
},

somehow, eventhough I wrote it the opposite way, it seems that first it calls the function, and only then it updates the title object. What is the best way to overcome this? I already thought about using setTimeout, and it actually worked this thing around. but, I wonder why this issue happened, and whether there is a cleaner solution here. attached JS fiddle: http://jsfiddle.net/sz82r7pg/

Upvotes: 0

Views: 1219

Answers (2)

Tamas Hegedus
Tamas Hegedus

Reputation: 29926

The first thing to note is that the directive has its own isolate scope object, of which the title attribute is bound to the title of the controller scope. This databinding is basically done by watches, and you might know that change detection using watches is not instantaneous, happens inside a digest loop, which always happens after the change was made, and the call stack is emtpied (in the next micro/macro task. I don't know exactly which, but doesn't matter here). Calling a function is however instantaneous. So what happens is the following:

  • Set title of directive scope
  • Call change, which calls foo, the controller scope title is the old one
  • After the call angular runs a digest loop, notices the change in the directive scope, and propagates it to the controller scope

This is totally expected behaviour. The easiest way in your case is not to use your own change detection, but use the one built into angular:

angular.module('zippyModule', [])
.controller("Ctrl3", function ($scope) {
  $scope.title = 'Ori';
  $scope.$watch("title", function(newValue, oldValue) {
    if (newValue !== oldValue) {
      alert(newValue);
    }
  });
})
.directive('zippy', function() {
  return {
    restrict: 'AE',
    scope: {
      title:'=zTitle'
    },
    template: '<button ng-click="setValue()">What would be the value of title??</button>',
    link: function(scope) {
      scope.setValue = function () {
        scope.title = "Yaniv";
      };
    }
  }
});
button {
  font-size:24px;
  margin:40px;
  padding: 10px 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.js"></script>

<div ng-app="zippyModule">
  <div ng-controller="Ctrl3">
    <div zippy z-title="title"></div>
  </div>
</div>

While writing this snippet I realized you are using an ancient version of angularjs in the fiddle (1.0.2). I had to make a little modification to work with 1.5.x.

If you dont want to use watches, then the best you can do is to justt pass the new title to the change function.

angular.module('zippyModule', [])
.controller("Ctrl3", function ($scope) {
  $scope.title = 'Ori';
  $scope.foo = function(newTitle) {
    alert(newTitle);
  };
})
.directive('zippy', function() {
  return {
    restrict: 'AE',
    scope: {
      change: '&'
    },
    template: '<button ng-click="setValue()">What would be the value of title??</button>',
    link: function(scope) {
      scope.setValue = function () {
        scope.change({title:"Yaniv"});
      };
    }
  }
});
button {
  font-size:24px;
  margin:40px;
  padding: 10px 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.js"></script>

<div ng-app="zippyModule">
  <div ng-controller="Ctrl3">
    <div zippy change="foo(title)"></div>
  </div>
</div>

UPDATE

It turned out in the comments you wish to make a somewhat tighter coupling between the controller and the directive. Such a thing is usually considered a bad design, but there are times when this is the only right solution (most of the time when non-angular components are involved). So the idea is to create an API object, which can directly be manipulated, and pass it to the directive.

angular.module('zippyModule', [])
.controller("Ctrl3", function ($scope) {
  $scope.title = 'Ori';
  $scope.fooApiImpl = {
    title: "Ori",
    change: function() {
      alert($scope.fooApiImpl.title);
    }
  };
})
.directive('zippy', function() {
  return {
    restrict: 'AE',
    scope: {
      fooApi: '='
    },
    template: '<button ng-click="setValue()">What would be the value of title??</button>',
    link: function(scope) {
      scope.setValue = function () {
        scope.fooApi.title = "Yaniv";
        scope.fooApi.change();
      };
    }
  }
});
button {
  font-size:24px;
  margin:40px;
  padding: 10px 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.js"></script>

<div ng-app="zippyModule">
  <div ng-controller="Ctrl3">
    <div zippy foo-api="fooApiImpl"></div>
  </div>
</div>

Upvotes: 1

Estus Flask
Estus Flask

Reputation: 222513

title two-way binding is updated in parent scope at the end of the digest. Until then title value change is not propagated from directive scope to parent scope.

setValue is called by ng-click and runs during the digest. This means that change should be called after the current digest, with setTimeout or $imeout:

   scope.setValue = function () {
                scope.title = "Yaniv"
                $imeout(() => scope.change());
   };

Which indicates bad smell and XY problem, because title update is already tied to digest, and change is redundant. It's the job for the framework. title changes should be tracked with

$scope.$watch('title', (newValue, oldValue) => {
  if (newValue === oldValue) return;
  ...
});

in parent scope.

Upvotes: 1

Related Questions