121

I am trying to understand how to use Observables in Angular 2. I have this service:

import {Injectable, EventEmitter, ViewChild} from '@angular/core';
import {Observable} from "rxjs/Observable";
import {Subject} from "rxjs/Subject";
import {BehaviorSubject} from "rxjs/Rx";
import {Availabilities} from './availabilities-interface'

@Injectable()
export class AppointmentChoiceStore {
    public _appointmentChoices: BehaviorSubject<Availabilities> = new BehaviorSubject<Availabilities>({"availabilities": [''], "length": 0})

    constructor() {}

    getAppointments() {
        return this.asObservable(this._appointmentChoices)
    }
    asObservable(subject: Subject<any>) {
        return new Observable(fn => subject.subscribe(fn));
    }
}

This BehaviorSubject is pushed new values as so from another service:

that._appointmentChoiceStore._appointmentChoices.next(parseObject)

I subscribe to it in the form of an observable in the component I want to display it in:

import {Component, OnInit, AfterViewInit} from '@angular/core'
import {AppointmentChoiceStore} from '../shared/appointment-choice-service'
import {Observable} from 'rxjs/Observable'
import {Subject} from 'rxjs/Subject'
import {BehaviorSubject} from "rxjs/Rx";
import {Availabilities} from '../shared/availabilities-interface'


declare const moment: any

@Component({
    selector: 'my-appointment-choice',
    template: require('./appointmentchoice-template.html'),
    styles: [require('./appointmentchoice-style.css')],
    pipes: [CustomPipe]
})

export class AppointmentChoiceComponent implements OnInit, AfterViewInit {
    private _nextFourAppointments: Observable<string[]>
    
    constructor(private _appointmentChoiceStore: AppointmentChoiceStore) {
        this._appointmentChoiceStore.getAppointments().subscribe(function(value) {
            this._nextFourAppointments = value
        })
    }
}

And the attempt to display in the view as so:

  <li *ngFor="#appointment of _nextFourAppointments.availabilities | async">
         <div class="text-left appointment-flex">{{appointment | date: 'EEE' | uppercase}}

However, availabilities isn't yet a property of the observable object so it errors out, even though I define it in the availabilities interface as so:

export interface Availabilities {
  "availabilities": string[],
  "length": number
}

How can I display an array asynchronously from an observable object with the async pipe and *ngFor? The error message I get is:

browser_adapter.js:77 ORIGINAL EXCEPTION: TypeError: Cannot read property 'availabilties' of undefined
BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
C. Kearns
  • 1,531
  • 3
  • 13
  • 18
  • What's the actual error message? – Günter Zöchbauer Jun 07 '16 at 03:11
  • edited to add error – C. Kearns Jun 07 '16 at 03:21
  • with the latest angular-rc1 the syntax is `*ngFor="let appointment of _nextFourAppointments.availabilities | async">` – Jagannath Jun 07 '16 at 03:39
  • this is true, but it is not causing the error. it simply throws a warning. – C. Kearns Jun 07 '16 at 03:40
  • 3
    I believe there is a typo somewhere. The error says `availabilties` while there should be `availabilities` – Ivan Sivak Jun 07 '16 at 03:46
  • fixed spelling error, but availabilities isn't the problem, the error is that the observable is undefined at the time the template is parsed, and despite the async pipe it is still trying to find the key I believe – C. Kearns Jun 07 '16 at 05:12
  • In `AppointmentChoiceStore` I think you can just return the Subject instead of putting it into a new Observable. `BehaviorSubject` extends from `Observable`. In `AppointmentChoiceComponent` you subscribe to the `AppointmentChoiceStore` observable. That should return you a `Availabilities` object (and not a `string[] observable`) that you put into `_nextFourAppointments`. So change `_nextFourAppointments: Observable` to `_nextFourAppointments: Availabilities` and update the template accordingly. @Ivan: that doesn't explain why the error is about reading property 'X' from undefined. – Sjoerd Jun 07 '16 at 05:14
  • that makes sense @Sjoerd. If I keep the template with `_nextFourAppointments.availabilities` with the changes you made, I still get the error. If I change the subscribe function to set `_nextFourAppointments = value.availabilities` and change the template to just iterate over `_nextFourAppointments`, I can confirm that it is being set to the array I want, but it is not populating the template, I believe because the `async` pipe is expecting an `Observable` and `_nextFourAppointments` is now `_nextFourAppointments: Availabilities` – C. Kearns Jun 07 '16 at 05:25
  • You are trying to read the `availabilities` key of `_nextFourAppointments` in the template, but (and this is what the error message is trying to tell you) `_nextFourAppointments` is undefined. That can happen when Angular is instantiating the component. I am actually not sure if it would work here, but you could try the Elvis operator: `_nextFourAppointments?.availabilities`. The Elvis operator protects against null values. – Sjoerd Jun 07 '16 at 05:58
  • @Sjoerd, the elvis operator does in fact prevent the error, and I am definitely setting _`nextFourAppointments` to the array in my subscription, but the template still never gets populated, with or without the `async pipe`. I believe the only way to use the async pipe and `*ngFor` is with an Observable Array and I am still not sure how to grab the Array from the observable object before I have the observable object. Will have to sleep on it. – C. Kearns Jun 07 '16 at 06:16
  • What about removing the aync pipe, because you are not longer looking at an Observable. By the way, it might also be helpful if you could provide a working plunker example. That way we can see more clearly what the problem is and how to fix it. – Sjoerd Jun 07 '16 at 06:27
  • I tried to remove the async pipe but it still did not populate. I will edit with a plunk in the morning. Wish I found rxjs less intimidating , would make working with angular 2 easier. – C. Kearns Jun 07 '16 at 06:30

