NgFor não atualiza dados com Pipe em Angular2


113

Nesse cenário, estou exibindo uma lista de alunos (matriz) para a visualização com ngFor:

<li *ngFor="#student of students">{{student.name}}</li>

É maravilhoso que ele seja atualizado sempre que adiciono outro aluno à lista.

No entanto, quando atribuo pipea filterpelo nome do aluno,

<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>

Ele não atualiza a lista até que eu digite algo no campo de filtro de nome do aluno.

Aqui está um link para 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;
    }
}


14
Adicione pure:falseem seu Pipe e changeDetection: ChangeDetectionStrategy.OnPushem seu Componente.
Eric Martinez

2
Obrigado @EricMartinez. Funciona. Mas você pode explicar um pouco?
Chu Son

2
Além disso, eu sugiro NÃO usar .test()em sua função de filtro. É porque, se o usuário inserir uma string que inclui caracteres de significado especial como: *ou +etc., seu código será interrompido. Acho que você deve usar .includes()ou escapar da string de consulta com uma função personalizada.
Eggy

6
Adicionar pure:falsee tornar seu canal com estado corrigirá o problema. Modificar ChangeDetectionStrategy não é necessário.
pixelbits

2
Para qualquer um que esteja lendo isso, a documentação para Angular Pipes ficou muito melhor e aborda muitas das mesmas coisas discutidas aqui. Confira.
0xcaff

Respostas:


153

Para compreender totalmente o problema e as possíveis soluções, precisamos discutir a detecção de alteração angular - para tubos e componentes.

Detecção de mudança de tubulação

Tubos sem estado / puros

Por padrão, os canais são sem estado / puros. Pipes sem estado / puros simplesmente transformam dados de entrada em dados de saída. Eles não se lembram de nada, então eles não têm nenhuma propriedade - apenas um transform()método. O Angular pode, portanto, otimizar o tratamento de tubos sem estado / puros: se suas entradas não mudarem, os tubos não precisam ser executados durante um ciclo de detecção de mudança. Para um tubo como {{power | exponentialStrength: factor}}, powere factorsão entradas.

Para esta pergunta, "#student of students | sortByName:queryElem.value", studentse queryElem.valuesão entradas e tubo sortByNameé apátrida / puro. studentsé uma matriz (referência).

  • Quando um aluno é adicionado, a referência do array não muda - studentsnão muda - portanto, o pipe sem estado / puro não é executado.
  • Quando algo é digitado na entrada do filtro, queryElem.valuemuda, portanto, o pipe sem estado / puro é executado.

Uma maneira de corrigir o problema do array é alterar a referência do array sempre que um aluno é adicionado - ou seja, criar um novo array cada vez que um aluno é adicionado. Podemos fazer isso com concat():

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

Embora isso funcione, nosso addNewStudent()método não deve ser implementado de uma determinada maneira apenas porque estamos usando um pipe. Queremos usar push()para adicionar ao nosso array.

Stateful Pipes

Pipes com estado têm estado - eles normalmente têm propriedades, não apenas um transform()método. Eles podem precisar ser avaliados mesmo que suas entradas não tenham mudado. Quando especificamos que um canal tem estado / não puro - pure: falseentão, sempre que o sistema de detecção de alterações do Angular verifica se há alterações em um componente e esse componente usa um canal com estado, ele verifica a saída do canal, quer sua entrada tenha mudado ou não.

Parece o que queremos, embora seja menos eficiente, pois queremos que o pipe seja executado mesmo que a studentsreferência não tenha mudado. Se simplesmente tornarmos o canal com estado, obteremos um erro:

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

De acordo com a resposta de @drewmoore , "este erro só acontece no modo dev (que é ativado por padrão a partir de beta-0). Se você chamar enableProdMode()ao inicializar o aplicativo, o erro não será gerado." Os documentos para oApplicationRef.tick() estado:

No modo de desenvolvimento, tick () também executa um segundo ciclo de detecção de alterações para garantir que nenhuma alteração adicional seja detectada. Se mudanças adicionais forem detectadas durante este segundo ciclo, as ligações no aplicativo têm efeitos colaterais que não podem ser resolvidos em uma única passagem de detecção de mudança. Nesse caso, o Angular gera um erro, pois um aplicativo Angular só pode ter uma passagem de detecção de alteração durante a qual todas as detecções de alteração devem ser concluídas.

Em nosso cenário, acredito que o erro é falso / enganoso. Temos um pipe com estado e a saída pode mudar cada vez que é chamado - pode ter efeitos colaterais e tudo bem. NgFor é avaliado após o tubo, então deve funcionar bem.

No entanto, não podemos realmente desenvolver com esse erro sendo lançado, então uma solução alternativa é adicionar uma propriedade de matriz (ou seja, estado) à implementação do tubo e sempre retornar essa matriz. Veja a resposta de @ pixelbits para esta solução.

