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}}
, power
e factor
são entradas.
Para esta pergunta, "#student of students | sortByName:queryElem.value"
, students
e queryElem.value
sã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 -
students
não muda - portanto, o pipe sem estado / puro não é executado.
- Quando algo é digitado na entrada do filtro,
queryElem.value
muda, 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: false
entã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 students
referê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 onPush
estraté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 checked
erro com essa configuração. Isso ocorre porque um onPush
componente 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 onPush
detecçã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 onPush
componentes 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 onPush
detecçã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.
pure:false
em seu Pipe echangeDetection: ChangeDetectionStrategy.OnPush
em seu Componente.