NgFor doesn't update data with Pipe in Angular2
Asked Answered
B

10

126

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;
    }
}

Blastocyst answered 24/12, 2015 at 18:7 Comment(5)
Add pure:false in your Pipe, and changeDetection: ChangeDetectionStrategy.OnPush in your Component.Intercross
Thanks @EricMartinez. It works. But can you explain a little bit?Blastocyst
Also, I would suggest to NOT using .test() in your filter function. Its because, if user inputs a string that includes special meaning characters like: * or + etc. your code will break. I think you should use .includes() or escape query string with custom function.Adz
Adding pure:false and making your pipe stateful will fix the issue. Modifying ChangeDetectionStrategy is not necessary.Ajar
For anyone reading this, the documentation for Angular Pipes has gotten much better and goes over many of the same things discussed here. Check it out.Peadar
O
180

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 and factor are inputs.

For this question, "#student of students | sortByName:queryElem.value", students and queryElem.value are inputs, and pipe sortByName is stateless/pure. students is an array (reference).

  • When a student is added, the array reference doesn't change – students doesn't change – hence the stateless/pure pipe is not executed.
  • When something is typed into the filter input, 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():

this.students = this.students.concat([{name: studentName}]);

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 use push() 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:

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

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 for ApplicationRef.tick() state:

In development mode, tick() also performs a second change detection cycle to ensure that no further changes are detected. If additional changes are picked up during this second cycle, bindings in the app have side-effects that cannot be resolved in a single change detection pass. In this case, Angular throws an error, since an Angular application can only have one change detection pass during which all change detection must complete.

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 that Expression ... has changed after it was checked error with this setting. This is because an onPush 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 with onPush 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 with onPush.

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.

Oyer answered 28/12, 2015 at 16:56 Comment(6)
Thanks for your detailed explanation of the problem. It's weird though that change detection of arrays is implemented as identity instead of equality comparison. Would it be possible to solve this with Observable?Febrifuge
@MartinNowak, if you are asking if the array were an Observable, and the pipe were stateless... I don't know, I haven't tried that.Oyer
Another solution would be a pure pipe where the output array is tracking changes in the input array with event listeners.Kolkhoz
I've just forked your Plunker, and it works for me without onPush change detection plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=previewHartmann
Making the pipe impure does not cause any errors in development mode in newer versions of Angular, so there is now no need to overcomplicate things. Please refer to my answer for more details.Plainsong
Thanks, I took the simple approach and cloned my array to trigger the state change. this.students = [...this.students];Anastomose
P
31

As Eric Martinez pointed out in the comments, adding pure: false to your Pipe decorator and changeDetection: ChangeDetectionStrategy.OnPush to your Component decorator will fix your issue. Here is a working plunkr. Changing to ChangeDetectionStrategy.Always, also works. Here's why.

According to the angular2 guide on pipes:

Pipes are stateless by default. We must declare a pipe to be stateful by setting the pure property of the @Pipe decorator to false. This setting tells Angular’s change detection system to check the output of this pipe each cycle, whether its input has changed or not.

As for the ChangeDetectionStrategy, by default, all bindings are checked every single cycle. When a pure: false pipe is added, I believe the change detection method changes to from CheckAlways to CheckOnce for performance reasons. With OnPush, 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 of angular2, check out the following links:

