Como detectar quando um valor @Input () é alterado no Angular?

471

Eu tenho um componente pai ( CategoryComponent ), um componente filho ( videoListComponent ) e um ApiService.

Eu tenho a maior parte deste trabalho bem, ou seja, cada componente pode acessar o json api e obter seus dados relevantes através de observáveis.

Atualmente, o componente da lista de vídeos obtém todos os vídeos. Gostaria de filtrar isso apenas para vídeos em uma categoria específica. Consegui isso passando o categoryId para a criança via @Input().

CategoryComponent.html

<video-list *ngIf="category" [categoryId]="category.id"></video-list>

Isso funciona e quando a categoria pai CategoryComponent muda, o valor categoryId é passado através, @Input()mas preciso detectá-lo no VideoListComponent e solicitar novamente a matriz de vídeos via APIService (com o novo categoryId).

No AngularJS, eu teria feito um $watchsobre a variável. Qual a melhor forma de lidar com isto?

Jon Catmull
fonte
assistir mudanças de entrada: - angulartutorial.net/2017/12/watch-input-changes-angular-4.html
Prashobh
para alterações na matriz: stackoverflow.com/questions/42962394/…
dasfdsa

Respostas:

720

Na verdade, existem duas maneiras de detectar e agir quando uma entrada é alterada no componente filho em angular2 +:

  1. Você pode usar o método do ciclo de vida ngOnChanges () como também mencionado nas respostas mais antigas:
    @Input() categoryId: string;

    ngOnChanges(changes: SimpleChanges) {

        this.doSomething(changes.categoryId.currentValue);
        // You can also use categoryId.previousValue and 
        // categoryId.firstChange for comparing old and new values

    }

Links da documentação: ngOnChanges, SimpleChanges, SimpleChange
Demo Exemplo: Veja este desentupidor

  1. Como alternativa, você também pode usar um configurador de propriedades de entrada da seguinte maneira:
    private _categoryId: string;

    @Input() set categoryId(value: string) {

       this._categoryId = value;
       this.doSomething(this._categoryId);

    }

    get categoryId(): string {

        return this._categoryId;

    }

Link da documentação: veja aqui .

Exemplo de demonstração: olhe para este afunilador .

QUE ABORDAGEM DEVE USAR?

Se o seu componente tiver várias entradas, se você usar ngOnChanges (), todas as alterações serão obtidas de todas as entradas de uma vez dentro de ngOnChanges (). Usando essa abordagem, você também pode comparar os valores atuais e anteriores da entrada que foi alterada e executar as ações adequadamente.

No entanto, se você quiser fazer algo quando apenas uma única entrada específica for alterada (e você não se importa com as outras entradas), pode ser mais simples usar um configurador de propriedades de entrada. No entanto, essa abordagem não fornece uma maneira integrada de comparar valores anteriores e atuais da entrada alterada (o que você pode fazer facilmente com o método do ciclo de vida ngOnChanges).

EDIT 2017-07-25: A DETECÇÃO DAS MUDANÇAS ANGULARES PODE AINDA NÃO FOGO SOB ALGUMAS CIRCUNSTÂNCIAS

Normalmente, a detecção de alterações para o setter e ngOnChanges será acionada sempre que o componente pai alterar os dados que passa para o filho, desde que os dados sejam um tipo de dados primitivo JS (string, número, booleano) . No entanto, nos seguintes cenários, ele não será acionado e você precisará executar ações extras para fazê-lo funcionar.

  1. Se você estiver usando um objeto ou matriz aninhada (em vez de um tipo de dados primitivo JS) para passar dados de Pai para Filho, a detecção de alterações (usando o setter ou ngchanges) poderá não ser acionada, como também mencionado na resposta do usuário: muetzerich. Para soluções, veja aqui .

  2. Se você estiver modificando dados fora do contexto angular (ou seja, externamente), o angular não saberá das alterações. Pode ser necessário usar o ChangeDetectorRef ou o NgZone em seu componente para conscientizar angularmente as alterações externas e, assim, acionar a detecção de alterações. Consulte isso .

