Angular 2 - Exibir não atualizando após alterações no modelo

128

Eu tenho um componente simples que chama uma API REST a cada poucos segundos e recebe de volta alguns dados JSON. Posso ver nas minhas instruções de log e no tráfego de rede que os dados JSON retornados estão mudando e meu modelo está sendo atualizado, no entanto, a exibição não está mudando.

Meu componente se parece com:

import {Component, OnInit} from 'angular2/core';
import {RecentDetectionService} from '../services/recentdetection.service';
import {RecentDetection} from '../model/recentdetection';
import {Observable} from 'rxjs/Rx';

@Component({
    selector: 'recent-detections',
    templateUrl: '/app/components/recentdetection.template.html',
    providers: [RecentDetectionService]
})



export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { this.recentDetections = recent;
             console.log(this.recentDetections[0].macAddress) });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

E minha visão se parece com:

<div class="panel panel-default">
    <!-- Default panel contents -->
    <div class="panel-heading"><h3>Recently detected</h3></div>
    <div class="panel-body">
        <p>Recently detected devices</p>
    </div>

    <!-- Table -->
    <table class="table" style="table-layout: fixed;  word-wrap: break-word;">
        <thead>
            <tr>
                <th>Id</th>
                <th>Vendor</th>
                <th>Time</th>
                <th>Mac</th>
            </tr>
        </thead>
        <tbody  >
            <tr *ngFor="#detected of recentDetections">
                <td>{{detected.broadcastId}}</td>
                <td>{{detected.vendor}}</td>
                <td>{{detected.timeStamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
                <td>{{detected.macAddress}}</td>
            </tr>
        </tbody>
    </table>
</div>

console.log(this.recentDetections[0].macAddress)Pude ver pelos resultados que o objeto recentDetections está sendo atualizado, mas a tabela na exibição nunca muda, a menos que eu recarregue a página.

Estou lutando para ver o que estou fazendo de errado aqui. Alguém pode ajudar?

Matt Watson
fonte
Eu recomendo tornar o código mais limpo e menos complexo: stackoverflow.com/a/48873980/571030
glukki:

Respostas:

215

Pode ser que o código em seu serviço de alguma forma saia da zona da Angular. Isso interrompe a detecção de alterações. Isso deve funcionar:

import {Component, OnInit, NgZone} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private zone:NgZone, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                 this.zone.run(() => { // <== added
                     this.recentDetections = recent;
                     console.log(this.recentDetections[0].macAddress) 
                 });
        });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Para outras maneiras de chamar a detecção de alterações, consulte Disparando manualmente a detecção de alterações no Angular.

Formas alternativas de chamar a detecção de alterações são

ChangeDetectorRef.detectChanges()

executar imediatamente a detecção de alterações para o componente atual e seus filhos

ChangeDetectorRef.markForCheck()

para incluir o componente atual na próxima vez que o Angular executar a detecção de alterações

ApplicationRef.tick()

executar a detecção de alterações para todo o aplicativo

Günter Zöchbauer
fonte
9
Outra alternativa é injetar ChangeDetectorRef e chamar em cdRef.detectChanges()vez de zone.run(). Isso poderia ser mais eficiente, pois não executará a detecção de alterações em toda a árvore de componentes, como zone.run()faz.
precisa saber é o seguinte
1
Aceita. Use apenas detectChanges()se alguma alteração feita for local para o componente e seus descendentes. Meu entendimento é que detectChanges()irá verificar o componente e seus descendentes, mas zone.run()e ApplicationRef.tick()irá verificar toda a árvore de componentes.
precisa saber é o seguinte
2
exatamente o que eu estava procurando ... usar @Input()não fazia sentido nem funcionava.
yorch 15/09/16
15
Realmente não entendo por que precisamos de todo esse material manual ou por que os deuses angular2 deixaram essa necessidade. Por que @Input não significa que realmente o componente depende de tudo o que o componente pai disse que dependia dos dados alterados? Parece extremamente deselegante que não funcione apenas. o que estou perdendo?
13
Trabalhou para mim. Mas esse é outro exemplo do motivo pelo qual sinto que os desenvolvedores da estrutura angular estão perdidos na complexidade da estrutura angular. Ao usar o angular como desenvolvedor de aplicativos, você gasta muito tempo para entender como o angular faz isso ou aquilo e como solucionar problemas como os questionados acima. Terrrible !!
1013 Karl
32

É originalmente uma resposta nos comentários de @Mark Rajcok, mas quero colocá-lo aqui como testado e funcionou como uma solução usando o ChangeDetectorRef , vejo um bom ponto aqui:

Outra alternativa é injetar ChangeDetectorRefe chamar em cdRef.detectChanges()vez de zone.run(). Isso poderia ser mais eficiente, pois não executará a detecção de alterações em toda a árvore de componentes, como zone.run()faz. - Mark Rajcok

Portanto, o código deve ser como:

import {Component, OnInit, ChangeDetectorRef} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

