3

I have a simple Pipe that filter an array of students. Here's the code (Plnkr)

import {Pipe} from 'angular2/core';

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe {
  temp = [];
  // i = 0;
  transform (value, [queryString]) {
    // console.log(this.i++);
    // console.log(value, queryString);

    // This does not work
    this.temp = value.filter((student)=>(student)=>student.name.includes(queryString)))
    return value.map(function(val){ return val.name.toUpperCase()});

    // This works
    // this.temp.length = 0;
    // this.temp.push(...value.filter((student)=>student.name.includes(queryString)))        
    // return this.temp;
  }
}

As you can see in Plnkr, Angular throws an error using the first method.

EXCEPTION: Expression 'students | sortByName:queryElem.value  in HelloWorld@7:6' has changed after it was checked. Previous value: 'SON,DAVID'. Current value: 'SON,DAVID' in [students | sortByName:queryElem.value  in HelloWorld@7:6]

Why?

Chu Son
  • 1,520
  • 3
  • 12
  • 13
  • Possible duplicate of [Expression \_\_\_ has changed after it was checked](http://stackoverflow.com/questions/34364880/expression-has-changed-after-it-was-checked) – Mark Rajcok Dec 29 '15 at 05:34

1 Answers1

8

Angular cannot make certain optimizations for a stateful pipe, than it can for a stateless (or pure) pipe. For example, if a pipe is stateless, then it follows that the output of the filter only depends on its inputs (left | pipe:args). As long as 'left' or 'args' hasn't changed, then the output will not change. This allows AngularJS to safely skip the execution of the pipe when the inputs haven't changed.

For a stateful pipe, the output of the pipe can change, even for the same inputs.

The error is telling you that the array reference has changed, after it has been checked following the first round of change detection:

... has changed after it was checked. 
Previous value: 'SON,DAVID'. Current value: 'SON,DAVID'...

I've modified your first example to preserve the array reference:

// This now works
var $this = this; // save this
$this.temp.length = 0;
var tmp = value.filter((student)=>student.name.includes(queryString));
tmp.forEach(function (val) {$this.temp.push(val);});
return $this.temp;

[Edit]

As Mark pointed out, the error only occurs during development mode. If you change to production mode the error goes away, and the code works as expected.

[Explanation]

Apparently, in dev mode, angular will check your bindings twice to make sure they're not changing.

https://github.com/angular/angular/issues/6006

https://github.com/angular/angular/issues/6005

The problem is that when a binding changes after the first round of change detection, it will not trigger a new round of change detection. This is undesirable because the binding will not be updated until some future round of change detection. To ensure that this does not happen, Angular checks the bindings twice in development mode, and raises a run-time error when changes are detected.

Updated Plunkr

Michael Kang
  • 52,003
  • 16
  • 103
  • 135
  • 1
    Just FYI, instead of `tmp.forEach( )`, you can use `$this.temp.push(...tmp)`. -- [Default + Rest + Spread](https://babeljs.io/docs/learn-es2015/#default-rest-spread) – Mark Rajcok Dec 27 '15 at 03:45
  • I'm trying to understand how change detection works. You stated "AngularJS must execute the Pipe every digest cycle and at least twice until the output stabilizes." Do you have a reference for that so I can learn more about this? I was reading [Savkin's blog](http://victorsavkin.com/post/110170125256/change-detection-in-angular-2), and in a comment (in a reply to user Foo) he states "In Angular2 we walk the change detection graph only once." I can't seem to make sense of this statement and yours -- they seem to conflict, but maybe I'm missing something. – Mark Rajcok Dec 29 '15 at 03:57
  • 1
    I think I found the answer. In [this answer by @drewmoore](http://stackoverflow.com/a/34364881/215945), he states "In short, when in dev mode, every round of change detection is followed immediately by a second round that verifies no bindings have changed since the end of the first, as this would indicate that changes are being caused by change detection itself." If I run @ChuSon's "this does not work" code in production mode, the error does not appear, and it does work. [plunker](http://plnkr.co/edit/oG6p2oFqwFqYQYsMGdQJ?p=preview) – Mark Rajcok Dec 29 '15 at 04:47
  • I think you are right. I've done some further research into how Angular2 does change detection, and I don't believe my answer is correct (it may actually only apply to angular 1.x). I'll try to revise this answer. – Michael Kang Dec 29 '15 at 08:55
  • The [ApplicationRef.tick() docs](https://angular.io/docs/ts/latest/api/core/ApplicationRef-class.html) has some discussion about the second changed detection cycle in dev mode. In the scenario here (stateful pipe), I think the error is bogus/misleading. We have a stateful pipe, and the output can change/have side-effects each time it is called – it's stateful, and that's okay. NgFor is evaluated after the pipe, so it should work fine. But since we can't develop with this error being thrown, preserving the array reference is one workaround. – Mark Rajcok Dec 30 '15 at 03:16
  • Another workaround is to use `onPush`, as I describe [here](http://stackoverflow.com/a/34497504/215945) – Mark Rajcok Dec 30 '15 at 03:17