Alan CS
fonte
8
Muito bom ponto sobre o uso de setters com entradas, atualizarei para que seja a resposta aceita, pois é mais abrangente. Obrigado.
precisa
2
@trichetriche, o setter (método set) será chamado sempre que o pai alterar a entrada. Além disso, o mesmo vale para ngOnChanges ().
Alan CS
Bem, aparentemente não ... Vou tentar descobrir isso, provavelmente é algo errado que eu fiz.
1
Acabei de me deparar com essa pergunta, notei que você disse que usar o setter não permitirá comparar valores, no entanto isso não é verdade, você pode chamar seu doSomethingmétodo e usar 2 argumentos como valores novos e antigos antes de realmente definir o novo valor, de outra maneira seria armazenar o valor antigo antes de definir e chamar o método depois disso.
usar o seguinte texto
1
@ T.Aoukar O que estou dizendo é que o configurador de entrada não suporta nativamente a comparação de valores antigos / novos como o ngOnChanges (). Você sempre pode usar hacks para armazenar o valor antigo (como você mencionou) para compará-lo com o novo valor.
Alan CS
105

Use o ngOnChanges()método do ciclo de vida em seu componente.

O ngOnChanges é chamado logo após a verificação das propriedades associadas aos dados e antes da verificação dos filhos de exibição e conteúdo, se pelo menos uma delas tiver sido alterada.

Aqui estão os documentos .

muetzerich
fonte
6
Isso funciona quando uma variável é configurada para um novo valor MyComponent.myArray = [], por exemplo , mas se um valor de entrada for alterado MyComponent.myArray.push(newValue), por exemplo , ngOnChanges()não é acionado. Alguma idéia de como capturar essa alteração?
Levi Fuller
4
ngOnChanges()não é chamado quando um objeto aninhado é alterado. Talvez você encontre uma solução aqui
muetzerich
33

Eu estava recebendo erros no console, assim como no compilador e no IDE ao usar o SimpleChangestipo na assinatura da função. Para evitar os erros, use a anypalavra - chave na assinatura.

ngOnChanges(changes: any) {
    console.log(changes.myInput.currentValue);
}

EDITAR:

Como Jon apontou abaixo, você pode usar a SimpleChangesassinatura ao usar a notação de colchete em vez da notação de ponto.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
Darcy
fonte
1
Você importou a SimpleChangesinterface na parte superior do seu arquivo?
precisa
@ Jon - Sim. O problema não era a assinatura em si, mas o acesso aos parâmetros atribuídos em tempo de execução ao objeto SimpleChanges. Por exemplo, no meu componente, vinculei minha classe User à entrada (ie <my-user-details [user]="selectedUser"></my-user-details>). Essa propriedade é acessada da classe SimpleChanges chamando changes.user.currentValue. Aviso userestá ligada em tempo de execução e não parte do objeto SimpleChanges
Darcy
1
Você já tentou isso? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
precisa
@ Jon - Mudar para a notação de suporte faz o truque. Atualizei minha resposta para incluir isso como alternativa.
Darcy
1
import {SimpleChanges} de '@ angular / core'; Se é nos docs angulares, e não um tipo typescript nativa, então é provavelmente a partir de um módulo angular, muito provavelmente 'core / angular'
BentOnCoding
13

A aposta mais segura é usar um serviço compartilhado em vez de um @Inputparâmetro. Além disso, o @Inputparâmetro não detecta alterações no tipo de objeto aninhado complexo.

Um exemplo simples de serviço é o seguinte:

Service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class SyncService {

    private thread_id = new Subject<number>();
    thread_id$ = this.thread_id.asObservable();

    set_thread_id(thread_id: number) {
        this.thread_id.next(thread_id);
    }

}

Component.ts

export class ConsumerComponent implements OnInit {

