Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 5 Component Not Updating When Observable Returns

I am building an application using Angular 5 and SignalR. I created a service which manages the hub, and when ever an event comes in from the Server it puts the value in the BehaviorSubject private variable. There is a read only property that should put out the observable which I can subscribe to from the component.

In the component, I have subscribed to the Observable of the service, but it still does not update when the property updates.

I know the "test" property is being updated because of the alerts that I have to tell me I received the config from the server.

Would someone be able to point out what I may be missing or do not fully understand about how Angular handles change detection or how I may have broken that?

Service:

import { Injectable, Input, Output, EventEmitter, NgZone } from '@angular/core';
import { Http, Response } from '@angular/http';
import 'rxjs/add/operator/map';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import { APIModels } from './../../shared/APIModels';
import {Observable} from "rxjs/Observable";

declare var $;
@Injectable()
export class DisplayViewerConfigurationService {
    private _displayConfiguration: BehaviorSubject<APIModels.ClientConfigurationModel>;
    public readonly displayConfiguraiton: Observable<APIModels.ClientConfigurationModel>;
    public hubProxy;
    @Input() displayID;

    constructor(public http: Http) {
        var test = new APIModels.ClientConfigurationModel();
        test.Display = new APIModels.DisplayConfig();
        test.Display.DisplayID = "testID";
        this._displayConfiguration = new BehaviorSubject(test);
        this.displayConfiguraiton = this._displayConfiguration.asObservable();
    }

    public startConnection(): void {
        var hubConnection = $.hubConnection("http://localhost:49890", {useDefaultCredentials: true});
        this.hubProxy = hubConnection.createHubProxy('testHub');
        hubConnection.qs = "displayid=" + this.displayID;
        this.hubProxy.on('receiveConfig', (e: any) => {
            this._displayConfiguration.next(e);
            alert("now" + JSON.stringify(e));
        });
        
        hubConnection.start();
    }
}

Component TS:

import { Component, OnInit, Output, Input, ChangeDetectorRef } from '@angular/core';
import { DisplayViewerConfigurationService } from './../../services/displayviewerconfiguration-service/displayviewerconfiguration-service';
import 'rxjs/symbol/observable';
import { Observable } from 'rxjs/observable';
import { APIModels } from './../../shared/APIModels';
import { Helpers } from './../../shared/Helpers';

@Component({
  selector: 'app-displayiframe',
  templateUrl: './display-iframe.component.html',
  
})

export class DisplayScreenComponent implements OnInit {
  
    constructor(public viewer: DisplayViewerConfigurationService) {
        this.viewer.displayID = "278307b8-da34-4569-8b93-d5212b9e0e0d";
        this.viewer.startConnection();
        this.ObjCopy = new Helpers.ObjectCopyHelpers();
      
    }

    ngOnInit() {
        this.viewer.displayConfiguraiton.subscribe(data => {this.test = data; alert(JSON.stringify(this.test);});
    }

    @Input() test: any;
    public stackHeight: string;
    public ObjCopy: Helpers.ObjectCopyHelpers;
   
}

Component Template:

<div>
   {{test.Display.DisplayID}}
</div>

I have also tried subscribing directly to the observable with this. Still didn't work.

<div>
   {{viewer.displayConfiguraiton.Display.DisplayID | async}}
</div>

EDIT:

Including the Module, and parent component for this stack to help provide further clarification.

MODULE:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DisplayViewerConfigurationService } from './../services/displayviewerconfiguration-service/displayviewerconfiguration-service';
import { DisplayViewerComponent } from './display-viewer.component';
import { DisplayScreenComponent } from './display-iframe/display-iframe.component';
import { HttpModule } from '@angular/http';
import { APIModels } from "./../shared/APIModels";
import { Helpers } from "./../shared/Helpers";

@NgModule({
  imports: [
    CommonModule
    HttpModule,
  ],
  declarations: [DisplayViewerComponent, DisplayScreenComponent],
  
  entryComponents: [DisplayViewerComponent, DisplayScreenComponent],
  
  providers: [DisplayViewerConfigurationService],
  exports: [DisplayViewerComponent],
})
export class DisplayViewerModule { }

Parent Component:

import { Component, OnInit } from '@angular/core';
import { NouisliderModule, NouisliderComponent } from 'ng2-nouislider/src/nouislider';
import { DisplayViewerConfigurationService } from './../services/displayviewerconfiguration-service/displayviewerconfiguration-service';
import { DisplayScreenComponent } from './display-iframe/display-iframe.component';
import 'rxjs/symbol/observable';
import { Observable } from 'rxjs/observable';

@Component({
  selector: 'app-displayviewer',
  templateUrl: './display-viewer.component.html'
  
})

export class DisplayViewerComponent implements OnInit {
  constructor() {
}

  ngOnInit() {

  }

}

Parent Component Template:

<app-displayiframe></app-displayiframe>

EDIT:

I have been doing a bit of research and decided to give this example a try: Pushing Real-Time Data to an Angular Service using Web Sockets.

The same behavior persisted after doing this, with no change detection being triggered.

After doing a bit more research and finding this page: Angular 2 - View not updating after model changes.

I decided to attempted to run ChangeDetectorRef.detectChanges(); after I update the property of the component. Doing this actually fixed my problem and caused the component to update as I expected it to.

Example:

this.viewer.startConnection()
    .subscribe(config => {
        this.test.push(config);
        alert(JSON.stringify(this.test));
        this.changeDetector.detectChanges();
        alert(NgZone.isInAngularZone());
    });

I also added an alert to tell me if the code I was running was within the AngularZone. As it turns out, the observable I am returning doesn't seem to be within the Angular Zone. No wonder I was not getting any change detection when changing the property.

Why would creating the the observable this way cause the subscription to be outside the AngularZone? Is there a way I can fix this to be within the bounds of the zone and not have to rely on manually forcing the application to detect changes?

like image 311
Pyrodius Avatar asked Jan 30 '18 00:01

Pyrodius


1 Answers

So, as it turns out I was working with a library that is out of the NgZone. I was able to determine if the calls were within the zone by using NgZone.isInAngularZone(); and placing alerts within the subscription callbacks. Once I figured out I was out of the zone, I figured out I could inject ChangeDetectorRef into the component and place a manual change detection call within the subscription. Example: this.changeDetector.detectChanges();

This was just a band-aid for the root problem. After a bit more research, I found this post. This pointed out that you can force a call to be run within the zone. So I replaced the hub proxy call back with this.

this.hubProxy.on('receiveConfig', (e: any) => {
    this.ngZone.run(() => this.observer.next(e));
});

This forced the observable to run within NgZone allowing the default change detection tree to detect the changes.

like image 195
Pyrodius Avatar answered Oct 13 '22 09:10

Pyrodius