0
votes

Getting back to learning Angular and extending the Tour of Heroes tutorial. I have a shared data service that loads data into a BehaviorSubject. The issue is that when I try to iterate the data with *ngFor, I get the "cannot find a differ supporting object" error. From all the other questions, I get that it is trying to bind to an object and not an Array, but for the life of me I cannot figure out why. Or what object needs to get converted into an Array.

I am using Angular 5.0.5. Interestingly enough, this was working with Angular 4 but apparently I have broken something in the upgrade.

Any thoughts on what I did wrong? Besides everything. :D lol

This is my service

import { Injectable, EventEmitter } from '@angular/core';
import { HttpHeaders, HttpClient } from '@angular/common/http';

import { Subject } from 'rxjs/Subject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';

import { Hero } from '../models/hero';

@Injectable()
export class HeroService {

    private heroesUrl = 'api/heroes';
    private headers = new HttpHeaders({'Content-Type': 'application/json'});

    private loadingSubject = new BehaviorSubject<boolean>(false);
    private dataSubject = new BehaviorSubject<Hero[]>([]);

    private http: HttpClient;

    constructor(http: HttpClient) {
        this.http = http;
    }

    public getLoadingStream(): Observable<boolean> {
        return this.loadingSubject.asObservable();
    }
    public getDataStream(): Observable<Hero[]> {
        return this.dataSubject.asObservable();
    }

    public load(): void {
        this.loadingSubject.next(true);
        this.http.get<Hero[]>(this.heroesUrl).subscribe(data => {
            this.dataSubject.next(data);
            this.loadingSubject.next(false);
        });
    }

    public search(term: string): void {
        this.loadingSubject.next(true);
        this.http
            .get<Hero[]>(`${this.heroesUrl}/?name=${term}`)
            .subscribe(data => {
                this.dataSubject.next(data);
                this.loadingSubject.next(false);
        });
    }
}

List Component

import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';

import { HeroService } from '../../services/hero.service';
import { Hero } from '../../models/hero';

@Component({
  selector: 'hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {

  private dataSubscription: Subscription;
  private loadingSubscription: Subscription;

  private heroService: HeroService;

  heroes: Hero[] = [];
  isLoading: boolean;

  @Output()
  public onHeroSelected = new EventEmitter<Hero>();

  constructor(heroService: HeroService) {
    this.heroService = heroService;
  }

  ngOnInit() {
    this.loadingSubscription = this.heroService.getLoadingStream()
        .subscribe(loading => {
            this.isLoading = loading;
        });

    this.dataSubscription = this.heroService.getDataStream()
        .subscribe(data => {
            this.heroes = data;
        });
  }

  ngOnDestory() {
    this.dataSubscription.unsubscribe();
    this.loadingSubscription.unsubscribe();
  }

  onSelect(hero: Hero) {
    this.onHeroSelected.emit(hero);
  }
}

List template

<div *ngIf="isLoading">Loading ...</div>

<div *ngIf="!isLoading" class="ui relaxed divided list">
  <div *ngFor="let hero of heroes"  class="item" (click)="onSelect(hero)">
    <span class="ui blue circular label">{{ hero.id }}</span>
    <div class="content">
      <a class="header">{{ hero.name }}</a>
      <div class="description">...</div> 
    </div>
  </div>
</div>  
2
looking really fast, I think it's because your service is async, so when your data isLoading is false it will try to display and iterate over your array. But in that moment, your array isn't set because it's async. try to update from *ngIf=!isLoading to *ngIf=!isLoading && heroes > 0 - sheplu
Replace this.dataSubject.next(data); by console.log(data); this.dataSubject.next(data);. What is being logged to the console? Is it really an array? - JB Nizet
@JBNizet yes, the data is an Array with 10 elements. - Joe Young
Prove it. Post the result that is logged in the console. If it's easier to copy and paste, use console.log(JSON.stringify(data)). - JB Nizet
@JBNizet {"data":[{"id":11,"name":"Batman"},{"id":12,"name":"Superman"},{"id":13,"name":"Spiderman"},{"id":14,"name":"Thor"},{"id":15,"name":"Wolverine"},{"id":16,"name":"Wonder Woman"},{"id":17,"name":"Captain America"},{"id":18,"name":"Iron Man"},{"id":19,"name":"The Hulk"},{"id":20,"name":"Duke"}]} - Joe Young

2 Answers

0
votes

Rather than defining a function that returns an observable, shouldn't you be defining a property that is in fact an observable? For example:

Service

public heroDataStream: Observable<Hero[]> = this.dataSubject.asObservable();

Then you would subscribe directly to heroDataStream. Thats typically how I go about converting BehaviorSubjects into observables rather than wrapping them in a new function.

You might be running into a cold vs hot issue with the Observable not being created until the subscription is called since its definition is within the function. In other words its cold. You want its declaration to take place right away, meaning it needs to be hot and ready to go. So defining it as a property within your service declares it with the service. I could be wrong, this is definitely scratching the boundary of my depth of knowledge when it comes to RxJs and how everything gets spooled up.

0
votes

Ugh, I hate answering my own questions, but this was a big "d'oh".

The root cause of the issue was that I was using a the In-Memory-Web-API and did not realize that it was encapsulating the response in a data object.

So the error message was correct, it was trying to iterate over an object {data: [] } instead of an array.

Upgrading the In-Memory-Web-API and putting false in the data encapsulation

    HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {dataEncapsulation: false})

resolved the issue.

Huge thanks to JB Nizet for pointing me in the right direction!