No entanto, podemos ser mais eficientes e, como veremos, não precisaremos da propriedade de array na implementação do tubo e não precisaremos de uma solução alternativa para a detecção de dupla alteração.

Detecção de alteração de componente

Por padrão, em cada evento do navegador, a detecção de mudança angular passa por cada componente para ver se ele mudou - entradas e modelos (e talvez outras coisas?) São verificados.

Se soubermos que um componente depende apenas de suas propriedades de entrada (e eventos de modelo), e que as propriedades de entrada são imutáveis, podemos usar a onPushestratégia de detecção de mudança muito mais eficiente . Com essa estratégia, em vez de verificar todos os eventos do navegador, um componente é verificado apenas quando as entradas mudam e quando os eventos do modelo são acionados. E, aparentemente, não recebemos esse Expression ... has changed after it was checkederro com essa configuração. Isso ocorre porque um onPushcomponente não é verificado novamente até que seja "marcado" ( ChangeDetectorRef.markForCheck()) novamente. Portanto, as ligações de modelo e as saídas de pipe com estado são executadas / avaliadas apenas uma vez. Pipes sem estado / puros ainda não são executados, a menos que suas entradas sejam alteradas. Portanto, ainda precisamos de um tubo com estado aqui.

Esta é a solução sugerida por @EricMartinez: pipe stateful com onPushdetecção de mudanças. Consulte a resposta de @caffinatedmonkey para essa solução.

Observe que, com esta solução, o transform()método não precisa retornar o mesmo array todas as vezes. Eu acho isso um pouco estranho: um tubo com estado sem estado. Pensando nisso um pouco mais ... o pipe stateful provavelmente sempre deve retornar o mesmo array. Caso contrário, ele só poderia ser usado com onPushcomponentes no modo de desenvolvimento.


Depois de tudo isso, acho que gosto de uma combinação das respostas de @Eric e @ pixelbits: tubo com estado que retorna a mesma referência de array, com onPushdetecção de mudança se o componente permitir. Uma vez que o canal com estado retorna a mesma referência de matriz, o tubo ainda pode ser usado com componentes que não estão configurados com onPush.

Plunker

Isso provavelmente se tornará um idioma Angular 2: se um array está alimentando um pipe e o array pode mudar (os itens do array, não a referência do array), precisamos usar um pipe stateful.


Obrigado por sua explicação detalhada do problema. É estranho, porém, que a detecção de alterações de matrizes seja implementada como identidade em vez de comparação de igualdade. Seria possível resolver isso com Observable?
Martin Nowak

@MartinNowak, se você está perguntando se o array era um Observable e o pipe era sem estado ... Não sei, não tentei isso.
Mark Rajcok

Outra solução seria um tubo puro, em que a matriz de saída rastreia as alterações na matriz de entrada com ouvintes de eventos.
Tuupertunut

Acabei de bifurcar seu Plunker e ele funciona para mim sem onPushdetecção de alteração plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan

27

Como Eric Martinez apontou nos comentários, adicionar pure: falseao seu Pipedecorador e changeDetection: ChangeDetectionStrategy.OnPushao seu Componentdecorador resolverá seu problema. Aqui está um plunkr de trabalho. Mudar para ChangeDetectionStrategy.Alwaystambém funciona. Aqui está o porquê.

De acordo com o guia angular2 em tubos :

Pipes são sem estado por padrão. Devemos declarar um tubo com estado definindo a purepropriedade do @Pipedecorador como false. Esta configuração informa ao sistema de detecção de mudança do Angular para verificar a saída deste tubo a cada ciclo, se sua entrada mudou ou não.

Quanto ao ChangeDetectionStrategy, por padrão, todas as ligações são verificadas a cada ciclo. Quando um pure: falsepipe é adicionado, acredito que o método de detecção de alterações muda de CheckAlwayspara CheckOncepor motivos de desempenho. Com OnPush, as ligações para o Componente são verificadas apenas quando uma propriedade de entrada muda ou quando um evento é disparado. Para obter mais informações sobre detectores de mudança, uma parte importante do angular2, confira os seguintes links:


"Quando um pure: falsepipe é adicionado, acredito que o método de detecção de alterações muda de CheckAlways para CheckOnce" - isso não concorda com o que você citou nos documentos. Meu entendimento é o seguinte: as entradas de um tubo sem estado são verificadas a cada ciclo por padrão. Se houver uma alteração, o transform()método do pipe é chamado para (re) gerar a saída. O transform()método de um pipe com estado é chamado de cada ciclo e sua saída é verificada quanto a alterações. Consulte também stackoverflow.com/a/34477111/215945 .
Mark Rajcok

