-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix($compile): don't run unnecessary update to one-way bindings #14580
Conversation
@@ -3756,6 +3760,7 @@ describe('$compile', function() { | |||
module('my'); | |||
inject(function($compile, $rootScope) { | |||
element = $compile('<c1 prop1="val"></c1>')($rootScope); | |||
$rootScope.$apply(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have no digest before changing the value of val
then effectively the system thinks that val
has not changed, so $onChanges
is not called.
676106f
to
83dc068
Compare
}; | ||
this.$onChanges = function(changes) { | ||
if (changes.input) { | ||
log.push(changes.input); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are pushing to log
, but never checking what was actually pushed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
One testcase I believe should work, but doesn't, is when the parent value changes after initializing the bindings but before the first call to the one-way binding watcher callback. it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() {
var log = [];
var component;
angular.module('owComponentTest', [])
.component('owComponent', {
bindings: { input: '<' },
controller: function() {
component = this;
this.input = 'constructor';
this.$onInit = function() {
this.input = '$onInit';
};
this.$onChanges = function(changes) {
if (changes.input) {
log.push(changes.input);
}
};
}
});
module('owComponentTest');
inject(function() {
$rootScope.name = 'outer1';
compile('<ow-component input="name"></ow-component>');
expect(component.input).toEqual('$onInit');
$rootScope.$apply('name = "outer2"');
expect($rootScope.name).toEqual('outer2');
expect(component.input).toEqual('outer2');
});
});
it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() {
var log = [];
var component;
angular.module('owComponentTest', [])
.directive('changeInput', function() {
return function(scope, elem, attrs) {
scope.name = 'outer2';
};
})
.component('owComponent', {
bindings: { input: '<' },
controller: function() {
component = this;
this.input = 'constructor';
this.$onInit = function() {
this.input = '$onInit';
};
this.$onChanges = function(changes) {
if (changes.input) {
log.push(changes.input);
}
};
}
});
module('owComponentTest');
inject(function() {
$rootScope.name = 'outer1';
compile('<ow-component input="name" change-input></ow-component>');
expect(component.input).toEqual('$onInit');
$rootScope.$digest();
expect($rootScope.name).toEqual('outer2');
expect(component.input).toEqual('outer2');
});
}); |
BTW, in order to support these usecases (and fix the original bug), we could change the code from: destination[scopeName] = parentGet(scope);
...
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
if (oldValue === newValue) return;
... to: var initialValue = destination[scopeName] = parentGet(scope);
...
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
if (oldValue === newValue) {
if (oldValue === initialValue) return;
oldValue = initialValue;
}
... |
60909c8
to
15de372
Compare
'constructor', | ||
['$onChanges', jasmine.objectContaining({ currentValue: 'outer' })], | ||
'$onInit' | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this test pass even without the fix ? (I haven't tested it, but I think so.)
I think it's a good idea to add a $rootScope.$digest()
and another round of expectation to make sure that nothing is overwritten when the watch callback is executed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. It did fail before the fix. But then I might have changed it again since then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It totally fails as it is without the fix. But I will add the extra digest and check
A couple of minor test-related comments. |
The watch handler was triggering on its first invocation, even though its change had already been dealt with when setting up the binding. Closes angular#14546
15de372
to
927cdbb
Compare
Hey, this just broke a whole bunch of our unit tests which I was hoping to get some clarification on. So right now, we have something like this: export someComponent = {
...
controller: class SomeController {
constructor(...) {
this.state = { ... }
}
set config(config: Config) {
if (!this.state) return
...
this.state.config = config
}
}
}
angular.component('someComponent', someComponent) The reason for the We can't move the constructor code since we rely on injected dependencies to do some setup. Perhaps there's a better pattern for what we want to do but not sure how our use case can be accomplished. Do you have any ideas on what to do here? |
TBH, I don't get what the problem is. Could you share some more code and/or tests. |
Just to give you some context, setters got called twice on startup (once before the component controller instantiates and once after). We relied on the behavior that setters got called after instantiation since dependencies are available at that point. Since this change returns early for matching values, our setter doesn't execute. |
OK I see the angular way of doing this is to hook into |
Glad you got it working with |
@gkalpak I think what @khoomeister is saying is that in 1.5.5, the order of calls was setter-constructor-setter. So on the first fire In 1.5.6, that 2nd call to the setter never happens, so the setter is never fully executed. This is a breaking change for anyone that relies on something being defined on |
@bcherny, it will be easier to investigate with an actual example, but this is expected behavior as far as I understand. The fact that it worked differently in 1.5.5 (i.e. overwriting the inner value even though the outer vaue hadn't changed) was a bug. This commit is restoring the expacted/correct behavior and thus is not considered a breaking change (although it would break a usecase that relied on the buggy behavior). The proper way to ensure something is run after the bindings have been initialized is using an appropriate hook (e.g. |
@gkalpak Demo here - http://codepen.io/anon/pen/EyVyzB |
@bcherny, fwiw it is expected behavior. The actual "problem" with your usecase is not how one-way binding watches are triggered, but the fact that we are pre-assigning the binding values on the controller instance before actually instantiating it. Although this is supposed to be a feature, this has also created its fair share of headaches as well, so it is scheduled for deprecation/removal in future versions. (We might even provide a way to opt-out in 1.5.x as well.) |
A backwards compatible way to make it work (at least in my case) would be to get rid of the 1st binding fire (caused by pre-assigning) rather than the 2nd binding fire -- is that what you mean? So in 1.5.5 it was:
In 1.5.6 it's:
And what I'm proposing is:
|
|
Good to hear - at least for me, that would be an improvement! |
Note that this is the behavior you might get today if your controller is a class (a real class). $compileProvider.enableBindingsPreAssignment(false); The feature would be enabled by default on 1.5.x (so backwards compatible), but can be turned off. |
@gkalpak I agree. But I think it should be simply:
|
@gkalpak @petebacondarwin Is there any progress on this? We have a component that requires a boolean value but it is always set to the default value.. not the one we use in the binding.. Some code: Se we pass true as a binding and if we don't have the binding the default value should be false. But now it is always set to false.. in our test:
in our component
|
A new flag to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580
A new flag to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See #14580 Closes #15095
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See #14580 Closes #15095
@RobbertWolfs, in case you haven't noticed, the related PR has been merged and the |
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580 Closes angular#15095
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580 Closes angular#15095
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580 Closes angular#15095
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580 Closes angular#15095
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See angular#14580 Closes angular#15095
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See #14580 Closes #15095
A new option to enable/disable whether directive controllers are assigned bindings before calling the controller's constructor. If enabled (true), the compiler assigns the value of each of the bindings to the properties of the controller object before the constructor of this object is called. If disabled (false), the compiler calls the constructor first before assigning bindings. The default value is enabled (true) in Angular 1.5.x but will switch to false in Angular 1.6.x. See #14580 Closes #15095
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Bug Fix
What is the current behavior? (You can also link to an open issue here)
The watch handler was triggering on its first invocation, even though
its change had already been dealt with when setting up the binding.
What is the new behavior (if this is a feature change)?
The handler does not trigger an update on its first invocation
Does this PR introduce a breaking change?
Nope - I don't think so. Although I had to modify a bunch of tests to ensure that a digest occurred between compilation and the first checks.
Please check if the PR fulfills these requirements
- [ ] Docs have been added / updated (for bug fixes / features)Other information:
Closes #14546