  constructor(
    public sync: SyncService
  ) {
     this.sync.thread_id$.subscribe(thread_id => {
          **Process Value Updates Here**
      }
    }

  selectChat(thread_id: number) {  <--- How to update values
    this.sync.set_thread_id(thread_id);
  }
}

Você pode usar uma implementação semelhante em outros componentes e todos os seus componentes compartilharão os mesmos valores compartilhados.

Sanket Berde
fonte
1
Obrigado Sanket. Este é um ponto muito bom a ser feito e, quando apropriado, uma maneira melhor de fazê-lo. Deixarei a resposta aceita como está, pois responde a minhas perguntas específicas sobre a detecção de alterações do @Input.
precisa
1
Um parâmetro @Input não detecta alterações dentro de um tipo de objeto aninhado complexo, mas se o próprio objeto for alterado, ele será acionado, certo? por exemplo, eu tenho o parâmetro de entrada "pai", portanto, se eu mudar o pai como um todo, a detecção de alterações será acionada, mas se eu mudar o pai.nome, não será?
yogibimbi
Você deve fornecer uma razão pela qual a sua solução é mais seguro
Joshua Kemmerer
1
O @Inputparâmetro @JoshuaKemmerer não detecta alterações no tipo de objeto aninhado complexo, mas em objetos nativos simples.
Sanket Berde 5/05
6

Eu só quero acrescentar que existe outro gancho de ciclo de vida chamado DoCheckque é útil se o @Inputvalor não for um valor primitivo.

Eu tenho uma matriz como uma, Inputpara que isso não acione o OnChangesevento quando o conteúdo for alterado (porque a verificação feita pelo Angular é 'simples' e não profunda, portanto a matriz ainda é uma matriz, mesmo que o conteúdo da matriz tenha sido alterado).

Em seguida, implemento um código de verificação personalizado para decidir se quero atualizar minha exibição com a matriz alterada.

Stevo
fonte
6
  @Input()
  public set categoryId(categoryId: number) {
      console.log(categoryId)
  }

por favor, tente usar este método. Espero que isto ajude

Akshay Rajput
fonte
4

Você também pode ter um observável que aciona alterações no componente pai (CategoryComponent) e fazer o que você deseja fazer na assinatura no componente filho. (videoListComponent)

service.ts 
public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);

-----------------
CategoryComponent.ts
public onCategoryChange(): void {
  service.categoryChange$.next();
}

-----------------
videoListComponent.ts
public ngOnInit(): void {
  service.categoryChange$.subscribe(() => {
   // do your logic
});
}
Josf
fonte
2

Aqui o ngOnChanges será acionado sempre que sua propriedade de entrada for alterada:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}
Chirag
fonte
1

Esta solução usa uma classe proxy e oferece as seguintes vantagens:

  • Permite que o consumidor aproveite o poder do RXJS
  • Mais compacto do que outras soluções propostas até agora
  • Mais seguro do que usarngOnChanges()

Exemplo de uso:

@Input()
public num: number;
numChanges$ = observeProperty(this as MyComponent, 'num');

Função útil:

export function observeProperty<T, K extends keyof T>(target: T, key: K) {
  const subject = new BehaviorSubject<T[K]>(target[key]);
  Object.defineProperty(target, key, {
    get(): T[K] { return subject.getValue(); },
    set(newValue: T[K]): void {
      if (newValue !== subject.getValue()) {
        subject.next(newValue);
      }
    }
  });
  return subject;
}
Stephen Paul
fonte
0

Se você não quiser usar o método ngOnChange implementar o método onChange (), também poderá se inscrever nas alterações de um item específico pelo evento valueChanges , ETC.

 myForm= new FormGroup({
first: new FormControl()   

});

this.myForm.valueChanges.subscribe(formValue => {
  this.changeDetector.markForCheck();
});

o markForCheck () escrito por causa do uso nesta declaração:

  changeDetection: ChangeDetectionStrategy.OnPush
user1012506
fonte
3
Isso é completamente diferente da pergunta e, embora as pessoas úteis devam estar cientes de que seu código é sobre um tipo diferente de alteração. A pergunta feita sobre a alteração de dados que é de FORA de um componente e depois informar seu componente e responder ao fato de que os dados foram alterados. Sua resposta está relacionada à detecção de alterações no próprio componente em itens como entradas em um formulário que são LOCAIS para o componente.
rmcsharry
0

Você também pode simplesmente passar um EventEmitter como Input. Não tenho certeza se essa é a melhor prática ...

CategoryComponent.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();

- OTHER CODE -

setCategoryId(id) {
 this.category.id = id;
 this.categoryIdEvent.emit(this.category.id);
}

CategoryComponent.html:

<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>

E em VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}
Cédric Schweizer
fonte
Forneça links úteis, trechos de código para dar suporte à sua solução.
mishsx
Eu adicionei um exemplo.
Cédric Schweizer