Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delay in Two-way binding in a nested directive with isolate scope

I have a controller containing a directive with an isolated scope. Whenever a user changes the value in the directive's dropdown, I need to notify the controller about the change. I do this by providing the directive with a callback function, and calling this function on ng-change event in the directive's dropdown. I can't (and don't want to) use watches in the parent controller, as I'm only interested in user-generated changes here.

My problem is that the values on the controller's scope has not yet been updated to those from the directive through the 2-way binding at the time the callback has been called (I guess this is because the callback is called in the same digest-cycle where the changes on the dropdown's model has been detected, but the models from the parent controller will only be updated in the subsequent cycles).

Here's a plunker example illustrating the issue (notice the console output):

http://plnkr.co/edit/igW4WiV2niFyrMX2zrjM?p=preview

Excerpt of the relevant parts from the plunker:

Controller:

  ...
  vm.countryChanged = function() {
    console.log('*** countryChanged callback ***');
    console.log('country:', vm.country); // old value here

    $timeout(function() {
      console.log('country (inside timeout):', vm.country); // new value here
    });
  };
  ...

Directive:

    ...
    template: '<select ng-model="vm.country" ng-change="vm.onChange()"  ' +
              '        ng-options="country for country in vm.countries">' +
              '</select>',
    scope: {
      country: '=',
      onChange: '&'
    },
    ...

What would be the most correct way to get the updated data inside the parent's controller callback? I can see that $timeout helps, but it feels like a code-smell, and there probably should be a way to force the models to update on the parent controller.

like image 464
longedok Avatar asked Apr 10 '17 13:04

longedok


2 Answers

I've checked and fixed your plnkr, http://plnkr.co/edit/8XoNEq12VmXKkyBmn9Gd?p=preview

it's not working because you're passing a primitive value to the directive, a string, two way binding works only with references.

Changing the country value to an object fixed the problem

vm.country = 'Belarus';

to

vm.country = {name: 'Belarus'};

then:

template: '<select ng-model="vm.country.name" ng-change="vm.onChange()"  ' +
  ' ng-options="country for country in vm.countries">' +
'</select>'
like image 114
Karim Avatar answered Nov 10 '22 11:11

Karim


I guess this is because the callback is called in the same digest-cycle where the changes on the dropdown's model has been detected, but the models from the parent controller will only be updated in the subsequent cycles.

You are a correct. The watcher that updates the value from the directive scope to the parent controller fires on the digest cycle after the ng-change directive evaluates its Angular expression.

Sharing a common object reference between directive and parent controller is one way to make it work. Since the controller and the directive share the same reference, any changes are seen immediately by both the directive and the parent controller.

Another way is to expose the new value as a local argument of the on-change function:

<country-chooser 
    country="vm.country" 
    country-change="vm.countryChange($event)">
</country-chooser>

OR

<country-chooser 
    country="vm.country" 
    country-change="vm.country=$event">
</country-chooser>

In the directive:

app.directive('countryChooser', function() {
  return {
    template: `<select ng-model="$ctrl.country"
                       ng-change="$ctrl.countryChange({'$event': vm.country})"
                       ng-options="country for country in $ctrl.countries">
              </select>
              `,
    scope: {
      country: '<',
      countryChange: '&'
    },
    bindToController: true,
    controllerAs: '$ctrl',

In the parent controller:

  vm.countryChange = function(country) {
      console.log('*** countryChange callback ***');
      console.log('country:', country);
  };

This way the function inside the parent controller does not need to wait a digest cycle for the watcher on the two-way binding to update the value from the directive scope to the parent controller scope.

The DEMO on PLNKR.


AngularJS v1.5 Components and Making Them Angular2+ Ready

app.component('countryChooser', {
    template: `<select ng-model="$ctrl.country"
                       ng-change="$ctrl.countryChange({'$event': vm.country})"
                       ng-options="country for country in $ctrl.countries">
              </select>
              `,
    bindings: {
      countries: '<'
      country: '<',
      countryChange: '&'
    }
});

AngularJS v1.5 introduces components to make the transition to Angular2+ easier.

To make the transition to Angular2+ easier, avoid two-way (=) bindings. Instead use one-way < binding and expression & binding.

In Angular2+, two-way binding syntax is really just syntactic sugar for a property binding and an event binding.

<country-chooser [(country)]="vm.country">
</country-chooser>

Angular2+ de-sugars two-way binding into this:

<country-chooser [country]="vm.country" (countryChange)="vm.country=$event">
</country-chooser>

For more information, see @Angular Developer Guide - Template Syntax (two-way)

like image 35
georgeawg Avatar answered Nov 10 '22 10:11

georgeawg