In this scenario, I'm displaying a list of students (array) to the view with ngFor
:
<li *ngFor="#student of students">{{student.name}}</li>
It's wonderful that it updates whenever I add other student to the list.
However, when I give it a pipe
to filter
by the student name,
<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
It does not update the list until I type something in the filtering student name field.
Here's a link to plnkr.
Hello_world.html
<h1>Students:</h1>
<label for="newStudentName"></label>
<input type="text" name="newStudentName" placeholder="newStudentName" #newStudentElem>
<button (click)="addNewStudent(newStudentElem.value)">Add New Student</button>
<br>
<input type="text" placeholder="Search" #queryElem (keyup)="0">
<ul>
<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
</ul>
sort_by_name_pipe.ts
import {Pipe} from 'angular2/core';
@Pipe({
name: 'sortByName'
})
export class SortByNamePipe {
transform(value, [queryString]) {
// console.log(value, queryString);
return value.filter((student) => new RegExp(queryString).test(student.name))
// return value;
}
}
Demo Plunkr
You don't need to change the ChangeDetectionStrategy. Implementing a stateful Pipe is enough to get everything working.
This is a stateful pipe (no other changes were made):
A workaround: Manually import Pipe in constructor and call transform method using this pipe
Actually you don't even need a pipe
Instead of doing pure:false. You can deep copy and replace the value in the component by this.students = Object.assign([], NEW_ARRAY); where NEW_ARRAY is the modified array.
It works for angular 6 and should work for other angular versions as well.
To fully understand the problem and possible solutions, we need to discuss Angular change detection -- for pipes and components.
Pipe Change Detection
Stateless/pure Pipes
By default, pipes are stateless/pure. Stateless/pure pipes simply transform input data into output data. They don't remember anything, so they don't have any properties – just a
transform()
method. Angular can therefore optimize treatment of stateless/pure pipes: if their inputs don't change, the pipes don't need to be executed during a change detection cycle. For a pipe such as{{power | exponentialStrength: factor}}
,power
andfactor
are inputs.For this question,
"#student of students | sortByName:queryElem.value"
,students
andqueryElem.value
are inputs, and pipesortByName
is stateless/pure.students
is an array (reference).students
doesn't change – hence the stateless/pure pipe is not executed.queryElem.value
does change, hence the stateless/pure pipe is executed.One way to fix the array issue is to change the array reference each time a student is added – i.e., create a new array each time a student is added. We could do this with
concat()
:Although this works, our
addNewStudent()
method shouldn't have to be implemented a certain way just because we're using a pipe. We want to usepush()
to add to our array.Stateful Pipes
Stateful pipes have state -- they normally have properties, not just a
transform()
method. They may need to be evaluated even if their inputs haven't changed. When we specify that a pipe is stateful/non-pure –pure: false
– then whenever Angular's change detection system checks a component for changes and that component uses a stateful pipe, it will check the output of the pipe, whether its input has changed or not.This sounds like what we want, even though it is less efficient, since we want the pipe to execute even if the
students
reference hasn't changed. If we simply make the pipe stateful, we get an error:According to @drewmoore's answer, "this error only happens in dev mode (which is enabled by default as of beta-0). If you call
enableProdMode()
when bootstrapping the app, the error won't get thrown." The docs forApplicationRef.tick()
state:In our scenario I believe the error is bogus/misleading. We have a stateful pipe, and the output can change each time it is called – it can have side-effects and that's okay. NgFor is evaluated after the pipe, so it should work fine.
However, we can't really develop with this error being thrown, so one workaround is to add an array property (i.e., state) to the pipe implementation and always return that array. See @pixelbits's answer for this solution.
However, we can be more efficient, and as we'll see, we won't need the array property in the pipe implementation, and we won't need a workaround for the double change detection.
Component Change Detection
By default, on every browser event, Angular change detection goes through every component to see if it changed – inputs and templates (and maybe other stuff?) are checked.
If we know that a component only depends on its input properties (and template events), and that the input properties are immutable, we can use the much more efficient
onPush
change detection strategy. With this strategy, instead of checking on every browser event, a component is checked only when the inputs change and when template events trigger. And, apparently, we don't get thatExpression ... has changed after it was checked
error with this setting. This is because anonPush
component is not checked again until it is "marked" (ChangeDetectorRef.markForCheck()
) again. So Template bindings and stateful pipe outputs are executed/evaluated only once. Stateless/pure pipes are still not executed unless their inputs change. So we still need a stateful pipe here.This is the solution @EricMartinez suggested: stateful pipe with
onPush
change detection. See @caffinatedmonkey's answer for this solution.Note that with this solution the
transform()
method doesn't need to return the same array each time. I find that a bit odd though: a stateful pipe with no state. Thinking about it some more... the stateful pipe probably should always return the same array. Otherwise it could only be used withonPush
components in dev mode.So after all that, I think I like a combination of @Eric's and @pixelbits's answers: stateful pipe that returns the same array reference, with
onPush
change detection if the component allows it. Since the stateful pipe returns the same array reference, the pipe can still be used with components that are not configured withonPush
.Plunker
This will probably become an Angular 2 idiom: if an array is feeding a pipe, and the array might change (the items in the array that is, not the array reference), we need to use a stateful pipe.
From the angular documentation
Pure and impure pipes
There are two categories of pipes: pure and impure. Pipes are pure by default. Every pipe you've seen so far has been pure. You make a pipe impure by setting its pure flag to false. You could make the FlyingHeroesPipe impure like this:
@Pipe({ name: 'flyingHeroesImpure', pure: false })
Before doing that, understand the difference between pure and impure, starting with a pure pipe.
Pure pipes Angular executes a pure pipe only when it detects a pure change to the input value. A pure change is either a change to a primitive input value (String, Number, Boolean, Symbol) or a changed object reference (Date, Array, Function, Object).
Angular ignores changes within (composite) objects. It won't call a pure pipe if you change an input month, add to an input array, or update an input object property.
This may seem restrictive but it's also fast. An object reference check is fast—much faster than a deep check for differences—so Angular can quickly determine if it can skip both the pipe execution and a view update.
For this reason, a pure pipe is preferable when you can live with the change detection strategy. When you can't, you can use the impure pipe.
As Eric Martinez pointed out in the comments, adding
pure: false
to yourPipe
decorator andchangeDetection: ChangeDetectionStrategy.OnPush
to yourComponent
decorator will fix your issue. Here is a working plunkr. Changing toChangeDetectionStrategy.Always
, also works. Here's why.According to the angular2 guide on pipes:
As for the
ChangeDetectionStrategy
, by default, all bindings are checked every single cycle. When apure: false
pipe is added, I believe the change detection method changes to fromCheckAlways
toCheckOnce
for performance reasons. WithOnPush
, bindings for the Component are only checked when an input property changes or when an event is triggered. For more information about change detectors, an important part ofangular2
, check out the following links: