I'm submitting a ...
Current behavior:
When I create a filter expression on an html attribute, an attribute that is later bounded incoming in a component and used inside an ng-repeat
, it triggers an infinite digest cycle/loop. The output comes up fine on the screen, but there are errors in the console suggesting that it triggers and infinite digest cycle. Code to reproduce is on a plnkr: http://plnkr.co/edit/JdiLEIyji2pHd3eeNMUL?p=preview.
This also happens when the array I'm passing is an array of literals such as number or string (http://plnkr.co/edit/mfjPgZjBro39Nb7WYpjU?p=preview).
Expected / new behavior:
Minimal reproduction of the problem with instructions:
Here's a plnkr with the code in action http://plnkr.co/edit/JdiLEIyji2pHd3eeNMUL?p=preview - open the devtools in your browser to see the errors appear.
Angular version: 1.5.* and 1.6.1/2/3
Browser: [all]
Anything else:
Error message
Error: [$rootScope:infdig] http://errors.angularjs.org/1.6.3/$rootScope/infdig?p0=10&p1=%5B%5B%7B%22ms鈥%2C%22type%22%3A1%2C%22%24%24hashKey%22%3A%22object%3A5%22%7D%5D%7D%5D%5D
at eval (angular.js:38)
at m.$digest (angular.js:18048)
at m.$apply (angular.js:18280)
at eval (angular.js:1912)
at Object.invoke (angular.js:5003)
at c (angular.js:1910)
at Object.Pc [as bootstrap] (angular.js:1930)
at execute (VM877 main.ts!transpiled:21)
at f (system.js:5)
at Object.execute (system.js:5)
Possible solutions
<todo-list todo-items="$ctrl.todoItems" filter-by="{completed:true}"></todo-list>
. For full source see here: https://github.com/aredfox/todo-angularjs-typescript/commit/e71900b96173b63ebcebb8e6c1fba00fe3997971. But I feel it's working around the problem, plus I don't understand why this triggers a $digest() cycle and why it shouldn't just work.Related issue
FWIW: I've edited your plunkr to reproduce the issue with an easier filter syntax. I guess it doesn't matter, but I wanted to drop it here just to be sure:
https://plnkr.co/edit/mnamqTtsogXZj8inB3L5?p=preview
plus I don't understand why this triggers a $digest() cycle and why it shouldn't just work.
A $digest
cycle get's triggered whenever something changes (and angularjs is aware of these changes), filtering your array results in a new array hence a changed input binding and a need for the digest cycle to trigger in order for angular to know what has to be updated and what does not.
The issue with filtered arrays in component bindings is discussed here https://github.com/angular/angular.js/issues/14039
Implementing $doCheck
is recommended, but I don't actually know how you would do that. :o
I'm interested to see how to implement a workaround using $doCheck
, as mentioned in the issue you linked!
One quick workaround is to use =*
binding rather than <
since the former will not continue to trigger digests. See https://plnkr.co/edit/fk9SXfRQa3cE8KGfGWqt?p=preview
Yep, that did solve the plunkr I added: https://plnkr.co/edit/4czmVoTGz1YvM63IrPqs?p=preview
Out of curiosity: Anything available regarding the $doCheck
you mentioned @petebacondarwin ?
Thinking about it now :-)
I think this is actually a bug - we are getting stuck in a state where it is testing ['a'] === ['a']
which is continually false.
Isn't this intended behavior?
If this is intended, I think it's odd to see the UI work but the console error out.
This is also expected behavior when there is an infinite digest error. We exit the digest loop to avoid freesing the browser. The error doesn't mean that there is something wrong with your app, except that some binding/watcher is constantly changing. In this case, the change doesn't matter, since the two array are identical (even if not pointing to the same reference), so it is expected that your app continues to work as expected.
But I don't think we should hit an infinite loop here. Why is the array identity changing?
Because of the filter, no?
OK, so the filter always returns a new object and the one-way binding only tests for object identity equivalence. So it is expected behaviour.
Here is a reasonable workaround (IMO), which could be tailored to the specific needs of the situation for best performance: https://plnkr.co/edit/g0AzLUAbwfiiIl8PNgcE?p=preview
<comp items="$ctrl.items | filter: $ctrl.filterBy | stable"></comp>
.filter('stable', function() {
var oldValue = NaN;
return function(value) {
oldValue = angular.equals(value, oldValue) ? oldValue : value;
return oldValue;
};
We should document this.
I am pretty sure this will break if you have more than one | stable
in your template.
Thanks, @petebacondarwin, would using =*
not be a viable option too? Or are there huge perf issues involved? Because this setup, it works of course, does make it a rather big workaround imho. Is there a possibility of ever having a <*
binding?
@gkalpak - for sure, it would need tweaking for production :-)
@aredfox - There is nothing wrong with using =*
as long as you don't want to ensure that changes inside the component do not appear outside the component.
I guess we are getting close to enough use cases to implement <*
@jbedard previously wondered if this could be handled in $parse directly: https://github.com/angular/angular.js/issues/14039#issuecomment-184002618
This should also be fixed by #16553 which adds one-way collection watching (<*
) similar to the bidirectional version (=*
).
Would be nice if we have one-time bindings in component and directive definition too, like "::<" or "<::" like we can use inside templates {{::$ctrl.variable}}
That would be cool indeed :smiley:
Right now, only the user of the component/directive has control over whether the binding is normal or one-time (which makes sense since they know whether the value can change). But it would be cool if the component/directive author could also have control, because oftentimes components/directives do to react to binding value changes, so there is no point in keeping the watcher active (but the user doesn't have to know about this).
@wilker7ribeiro, could you create a separate issue about this.
(We are aiming to have a feature freeze before the end of the month (as AngularJS is entering its LTS period), so we can't promise anything :wink:)
Two ways you could do this today:
link: function(scope, element, attr) {
var oneTimeValue = $parse(attr.oneTimeAttr)(scope.$parent);
});
link: function(scope, element, attr) {
var oneTimeValue = undefined;
var watchRemover = scope.$parent.$watch(attr.oneTimeAttr, function(newValue, oldValue) {
if (newValue !== undefined) {
oneTimeValue = newValue;
watchRemover();
});
});
Duplicate of https://github.com/angular/angular.js/issues/16173, right?
Indeed. And I'm afraid I'll have to agree with myself :grin:
Most helpful comment
@gkalpak - for sure, it would need tweaking for production :-)
@aredfox - There is nothing wrong with using
=*
as long as you don't want to ensure that changes inside the component do not appear outside the component.I guess we are getting close to enough use cases to implement
<*