Peadar answered 25/12, 2015 at 2:30 Comment(6)
"When a pure: false pipe is added, I believe the change detection method changes to from CheckAlways to CheckOnce" -- that doesn't agree with what you quoted from the docs. My understanding is as follows: a stateless pipe's inputs are checked every cycle by default. If there is a change, the pipe's transform() method is called to (re)generate the output. A stateful pipe's transform() method is called every cycle, and its output is checked for changes. See also https://mcmap.net/q/67061/-error-value-has-changed-after-checked-stateful-pipe-in-angular2.Oyer
@MarkRajcok, Oops, you're right, but if that is the case, why does changing the change detection strategy work?Peadar
Great question. It would seem that when the change detection strategy is changed to OnPush (i.e., component is marked as "immutable"), the stateful pipe output does not have to "stabilize" -- i.e., it seems the transform() method is run only once (probably all change detection for the component is run only once). Contrast that to @pixelbits's answer, where the transform() method is called multiple times, and it has to stabilize (according to pixelbits's other answer), hence the need to use the same array reference.Oyer
@MarkRajcok If what you say is correct, in terms of efficiency, this is probably the better way, in this particular use case because transform is only called when new data is pushed instead of multiple times until the output stabilizes, right?Peadar
Probably, see my long-winded answer. I wish there was more documentation about change detection in Angular 2.Oyer
I thought about this some more... I like a combination of the two approaches: stateful pipe (required), have transform() return the same array reference (so it can be used with any component), use onPush if the component allows it. I updated my answer to discuss this somewhat.Oyer
A
24

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):

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe {
  tmp = [];
  transform (value, [queryString]) {
    this.tmp.length = 0;
    // console.log(value, queryString);
    var arr = value.filter((student)=>new RegExp(queryString).test(student.name));
    for (var i =0; i < arr.length; ++i) {
        this.tmp.push(arr[i]);
     }

    return this.tmp;
  }
}
Ajar answered 25/12, 2015 at 22:46 Comment(4)
I got this error without altering changeDectection to onPush: [plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=preview](Plnkr) Expression 'students | sortByName:queryElem.value in HelloWorld@7:6' has changed after it was checked. Previous value: '[object Object],[object Object],[object Object]'. Current value: '[object Object],[object Object],[object Object]' in [students | sortByName:queryElem.value in HelloWorld@7:6]Blastocyst
Can you example why you need to store values to a local variable in SortByNamePipe and then return it in transform function @pixelbits? I also noticed that Angular cycle through transform function twice with changeDetetion.OnPush and 4 times without itBlastocyst
@ChuSon, you're probably looking at logs from a previous version. Instead of returning an array of values, we are returning a reference to an array of values, which can be change detected, I believe. Pixelbits, your answer makes more sense.Peadar
For the benefit of other readers, regarding the error ChuSon mentioned above in the comments, and the need to use a local variable, another question was created, and answered by pixelbits.Oyer
S
22

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.

Subtlety answered 6/6, 2017 at 12:13 Comment(1)
Answer is correct, but a bit more effort when posting it would be niceSwamper
P
5

There is now no need to overcomplicate things!

Making the pipe impure does not cause any errors in development mode in newer versions of Angular. I guess the error mentioned in the currently accepted answer had something to do with this issue, which was resolved 5.5 (!) years ago (shortly after this question was posted).

As far as I understand, Angular now uses IterableDiffer to detect changes in arrays returned by impure pipes exactly as it does with normal arrays that appear in a template (when the default change detection strategy is used), so it does not think it is a problem when the array reference has changed provided that its contents have not. That means there won't be any error if we generate a new array each time, so we can just make the pipe impure, and that will do the trick.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe implements PipeTransform {
  transform(value, queryString) {
    return value.filter(student => new RegExp(queryString).test(student.name));
  }
}
Plainsong answered 15/8, 2021 at 8:11 Comment(0)
A
3

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.

Alec answered 4/11, 2018 at 15:3 Comment(0)
S
0

A workaround: Manually import Pipe in constructor and call transform method using this pipe

constructor(
private searchFilter : TableFilterPipe) { }

onChange() {
   this.data = this.searchFilter.transform(this.sourceData, this.searchText)}

Actually you don't even need a pipe

Statue answered 25/10, 2018 at 20:4 Comment(0)
A
0

Add to pipe extra parameter, and change it right after array change, and even with pure pipe, list will be refreshed

let item of items | pipe:param

Amandaamandi answered 27/1, 2019 at 1:51 Comment(0)
V
0

In this usage case i used my Pipe in ts file for data filtering. It is much better for performance, than using pure pipes. Use in ts like this:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}
Valladolid answered 26/7, 2019 at 14:8 Comment(0)
S
0

to create impure pipes are costly at performance. so not create impure pipes, rather change the reference of data variable by create copy of data when change in data and reassign the reference of copy in original data variable.

            emp=[];
            empid:number;
            name:string;
            city:string;
            salary:number;
            gender:string;
            dob:string;
            experience:number;

            add(){
              const temp=[...this.emps];
              const e={empid:this.empid,name:this.name,gender:this.gender,city:this.city,salary:this.salary,dob:this.dob,experience:this.experience};
              temp.push(e); 
              this.emps =temp;
              //this.reset();
            } 
Sundry answered 18/5, 2020 at 16:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.