0

I'm new to Angular 6 and I'm trying to iterate over an array of objects but it's producing nothing. People online are talking about subscribing to the observable and I can't figure out how to do that if it's the problem.

This is my component:

import { Component, OnInit } from '@angular/core';
import { Topic } from '../topic';
import { TopicFetcherService } from '../topic-fetcher.service'

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

  constructor( private topicFetcher: TopicFetcherService) { }

  topics: Topic[];

  ngOnInit() { 

    // this grabs my topics from my http service
    this.processTopics(this.topicFetcher.getData()); 

  }

  processTopics(rawTopics: Topic[]) {

    console.log(rawTopics); // this works 

    rawTopics.forEach(topic => {
      console.log(topic.id); // this does not work ? 
    });

  }

}

I don't understand why console.log(rawTopics) works but if you try to iterate over it, you have to get involved with observables. That seems a little heavy handed.

EDIT:

This is my topic-fetcher service. It currently just .get's a .json file in assets:

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

import { Topic } from './topic';

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

  constructor(private http: HttpClient) { }

  topics: Topic[];

  getData(): Topic[] {

    this.topics = [];
    let index = 1;

    this.http.get('assets/topics.json')
    .subscribe(data => {     
      for (const key in data) {
        let topic = new Topic();
        topic.id = index;
        topic.title = data[key].topic;
        topic.description = data[key].narrative;
        topic.type = data[key].type;
        topic.dataset = data[key].dataset;
        this.topics.push(topic);
        index++;
      }
     });

     return this.topics;

  }

}
Felix Lemke
  • 6,189
  • 3
  • 40
  • 67
aadu
  • 3,196
  • 9
  • 39
  • 62
  • What does the working console log output? – Phix Oct 19 '18 at 16:47
  • It outputs an array in the console, which you can click on and then you see all the individual Topic objects and their properties. – aadu Oct 19 '18 at 16:50
  • Possible duplicate of [How do I return the response from an Observable/http/async call in angular2?](https://stackoverflow.com/questions/43055706/how-do-i-return-the-response-from-an-observable-http-async-call-in-angular2) – user184994 Oct 19 '18 at 16:52
  • But I'm able to return the response no problem? I can even log the array of objects to the console and see everything perfectly. I just can't iterate over it? – aadu Oct 19 '18 at 16:54
  • @AzzyDude That's because the code is asynchronous. The items are not being added to the array until after you try to iterate – user184994 Oct 19 '18 at 17:00
  • I think I understand, but why can I log the entire array to the console if the items haven't been added to it yet? – aadu Oct 19 '18 at 17:02
  • 2
    Try logging the length of the array, you should see it's zero. When you log the object to the array, the console actually holds a reference to the object, so will update when you expand, as per https://stackoverflow.com/questions/17320181/console-log-showing-only-the-updated-version-of-the-object-printed – user184994 Oct 19 '18 at 17:03

1 Answers1

1

If getData() returns an observable of type Observable<Topic[]>, you need to subscribe (and unsubscribe) to it. At first import Observable

import { Observable } from 'rxjs'

and in your component class

ngOnInit() {
  this.processTopics(this.topicFetcher.getData()); 
}

processTopics(rawTopics$: Ovservable<Topic[]>) {
  rawTopics$.subscribe((rawTopics) => {
    console.log(rawTopics); // this works 

    rawTopics.forEach(topic => {
      console.log(topic.id); // this does not work ? 
    });
  });
}

It is a good practice to mark observables by $ at the end, but that is primarly opinion based.

Unsubscribe

To unsubscribe it makes sense to introduce the OnDestroy component livecycle hook and unsubscribe in there.

import { OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

Add a member to your component class, e.g. named subscription: Subscription. Then assign the subscription in processTopics to that variable.

this.subscription = rawTopics$.subscribe(...)

Tell the component class to implement the ondestroy livecycle hook.

export class TopicsComponent implements OnInit, OnDestroy {}

And finally create the hook function

ngOnDestory() {
  if (this.subscription) {
    this.subscription.unsubscribe();
  }
}

EDIT

Change your service to the following snippet (of course with all your object properties and not ...). Do not subscribe in the service itself, but return the http.get as an observable and subscribe in your components.

getData(): Observable<Topic[]> {
  return this.http.get<Topic[]>('assets/topics.json').pipe(
    map(data => {
      data.map((entry, index) => {
        const topic = {
          id: index,
          title: entry.topic
          ...
        }
        return topic;
      });
    })
  );
}

Also http.get requests have the advantage that they also complete when the observable receives a value, means, you don't have to unsubscribe to them. But it really can curse your application with performance issues when you have got too many unwanted subscriptions.

Felix Lemke
  • 6,189
  • 3
  • 40
  • 67
  • If I switch Topic[] to Observable I get a really weird error... "Argument of type 'Topic[]' is not assignable to parameter of type 'Observable'. Property '_isScalar' is missing in type 'Topic[]'" – aadu Oct 19 '18 at 16:57
  • I highly recommend to rework your `getData` method as it is in my edit. You should return the http.get directly and don't subscribe in the service itself. – Felix Lemke Oct 19 '18 at 17:02