dafna
dafna

Reputation: 973

Angular - Observable data not ready?

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!

Upvotes: 3

Views: 1571

Answers (3)

Andr&#233; Werlang
Andr&#233; Werlang

Reputation: 5944

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);
      );
    });
}

Upvotes: 2

Fals
Fals

Reputation: 6839

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);
        });
}

Upvotes: 1

Andr&#233; Werlang
Andr&#233; Werlang

Reputation: 5944

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.

Upvotes: 0

Related Questions