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.
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>'
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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With