@MarkRajcok, Opa, você está certo, mas se for esse o caso, por que mudar a estratégia de detecção de alterações funciona?
0xcaff

1
Ótima pergunta. Parece que quando a estratégia de detecção de mudança é alterada para OnPush(ou seja, o componente é marcado como "imutável"), a saída do tubo com estado não precisa "estabilizar" - ou seja, parece que o transform()método é executado apenas uma vez (provavelmente todos a detecção de alterações para o componente é executada apenas uma vez). Compare isso com a resposta de @ pixelbits, onde o transform()método é chamado várias vezes e precisa se estabilizar (de acordo com a outra resposta do pixelbits ), daí a necessidade de usar a mesma referência de array.
Mark Rajcok

@MarkRajcok Se o que você diz está correto, em termos de eficiência, esta é provavelmente a melhor maneira, neste caso de uso em particular porque transformsó é chamado quando novos dados são enviados em vez de várias vezes até que a saída se estabilize, certo?
0xcaff

1
Provavelmente, veja minha resposta prolixo. Gostaria que houvesse mais documentação sobre a detecção de alterações no Angular 2.
Mark Rajcok

22

Demo Plunkr

Você não precisa alterar ChangeDetectionStrategy. Implementar um Pipe stateful é suficiente para fazer tudo funcionar.

Este é um canal com estado (nenhuma outra alteração foi feita):

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

Recebi este erro sem alterar changeDectection para 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]
Chu Son

1
Você pode exemplificar por que precisa armazenar valores em uma variável local em SortByNamePipe e, em seguida, retorná-los na função de transformação @pixelbits? Também notei que o ciclo angular através da função de transformação duas vezes com changeDetetion.OnPush e 4 vezes sem ele
Chu Son

@ChuSon, você provavelmente está olhando os registros de uma versão anterior. Em vez de retornar uma matriz de valores, estamos retornando uma referência a uma matriz de valores, que pode ser detectada por alterações, acredito. Pixelbits, sua resposta faz mais sentido.
0xcaff

Para benefício de outros leitores, em relação ao erro ChuSon mencionado acima nos comentários, e a necessidade de usar uma variável local, outra questão foi criada , respondida por pixelbits.
Mark Rajcok

19

Da documentação angular

Canos puros e impuros

Existem duas categorias de tubos: puro e impuro. Pipes são puros por padrão. Cada cachimbo que você viu até agora é puro. Você torna um cano impuro definindo seu sinalizador puro como falso. Você poderia tornar o FlyingHeroesPipe impuro assim:

@Pipe({ name: 'flyingHeroesImpure', pure: false })

Antes de fazer isso, entenda a diferença entre puro e impuro, começando com um tubo puro.

Canais puros O Angular executa um cano puro apenas quando detecta uma mudança pura no valor de entrada. Uma mudança pura é uma mudança em um valor de entrada primitivo (String, Number, Boolean, Symbol) ou uma referência de objeto alterada (Date, Array, Function, Object).

Angular ignora alterações em objetos (compostos). Ele não chamará um pipe puro se você alterar um mês de entrada, adicionar a uma matriz de entrada ou atualizar uma propriedade de objeto de entrada.

Isso pode parecer restritivo, mas também é rápido. Uma verificação de referência de objeto é rápida - muito mais rápida do que uma verificação profunda de diferenças - então o Angular pode determinar rapidamente se pode pular a execução do tubo e uma atualização da vista.

Por esse motivo, um pipe puro é preferível quando você pode conviver com a estratégia de detecção de alterações. Quando você não pode, você pode usar o cano impuro.


A resposta está correta, mas seria bom um pouco mais de esforço ao postá-la
Stefan

2

Em vez de fazer puro: falso. Você pode copiar em profundidade e substituir o valor no componente por this.students = Object.assign ([], NEW_ARRAY); onde NEW_ARRAY é a matriz modificada.

Ele funciona para o angular 6 e deve funcionar para outras versões do angular também.


0

Uma solução alternativa: importar manualmente o tubo no construtor e chamar o método de transformação usando este tubo

constructor(
private searchFilter : TableFilterPipe) { }

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

Na verdade você nem precisa de um cachimbo


0

Adicione o parâmetro extra ao pipe e altere-o logo após a mudança da matriz, e mesmo com o pipe puro, a lista será atualizada

deixar item de itens | tubo: param


0

Neste caso de uso, usei meu Pipe no arquivo ts para filtragem de dados. É muito melhor para desempenho do que usar tubos puros. Use em ts como este:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}

0

para criar tubos impuros são caros no desempenho. portanto, não crie tubos impuros, em vez disso, altere a referência da variável de dados criando uma cópia dos dados quando alterar os dados e reatribua a referência de cópia na variável de dados original.

            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();
            } 
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.