Angular 2: la vista no se actualiza después de los cambios de modelo

128

Tengo un componente simple que llama a una API REST cada pocos segundos y recibe algunos datos JSON. Puedo ver por mis declaraciones de registro y el tráfico de red que los datos JSON que se devuelven están cambiando, y mi modelo se está actualizando, sin embargo, la vista no está cambiando.

Mi componente se ve así:

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

Y mi punto de vista se ve así:

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

Puedo ver de los resultados de console.log(this.recentDetections[0].macAddress) que el objeto RecentDetections se está actualizando, pero la tabla en la vista nunca cambia a menos que vuelva a cargar la página.

Estoy luchando por ver qué estoy haciendo mal aquí. ¿Alguien puede ayudar?

Matt Watson
fuente
Recomiendo hacer el código más limpio y menos complejo: stackoverflow.com/a/48873980/571030
glukki

Respuestas:

215

Puede ser que el código en su servicio de alguna manera salga de la zona de Angular. Esto rompe la detección de cambios. Esto debería 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 conocer otras formas de invocar la detección de cambios, consulte Cómo activar la detección de cambios manualmente en Angular

Las formas alternativas de invocar la detección de cambios son

ChangeDetectorRef.detectChanges()

ejecutar inmediatamente la detección de cambios para el componente actual y sus hijos

ChangeDetectorRef.markForCheck()

para incluir el componente actual la próxima vez que Angular ejecute la detección de cambios

ApplicationRef.tick()

ejecutar detección de cambios para toda la aplicación

Günter Zöchbauer
fuente
9
Otra alternativa es inyectar ChangeDetectorRef y llamar en cdRef.detectChanges()lugar de zone.run(). Esto podría ser más eficiente, ya que no ejecutará la detección de cambios en todo el árbol de componentes como lo zone.run()hace.
Mark Rajcok
1
De acuerdo. Solo use detectChanges()si los cambios que realiza son locales para el componente y sus descendientes. Mi entendimiento es que detectChanges()comprobará el componente y sus descendientes, pero zone.run()y ApplicationRef.tick()comprobará todo el árbol de componentes.
Mark Rajcok
2
exactamente lo que estaba buscando ... usar @Input()no tenía sentido ni funcionó.
yorch
15
Realmente no entiendo por qué necesitamos todas estas cosas manuales o por qué los dioses angular2 dejaron tan necesario. ¿Por qué @Input no significa que ese componente realmente depende realmente de lo que el componente padre dijo que dependía en sus datos cambiantes? Parece extremadamente poco elegante que no solo funcione. ¿Qué me estoy perdiendo?
13
Trabajó para mi. Pero este es otro ejemplo de por qué tengo la sensación de que los desarrolladores de marcos angulares se pierden en la complejidad del marco angular. Al usar angular como desarrollador de aplicaciones, debe dedicar mucho tiempo a comprender cómo angular hace esto o lo que hace y cómo solucionar cosas como las preguntas anteriores. ¡Terrible!
Karl
32

Originalmente es una respuesta en los comentarios de @Mark Rajcok, pero quiero colocarlo aquí como probado y funcionó como una solución usando ChangeDetectorRef , veo un buen punto aquí:

Otra alternativa es inyectar ChangeDetectorRefy llamar en cdRef.detectChanges()lugar de zone.run(). Esto podría ser más eficiente, ya que no ejecutará la detección de cambios en todo el árbol de componentes como lo zone.run()hace. - Mark Rajcok

Entonces el código debe 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 : el uso de .detectChanges()una subscibe interna podría generar problemas. Intento de usar una vista destruida: detectChanges

Para resolverlo, debe hacerlo unsubscribeantes de destruir el componente, por lo que el código completo será 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
fuente
this.cdRef.detectChanges (); arreglaron mi problema ¡Gracias!
mangesh
Gracias por su sugerencia, pero después de varios intentos de llamada recibo un error: error: Error: ViewDestroyedError: intento de usar una vista destruida: detectChanges For me zone.run () me ayuda a salir de este error.
Shivam Srivastava
8

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

EDITAR: La razón por la cual@Input()es importante es porque desea vincular el valor en el archivo de mecanografiado / javascript a la vista (html). La vista se actualizará sola si@Input()se cambiaun valor declarado con eldecorador. Si una@Input()o@Output()se cambia decorador, unngOnChanges-Evento se disparará, y la vista se actualizará con el nuevo valor. Puede decir que la@Input()voluntad de dos vías unirá el valor.

busque Entrada en este enlace por angular: glosario para más información

EDITAR: después de aprender más sobre el desarrollo de Angular 2, pensé que@Input()realmente no es la solución, y como se menciona en los comentarios,

@Input() solo se aplica cuando los datos se modifican mediante el enlace de datos desde fuera del componente (los datos vinculados en el elemento primario se modifican) no cuando los datos se modifican desde el código dentro del componente.

Si echas un vistazo a la respuesta de @ Günter, es una solución más precisa y correcta al problema. Todavía mantendré esta respuesta aquí, pero siga la respuesta de Günter como la correcta.

Juan
fuente
2
Eso fue todo. Había visto la anotación @Input () antes pero siempre la asociaba con entradas de formulario, nunca pensé que la necesitaría aquí. Salud.
Matt Watson el
1
@John gracias por la explicación adicional. Pero lo que agregó solo se aplica cuando los datos se cambian mediante el enlace de datos desde fuera del componente (los datos vinculados en el elemento primario cambiaron) no cuando los datos se cambian del código dentro del componente.
Günter Zöchbauer
Hay un enlace de propiedad adjunto cuando se usa la @Input()palabra clave, y ese enlace asegura que el valor se actualice en la vista. El valor será parte de un evento del ciclo de vida. Esta publicación contiene más información sobre la @Input()palabra clave
John
Voy a rendirme. No veo dónde @Input()está involucrado un aquí o por qué debería estar, y la pregunta no proporciona suficiente información. Gracias por tu paciencia de todos modos :)
Günter Zöchbauer
3
@Input()Es más una solución que oculta la raíz del problema. Los problemas tienen que estar relacionados con las zonas y la detección de cambios.
magiccrafter
2

En mi caso, tuve un problema muy similar. Estaba actualizando mi vista dentro de una función que estaba siendo llamada por un componente primario, y en mi componente primario olvidé usar @ViewChild (NameOfMyChieldComponent). Perdí al menos 3 horas solo por este estúpido error. es decir: no necesitaba usar ninguno de esos métodos:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()
usuario3674783
fuente
1
¿Puede explicar cómo actualizó su componente hijo con @ViewChild? gracias
Lucas Colombo
Sospecho, padre estaba llamando a un método de una clase hija que no se rindió, y sólo existía en la memoria
glukki
1

En lugar de ocuparse de zonas y detección de cambios, deje que AsyncPipe maneje la complejidad. Esto colocará la suscripción observable, la cancelación de la suscripción (para evitar pérdidas de memoria) y la detección de cambios en los hombros angulares.

Cambie su clase para hacer un observable, que emitirá resultados de nuevas solicitudes:

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

Y actualice su vista para usar AsyncPipe:

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

Quiere agregar que es mejor hacer un servicio con un método que tome interval argumentos y:

  • crear nuevas solicitudes (utilizando exhaustMap como en el código anterior);
  • manejar solicitudes de errores;
  • evitar que el navegador realice nuevas solicitudes mientras está desconectado.
glukki
fuente