3
votes

I am writing an Angular 4 app. This contains 2 lists. If I click an element in the first list the result should be a subset of my second list (via foreign keys) in a new view. I do the filtering by id (foreign key) with a function in my service. In my component I receive just 'undefined'. I think, the reason is that I use an Observable in my service and the data is not ready, when the new view for the subset list is shown. How can I do this in another way to reach my goal?

calling the method in my service via click event in a mat-table-cell:

method in my service.ts

getItemsByID(id: number): Observable<Item[]> {
    return this.http.get('/api/Item').map((response) => {
      console.log(id); //shows correct item id
      console.log(this.items.filter(iteration => item.item_id === id)); // shows correct array of subset list
      return this.items.filter(item=> item.item_id === id);
    });
  }

method in my component.ts

getItem(): void { 
    const id = +this.route.snapshot.paramMap.get('id');
    this.itemService.getItemsByID(id)
        .subscribe(response => this.items = response);    
    console.log(this.items); // shows 'undefined'
}

What else do I need to show you from my code?

Complete log:

undefined

4

Array [ {…}, {…}, {…}, {…} ]

Thanks a lot!

3
Put the console.log() inside the callback passed to subscribe(). http is asynchronous. That's why it returns an Observable. It if blocked until the response is available, it wouldn't bother you with observables. It would return the response directly. You can't just eat a toast immediately after you've put it in the toaster. eat it when the toaster notifies you that the toast is ready. - JB Nizet
Please add details that help troubleshooting the problem. There are two Http services provided by Angular. Which module are you using? - André Werlang

3 Answers

2
votes

I infer you're using the HttpClient as http, given response as returned a list and .json() is not a function, as you wrote.

So, it's better if you remove the this.items property on your service. Perhaps you think that this property exist as this.items in both service and component? this points to separate objects. And would be best if service didn't had to maintain a state like this one, but only methods.

getItemsByID(id: number): Observable<Item[]> {
  return this.http.get<Item[]>('/api/Item').map((items) => {
    console.log(id); //shows correct item id
    const filtered = items.filter(item => item.item_id === id);
    console.log(filtered); // shows correct array of subset list
    return filtered;
  });
}

Then consume the service:

getItem(): void {
  const id = +this.route.snapshot.paramMap.get('id');
  this.itemService.getItemsByID(id)
    .subscribe(items => {
        this.items = items;
        console.log(items);
      );
    });
}
1
votes

I don't know why you're saving this response inside a service array variable, but you only need to return the mapped data from the service.

getItemsByID(id: number): Observable<Item[]> {
    return this.http.get('/api/Item')
      .map((response: Response) => {
      // return json object from http response
      let mapped = response.json();
      // given that mapped is an array of itens
      return mapped.filter(item => item.item_id === id);
    });
}

Also, your console is outside the subscribe method. It's async, you have to wait until the response ends.

getItem(): void { 
    const id = +this.route.snapshot.paramMap.get('id');
    this.itemService.getItemsByID(id)
        .subscribe((response) => { 
           this.items = response;
           console.log(this.items);
        });
}
0
votes

Observables, unless they have an initial value (BehaviorSubject, .startsWith()) or are synchronous, won't be ready just after a call to subscribe. The Http service is asynchronous, so you'll need to adapt your consumer. In time, I find it better to structure as if all observables are async.

Given your getItem() is void, one option is moving processing to the subscriber:

getItem(): void {
  const id = +this.route.snapshot.paramMap.get('id');
  this.itemService.getItemsByID(id)
    .subscribe(response => {
      this.items = response;
      // process it here, for instance calling ChangeDetectorRef#markForChanges() 
      // to make Angular update the component view
      // see https://stackoverflow.com/a/47463896/592792
    });
}

Another, pretty cool feature of Angular is using the async pipe:

getItem(): void {
  const id = +this.route.snapshot.paramMap.get('id');
  return this.itemService.getItemsByID(id);
}
Access properties:
{{ (getItem() | async).someProperty }} 

Pass to inner components: 
<cmp [item]="item | async"></cmp>

In this case we pass an Observable to view. async unwraps and passes values to wherever we need them. It's also possible to store the observable to a component property so it will be created and subscribed to just once.