AngularJS: Parent scope not updated in directive (with isolated scope) two way binding
Asked Answered
H

2

10

I have the following code, which can also be fiddled on http://jsfiddle.net/garukun/u69PT/.

View:

<div data-ng-app="testApp">
    <div data-ng-controller="testCtrl">
        <strong>{{pkey}}</strong>
        <span data-test-directive data-parent-item="pkey" 
            data-parent-update="update(pkey)"></span>
    </div>
</div>

JS:

var testApp = angular.module('testApp', []);

testApp.directive('testDirective', function ($timeout) {
    return {
        scope: {
            key: '=parentItem',
            parentUpdate: '&'
        },
        replace: true,
        template: '<div><p>{{key}}</p>' +
            '<button data-ng-click="lock()">Lock</button>' +
            '</div>',
        controller: function ($scope, $element, $attrs) {
            $scope.lock = function () {
                $scope.key = 'D+' + $scope.key;
                console.log('DIR :', $scope.key);

                // Expecting $scope.$parent.pkey to have also been
                // updated before invoking the next line.
                $scope.parentUpdate();
                // $timeout($scope.parentUpdate); // would work.
            };
        }
    };
});

testApp.controller('testCtrl', function ($scope) {
    $scope.pkey = 'golden';
    $scope.update = function (k) {
        // Expecting local variable k, or $scope.pkey to have been
        // updated by calls in the directive's scope.
        console.log('CTRL:', $scope.pkey, k);
        $scope.pkey = 'C+' + k;
        console.log('CTRL:', $scope.pkey);
    };
});

Basically, I'm setting up the directive with an isolated scope, in which I'm two-way binding a property (key) from the parent scope (pkey), and also delegating a method (parentUpdate) to be called in the context of the parent scope.

Now, during a ng-click event handler in the directive, I want to invoke the parentUpdate method and do something within. When I'm invoking that method, I'm expecting my parent scope's model to have been updated. But in reality, it is not, and this is what's puzzling me.

It's probably because of some missing $digest cycles in the middle, since wrapping the parentUpdate call with $timeout would work as expected.

Could someone shed some light on what's missing? Or how to properly invoked parentUpdate?

Hedveh answered 15/10, 2013 at 21:51 Comment(1)
OK, so targeting a bit here... you're wondering why $scope.key = 'D+' + $scope.key; from the lock() function doesn't seem to have any effect, right?Quipster
Q
28

OK, I'm going to take a crack at this one... It seems you're changing both the isolated child AND parent variables before a $digest cycle where the bi-direction logic syncs the two. Here's the details:

  1. First your lock() function is executed by clicking on the button. This updates the isolated $scope.key variable. NOTE: This does NOT immediately update the parent $scope.pKey; that would normally happen at the next $digest cycle but does not in this case. Read on...
  2. Within lock() you are calling parentUpdate() which updates the parent's $scope.pKey variable.
  3. THEN the $digest cycle executes. When it loops it's way to the parent scope a change to $scope.pKey is correctly detected.
  4. The change to $scope.pKey triggers a watch() that was created by the bi-directional binding in the isolated scope. These lines are the critical ones..
  5. The watch() created by the isolated scope checks whether it's value for the bi-directional binding is in sync with the parent's value. If it isn't (and it's not in this scenario) the parent's value is copied to the isolated scope even though the isolated scope's value has changed also and in fact was changed first.

Misko's famous post on Angular data-binding describes the benefits of the $digest cycle approach. What you're seeing here is a conscious side-effect of the $digest's approach to change coalesence wherein, as the source code comment says, parent changed and it has precedence... and that means your isolated scope's change loses.

The $timeout() approach you noted above avoids this issue by changing only the isolated scope's value in the first $digest cycle which allows it to be copied to the parent scope successfully and THEN calling parentUpdate()

The $compile documentation says:

Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).

This means, at step #2 you could pass in your value for pkey via an object map like this:

parentUpdate({pkey: 'D+' + $scope.key })

Here's the updated fiddle: http://jsfiddle.net/KbYcr/

Quipster answered 17/10, 2013 at 14:26 Comment(2)
Thanks for the explanation, it's really helpful! While I understand that it's due to the design of the $digest cycles. I'm seeking a way to have the properties of the isolated scope build on top of parent scope's property without having to invoke timeout function. And in this case, since we are already in a digest cycle, we cannot invoke anymore $digest or $apply. Ideally, I would like to invoke evalAsync, but that doesn't seem to work either. If there could be one trick in the angular's sleeves that can bypass this design, it would be awesome.Hedveh
I think there is just such a trick :-) I just updated my answer above with the details and a new jsFiddle with a minor change demonstrating it.Quipster
C
0

Using $scope.$apply() instead of $scope.$digest() works too. This will also trigger the digest on the rootScope.

Cognition answered 4/2, 2015 at 14:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.