tomastrajan / angular-ngrx-material-starter

Angular, NgRx, Angular CLI & Angular Material Starter Project
https://tomastrajan.github.io/angular-ngrx-material-starter
MIT License
2.82k stars 918 forks source link

Interpolated properties are not updated in view/template after observable subscription #456

Closed felixunivers closed 5 years ago

felixunivers commented 5 years ago

Minimal reproduction of the bug with instructions:

I have a sample WebSocket component that works well outside the 'starter' project. If I bring the component to the 'starter' project, the properties/variables interpolated with the view/template are not updated as expected after the WebSocket observable is triggered on new data.

Here are the key parts of the code:

file: websocket.service.ts

` import { Injectable } from '@angular/core'; import { WebSocketSubject } from 'rxjs/webSocket';

@Injectable({
  providedIn: 'root'
})
export class WebsocketService {

  wss$: WebSocketSubject<any>;

  constructor() {}

  connect(url: string) {
    this.wss$ = new WebSocketSubject(url);
  }
}

`

file: websocket.component.ts ( not complete - relevant parts only)

    constructor(private wsService: WebsocketService) {}
    ....
    conStatus = '';
    dataInBlock = '';
    ....
    connect() {           // if user clicks connect button
      ...                                            
      this.wsService.connect(this.url);        // initialize the WebSocket service and it 'Subject / Observable'
      this.wss$ = this.wsService.wss$;         // pass the websocket Subject to local var. (for simpler code)
      this.wss_subscribe();                   // subscribe to websocket Subject / connect to server

                                             // check if the server will respond to our initial message
                                             // pass test msg/data to websocket to server
                                             // function: onSocketNewData() will be triggered and handle the response
      this.wss$.next('test message');
      ...
    }

    wss_subscribe() {
      this.wss$.subscribe(
        msg => this.onSocketNewData(msg),       // triggered whenever there is a message from the server.
        err => this.onSocketError(err),         // triggered if at any point WebSocket signals an error.
        () => this.onSocketComplete()         // triggered when connection is closed (for whatever reason).
      );
    }

    private onSocketNewData(wsData) {        // triggered whenever there is a message from the server.
      ....
      this.conStatus = 'Connected';
      ....
      this.dataInBlock = this.dataInBlock   // concat messages,  preceded by number -, separated by new line
                       + (this.counter++)
                       + ' - '
                       + wsData
                       + '\n';
        ....
    }
     ....

file: websocket.component.html

<div class="container">

  <div class="title-area">
    <span class="title">Angular WebSocket Test </span>
  </div>

  <div class="form-group">
      <label for="url">URL: </label>
      <input type="text"
             name="url"
             id="url"
             class="form-control data"
             [(ngModel)]="url"
             value={{url}}>
    <div class="btn-area">
      <span class="status">{{conStatus}}</span>
      <button (click)="disconnect()"class="btn btn-success">Disconnect</button>
      <button (click)="connect()"class="btn btn-success">Connect</button>
    </div>
    <div class="v-spacer"></div>
  </div>

  <form #webSocketForm="ngForm"
        (ngSubmit)="submit()">
    <div class="form-group">
      <label for="dataOut">Message/data out &rarr; to server: </label>
      <input type="text"
             name="dataOut"
             id="dataOut"
             class="form-control data"
             [(ngModel)]="dataOut"
             value={{dataOut}}>
    </div>
    <div class="btn-area">
      <span class="status">{{ wsds.outStatusText }}</span>
      <button type="submit" class="btn btn-success">Submit</button>
    </div>
  </form>

  <div class="v-spacer"></div>
  <div class="form-group">
    <label for="dataAreaA01">Messages/data in &larr; from server:</label>
    <textarea name="dataAreaA01"
              id="dataAreaA01"
              class="dataArea data"
              [ngModel]="dataInBlock">
    </textarea>
  </div>
  <div class="btn-area">
    <span class="status">{{ wsds.inStatusText }}</span>
    <button (click)="clear()"class="btn btn-success">Clear</button>
  </div>
  <div class="v-spacer"></div>
</div>

Expected behavior:

It is expected that after the function: onSocketNewData(msg) gets triggered by each server response the updated properties/variables (such as: conStatus, dataInBlock, etc. ) associated/interpolated with the view get updated/rendered. This works well if the component is is in its own project, but not when it becomes part of the 'starter' project.

Other information:

I would be willing to submit a PR to fix this issue:

[ ] Yes (Assistance is provided if you need help submitting a pull request)
[ ] No

timdeschryver commented 5 years ago

If you would console.log out the incoming changes, do you see them? Or is it only the view that is not updating?

A cause of this could be the change detection, we use OnPush because we're NgRx.

changeDetection: ChangeDetectionStrategy.OnPush,
tomastrajan commented 5 years ago

@felixunivers as @timdeschryver said, setting component properties after async processing will not refresh the component if the component is using ChangeDetectionStrategy.OnPush, for such a component it would make sense to remove this and use default change detection strategy.

Another solution would be to inject change detector reference and call .markForChangeDetection() every time data arrives.

felixunivers commented 5 years ago

Would you please provide a piece of code sample (something that may fit the above scenario) on how to properly use the: .markForChangeDetection()

Here is what I did and it appears working - please comment if any issues with this solution.

    import { ChangeDetectorRef } from '@angular/core';
    ...
    constructor(private cdRef: ChangeDetectorRef, ..... 
    ...

    private onSocketNewData(wsData) {        // triggered whenever there is a message from the server.
      this.cdRef.markForCheck();
      this.conStatus = 'Connected';
      ....