javascriptangulartypescriptobservableangular2-observables

Using Observables for streams in Angular and subscribing to them in another class what is the best way to approach delivery to the DOM/template


In my app component I am using the constructor to bring in a service. MyService As a reference this is a test service for data streaming from an IOT device. What I am wondering is whether using this method I have below or which is taking an Input data1 and setting that as an Array. Or, I am reading about Observables and streams and binding directly to them in the DOM/template with Ng-If-as.

The below dom with the ngFor is fine but when I utilize the ngIf as async pipe that fails with the error below.

Property 'i' does not exist on type 'AppComponent'.

 <ul>
    <ng-container *ngFor="let item of data1">
      <!-- <li *ngIf="item.isValid"></li> -->
      <li ngIf="_myService | async as i; else loading">
        {{ i }} 
      </li>
    </ng-container>
  </ul>
  <div #loading>Waiting...</div>

Below is what I have in regards to the appcomponent setup. I have app = construct myService => set to property => serve `<ng-container *ngFor="let item of data1"> That works but it is because I am setting the data to another property and having to setup that property.

 @Input() data1: Array<IContentObj> = [];

  constructor(public _myService: MyService) { }

ngOnInit() {
    this._myService.getServerSentEvent('http://localhost:8888/sse').subscribe((data: IContentObj) => {
      // console.log('DATA ', data);
      
      return this.data1.push({
        id: data.id,
        content: data.content,
        type: data.type,
        time_stamp: data.time_stamp,
        data: data.data
      });
      // // this.someObs = data;
      console.log('BLUE ', this.data1);
    });
  }

Here is from the angular docs as storing a conditional result in a variable

@Component({
  selector: 'ng-if-as',
  template: `
    <button (click)="nextUser()">Next User</button>
    <br>
    <div *ngIf="userObservable | async as user; else loading">
      Hello {{user.last}}, {{user.first}}!
    </div>
    <ng-template #loading let-user>Waiting... (user is {{user|json}})</ng-template>
`
})
export class NgIfAs {
  userObservable = new Subject<{first: string, last: string}>();
  first = ['John', 'Mike', 'Mary', 'Bob'];
  firstIndex = 0;
  last = ['Smith', 'Novotny', 'Angular'];
  lastIndex = 0;

The issue I see from my code above is that I am not directly referencing the variable. But it is async and it will be undefined until data streams in. So for me, I want access to my data from the subscription (that is constructed in from the service) to act as a stream in the ngifas per the Angular docs.

Storing a conditional result in a variable You might want to show a set of properties from the same object. If you are waiting for asynchronous data, the object can be undefined. In this case, you can use ngIf and store the result of the condition in a local variable as shown in the following example.

In summary, how do I get the proper Observable effectively as a async pipe ng-if-as on my DOM/template from a pulled in service?

What I was thinking is that I could setup an observable proper on the component I am worried about and stream the async data into that Observable and just serve that up to the dom. But something about that seems like code smell to me as what is the point of the service then?

There is also Ngrx Let Component but I am not even getting the above to work so not sure what the advantage of this is over the form.


Solution

  • how do I get the proper Observable effectively as a async pipe ng-if-as on my DOM/template from a pulled in service?

    You must apply the async pipe to an observable, not the entire service. You could do this:

    <li ngIf="_myService.getServerSentEvent(...) | async as i; else loading">
    

    But usually, you'd define an observable property and use that in the template:

    serverEvents$ = this._myService.getServerSentEvent(...);
    
    <li ngIf="serverEvents$ | async as i; else loading">
    

    I'm not 100% clear on what you are trying to achieve, but it looks like you receive some initial data from an @Input() and then you want to append new messages from the server as they are received.

    If that's that case you could do something like this:

    <ul>
      <li *ngFor="let event of initialEvents"> {{ event.id}} </li>
      <li *ngFor="serverEvents$ | async as event"> {{ event.id }} </li>
    </ul>
    

    One thing to note is that serverEvents$ needs to be an array, so we can use the scan operator to create an observable that collects the server emissions into an array and emits the entire array each time a new items is received:

    @Input() initialEvents: IContentObj[];
    
    serverEvents$: Observable<IContentObj[]> = this._myService.getServerSentEvent(...).pipe(
        scan((all, event) => all.concat(event), [] as IContentObj[])
    );
    

    I'd probably lean towards having a single observable array that includes both the initial items and the newly received ones:

    <ul>
      <li *ngFor="let event of events$ | async"> {{ event.id }} </li>
    </ul>
    

    To create a single observable, we can use startWith to provide the initial value (passed via the @Input()):

    @Input() initialEvents: IContentObj[];
    
    private serverEvent$: Observable<IContentObj> = this._myService.getServerSentEvent(...);
    
    public events$: Observable<IContentObj[]>;
    
    constructor(private_myService: MyService) { }
    
    ngOnInit() {
        events$ = this.serverEvents$.pipe(
            startWith(this.initialEvents),
            scan((all, event) => all.concat(event), [] as IContentObj[])
        );
    }
    

    All that said, it may end up being cleaner to pass an IContentObj[] via the @Input() and let the parent component be in charge of doing the work to build up that collection. Essentially, moving the definition of events$ into the parent and passing it with async pipe to the child:

    <child [events]="events$ | async"></child>