4 Answers4

176

Here's an example

// in the service
getVehicles(){
    return Observable.interval(2200).map(i=> [{name: 'car 1'},{name: 'car 2'}])
}

// in the controller
vehicles: Observable<Array<any>>
ngOnInit() {
    this.vehicles = this._vehicleService.getVehicles();
}

// in template
<div *ngFor='let vehicle of vehicles | async'>
    {{vehicle.name}}
</div>
Relu Mesaros
  • 4,952
  • 4
  • 25
  • 33
  • my get function is returning a subject though: `public _appointmentChoices: Subject = new Subject() getAppointments() { return this._appointmentChoices.map(object=>object.availabilities).subscribe() } `, in the controller when I set it equal, I get the error: `browser_adapter.js:77Error: Invalid argument '[object Object]' for pipe 'AsyncPipe'`, how do I turn the subject into an observable? – C. Kearns Jun 07 '16 at 16:27
  • `public _appointmentChoices: Subject = new Subject() getAppointments() { return (this._appointmentChoices.map(object=>object.availabilities).asObservable()) } }` this gives me the error: `property asObservable does not exist on type observable`, but _appointmentChoices is a `Subject`? – C. Kearns Jun 07 '16 at 16:56
  • It already was an observable! I just needed to subscribe to it! – C. Kearns Jun 07 '16 at 16:58
  • I had an additional problem with integrating subjects. Here's a StackBlitz using observables and subjects: https://stackblitz.com/edit/subject-as-observable-list-example – IceWarrior353 Jul 23 '18 at 20:12
25

Who ever also stumbles over this post.

I belive is the correct way:

  <div *ngFor="let appointment of (_nextFourAppointments | async).availabilities;"> 
    <div>{{ appointment }}</div>
  </div>
Martin Søberg
  • 491
  • 5
  • 8
5

I think what u r looking for is this

<article *ngFor="let news of (news$ | async)?.articles">
<h4 class="head">{{news.title}}</h4>
<div class="desc"> {{news.description}}</div>
<footer>
    {{news.author}}
</footer>

Nixon Mathew
  • 131
  • 2
  • 3
2

If you don't have an array but you are trying to use your observable like an array even though it's a stream of objects, this won't work natively. I show how to fix this below assuming you only care about adding objects to the observable, not deleting them.

If you are trying to use an observable whose source is of type BehaviorSubject, change it to ReplaySubject then in your component subscribe to it like this:

Component

this.messages$ = this.chatService.messages$.pipe(scan((acc, val) => [...acc, val], []));

Html

<div class="message-list" *ngFor="let item of messages$ | async">
Post Impatica
  • 14,999
  • 9
  • 67
  • 78