5

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?

Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
Pyrodius
  • 111
  • 1
  • 6
  • Why is test decorated with 'Input()'? Is it only to set the initial value? Changes to Input() decorated members are only detected in a parent-child binding context. Also, are you certain that this is the only instance of the DisplayViewerConfigurationService? – Abraham Al-Dabbagh Jan 30 '18 at 06:13
  • Ahh, I was not aware that changes to Input() decorated members are only detected in the parent-child binding context. I pulled off Input() and it did not change the behavior. As for only one instance of the Service. The stack goes Module -> component -> component. The only time I declare the service in the constructor is on the last component. Which is the only place I subscribe to events from it. I will update my post to include the full stack if that would help. – Pyrodius Jan 30 '18 at 06:37
  • Thanks for posting your solution. I was having a similar problem - I wasn't aware that my Observable was running outside the Angular Zone. I spent hours trying to resolve this - not sure I would have found the answer without your solution, so thank you! – Clay Mar 15 '20 at 23:42

2 Answers2

6

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.

Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
Pyrodius
  • 111
  • 1
  • 6
0

Would like to add my newbie experience...

obs.subscribe(
        (data) => {
            console.log('this was sent to the DB : ' + data);
            this.myItem = data;
            this.itemOUT.emit(this.myItem);
            this.itemCLOSE.emit(true);

        });

I had the 2 emit statements outside my subscribe. The emits were executing and I was refreshing the parent component before the update happened.

This timing is a bit counter intuitive to me, but it happened (consistently). Just goes to prove that with async operations, you should make no assumptions.

Obvious in hindsight, but it took a fair amount of console.log()-ing before I realized my mistake.

hope it helps someone.

greg
  • 1,673
  • 1
  • 17
  • 30