Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get an Observable to return an array of transformed items (Rxjs)

I have an endpoint that produces a json list of products. I have a custom class type defined in my code for Products. I'm trying to get the data from the endpoint and have the json array of products transformed into an array of the Product class.

Sample API json (simplified from my actual data):

{
    "products": [{
        "id": 1,
        "name": "Product 1",
        "materials": [{
            "id": 100,
            "desc": "wood"
        }, {
            "id": 101,
            "desc": "metal"
        }]
    }, {
        "id": 2,
        "name": "Product 2",
        "materials": [{
            "id": 100,
            "desc": "wood"
        }, {
            "id": 102,
            "desc": "brick"
        }]
    }]
}

My code:

loadProducts(){
    this.fetchProducts().subscribe(data => {
            console.log("the data:", data);
        })
}


fetchProducts(): Observable<Product[]> {
    return this.http.get("http://example.com/mydata.json")
        .map((response) => {
            const products = (response.json()).products;
            console.log("Converting to json" + products.length);
            return products;
        })
        .map( data => {
            console.log("Working on data: ", data);
            return new Product(data.id, data.name, data.materials);
        });

What I would expect to see in my console is..

"Converting to json 2"
"Working on data: " [object]
"Working on data: " [object]
"the data:" [object,object]

.. but what I am seeing is..

"Converting to json 2"
"Working on data: " [object,object]
"the data:" [object,object]

I thought that the .map function would run for each item sent to it. I can see that when I first call .map that it is being run once on the one item (the response) that it has -- and I know that there are products 2 items being returned. I would expect the second map function to then be run twice - once for each product item. Instead it seems that it's called once being passed in the array of products.

To make matters more complicated I want to also convert the list of materials into a Material class type I've created. I know I could do all of this with forEach loops, but I want to do this the "React" way.

like image 406
captain_jim1 Avatar asked Jul 21 '17 20:07

captain_jim1


People also ask

How do you declare an Observable array?

Subscribing to an observable always return the value of the observable after it emits it. If you want to declare the variable as the observable itself, you simply assign it as the call: this. listeIsolements$ = this.

How do you map an array to an Observable?

To convert from array to observable you can use Rx. Observable. from(array) . To convert from observable to array, use obs.


2 Answers

In your case, you're getting something like this:

this.http.get(...)
  .map(res => res.json().products)  // turns this into Observable<Products[]>
  .map(products => ... ) // here, products is already an array.

If you want to process this array same as you would in React, meaning doing something for each product, you could do something like:

this.http.get(...)
  .map(res => res.json().products)
  .switchMap(products => Observable.from(products))

Here, Observable.from(products) returns Observable<Product> and switchMap makes the chain return Observable<Product> you got before.

this.http.get(...)
  .map(res => res.json().products)
  .switchMap(products => Observable.from(products))
  .subscribe(product => console.log(product))

Will print every one of those products.

Disclamer: I've not checked this code by running it, you may want a different operator then switchMap, but this is what I remember that works

like image 195
Giora Guttsait Avatar answered Oct 09 '22 06:10

Giora Guttsait


I finally found the right combination of Observable operations to yield what I was looking for.

fetchProducts(): Observable<Product[]> {
    return this.http.get("http://examples.com/myData")
        .map((response) => {
            return response.json().products;
        })
        .switchMap( productArray => {
            return Observable.from(productArray);
        })
        .map( (productData: any) => {
            return new Product(
                productData.id,
                productData.name,
                productData.materials
            );
        })
        .toArray();
}

I misunderstood how Observable.map works -- thinking that it would be run on each item in my data when it actually runs on each piece of data.. and I had one piece of data coming to it -- an array. Thanks to @jonrsharpe for helping me there.

By using switchMap to return a new Observable from my array I was able to then emit each piece of data in my array separately. Thanks @giora-guttsait for that help.

Finally, I needed to combine all of those new pieces of the stream back onto a single array. Observable.toArray() did this for me.

like image 22
captain_jim1 Avatar answered Oct 09 '22 05:10

captain_jim1