Editar : o uso de .detectChanges()subscibe interno pode levar ao problema Tente usar uma exibição destruída: detectChanges

Para resolvê-lo, você precisa unsubscribeantes de destruir o componente, para que o código completo seja como:

import {Component, OnInit, ChangeDetectorRef, OnDestroy} from 'angular2/core';

export class RecentDetectionComponent implements OnInit, OnDestroy {

    recentDetections: Array<RecentDetection>;
    private timerObserver: Subscription;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        this.timerObserver = timer.subscribe(() => this.getRecentDetections());
    }

    ngOnDestroy() {
        this.timerObserver.unsubscribe();
    }

}
Al-Mothafar
fonte
this.cdRef.detectChanges (); corrigiu meu problema. Obrigado!
MANGESH
Obrigado pela sua sugestão, mas após várias tentativas de chamada, recebo erro: falha: Erro: ViewDestroyedError: Tentativa de usar uma exibição destruída: detectChanges Para mim, zone.run () me ajuda a sair desse erro.
shivam srivastava
8

Tente usar @Input() recentDetections: Array<RecentDetection>;

EDIT: A razão pela qual@Input()é importante é porque você deseja vincular o valor no arquivo typescript / javascript à visualização (html). A visualização será atualizada se um valor declarado com o@Input()decorador for alterado. Se um@Input()ou@Output()decorador for alterado, umngOnChanges-event será acionado e a visualização será atualizada com o novo valor. Você pode dizer que o@Input()bidirecional vinculará o valor.

procure por Input neste link por angular: glossário para mais informações

EDIT: depois de aprender mais sobre o desenvolvimento do Angular 2, percebi que fazer@Input()realmente não é a solução e, conforme mencionado nos comentários,

@Input() aplica-se apenas quando os dados são alterados pela ligação de dados de fora do componente (os dados vinculados no pai foram alterados) e não quando os dados são alterados do código dentro do componente.

Se você der uma olhada na resposta da Günter, é uma solução mais precisa e correta para o problema. Ainda vou manter essa resposta aqui, mas siga a resposta de Günter como a correta.

John
fonte
2
Foi isso. Eu já tinha visto a anotação @Input () antes, mas sempre a associava a entradas de formulário, nunca pensei que precisaria aqui. Felicidades.
28516 Matt Watson
1
@ John obrigado pela explicação adicional. Mas o que você adicionou somente se aplica quando os dados são alterados pela ligação de dados de fora do componente (os dados vinculados no pai foram alterados) e não quando os dados são alterados do código dentro do componente.
Günter Zöchbauer
Há uma ligação de propriedade anexada ao usar a @Input()palavra-chave, e essa ligação garante que o valor seja atualizado na exibição. o valor fará parte de um evento do ciclo de vida. Este post contém mais algumas informações sobre a @Input()palavra-chave
John
Eu vou desistir Não vejo onde um @Input()está envolvido aqui ou por que deveria estar e a pergunta não fornece informações suficientes. Obrigado por você paciência de qualquer maneira :)
Günter Zöchbauer
3
@Input()é mais uma solução alternativa que oculta a raiz do problema. Os problemas devem estar relacionados às zonas e à detecção de alterações.
magiccrafter
2

No meu caso, eu tive um problema muito semelhante. Eu estava atualizando minha visão dentro de uma função que estava sendo chamada por um componente pai, e no meu componente pai esqueci de usar @ViewChild (NameOfMyChieldComponent). Perdi pelo menos 3 horas apenas por esse erro estúpido. ou seja: eu não precisei usar nenhum desses métodos:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()
user3674783
fonte
1
você pode explicar como atualizou seu componente filho com o @ViewChild? obrigado
Lucas Colombo
Eu suspeito, pai foi chamar um método de uma classe criança que não foi proferida, e só existia na memória
glukki
1

Em vez de lidar com zonas e detecção de alterações - deixe o AsyncPipe lidar com a complexidade. Isso colocará assinatura observável, cancelamento de assinatura (para evitar vazamentos de memória) e detecção de alterações nos ombros angulares.

Mude sua classe para tornar um observável, que emitirá resultados de novas solicitações:

export class RecentDetectionComponent implements OnInit {

    recentDetections$: Observable<Array<RecentDetection>>;

    constructor(private recentDetectionService: RecentDetectionService) {
    }

    ngOnInit() {
        this.recentDetections$ = Observable.interval(5000)
            .exhaustMap(() => this.recentDetectionService.getJsonFromApi())
            .do(recent => console.log(recent[0].macAddress));
    }
}

E atualize sua visualização para usar o AsyncPipe:

<tr *ngFor="let detected of recentDetections$ | async">
    ...
</tr>

Quer acrescentar que é melhor fazer um serviço com um método que terá intervalargumentos e:

  • crie novas solicitações (usando exhaustMapcomo no código acima);
  • lidar com pedidos de erros;
  • impedir que o navegador faça novas solicitações enquanto estiver offline.
glukki
fonte