Filter JSON in Angular

Viewed 63

I have the below JSON file which is part of my Angular project

[
  {
    "name": "Product A",
    "slug": "product-a",
    "short_description": "some description"
  },
  {
    "name": "Product B",
    "slug": "product-b",
    "short_description": "some description 2"
  }
]

In my service, I am doing a get request to get the above json

getProducts() {
  return this.http.get('../../../assets/products.json');
}

Now in my products listing page, I am doing a request to get the list of products and looping over them to render them on page. This is working fine. Below is what I'm doing:

products = <any>[];

ngOnInit(): void {
    this.products = this.productsService.getProducts();
}

My issue is that once the user clicks on one of the products, I'm redirecting him to the product details page and I need to load the product details. The way I was tackling it was to try and do another request to get the products and trying to filter them to load the needed product. Below is what I was trying, which is returning and array with the needed object. However I just need the object to be able to use it in the template.

this.route.params.subscribe((params) => {
  const productSlug = params['product-name'];

  this.productService
  .getProducts()
  .subscribe((data: any) => {
    this.product = data.filter((item: { slug: any; }) => item.slug === productSlug);
  })

Can someone tell me how I can get the object directly, rather than an object inside an array? I am new to Angular and for sure there should be a more elegant solution to do this rather than doing the same request twice and then filter. So any suggestions on how to improve this would be more than welcome.

3 Answers

Maybe I'm missing some information here, but I'm not sure the purpose of a hardcoded JSON file. You can simply put this array into a TS file and export it. You can index the information by slug for easy lookup.

products.ts

export const productsArray = [
  {
    name: 'Product A',
    slug: 'product-a',
    short_description: 'some description',
  },
  {
    name: 'Product B',
    slug: 'product-b',
    short_description: 'some description 2',
  },
];

export const productsMap = new Map<string, Object>();
for (const p of productsArray) productsMap.set(p.slug, p);

Then you simply import the data where you need it

app.component.ts

import { productsMap } from './products';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  ngOnInit() {
    console.log(productsMap.get('product-a'));
  }
}

As of now, anyone using your app needs to parse the JSON file and turn it into a JS object. If it's a hardcoded asset this is unnecessary work for the client and unnecessary complication for the developer.


If this JSON is actually being requested through an external API, then what you're doing makes sense. It sounds like you just want to request the data once and store it. In this case you'll need some kind of mechanism to let you know whether the initial loading is finished. There's many ways to do that, I'll just use a BehaviorSubject that emits a boolean value.

In this example I create a service that makes the request when it is initialized, stores the result, creates the indexed map, then notifies subscribers that the data is loaded.

product.service.ts

export class ProductService {
  private url = './assets/products.json'; // should be an external url

  loaded = new BehaviorSubject(false);
  productsArray = [];
  productsMap = new Map<string, Object>();

  constructor(private http: HttpClient) {
    http.get<any[]>(this.url).subscribe((res) => {
      this.productsArray = res;
      for (const p of res) this.productsMap.set(p.slug, p);
      this.loaded.next(true);
    });
  }
}

Services are singleton, so this request will only be made once when the service is first injected.

In the products listing page you can simply point to the array within the service. Change detection will handle the rest.

listing.component.ts

export class ListingComponent {
  constructor(private productService: ProductService) {}

  get products() {
    return this.productService.productsArray;
  }
}

In the details component you can inject the service and subscribe to the loaded observable in order to perform a search only after the data is loaded. You can also notify the user if the data is still loading.

details.component.ts

export class DetailsComponent implements OnInit {
  constructor(private productService: ProductService) {}

  ngOnInit() {
    const sub = this.productService.loaded.subscribe((loaded) => {
      if (!loaded) return console.log('loading...');
      console.log(this.productService.productsMap.get('product-a'));
      sub.unsubscribe();
    });
  }
}

Note: you can potentially pre-load the products by injecting the service into the root component.

app.component.ts

export class AppComponent {
  constructor(private productService: ProductService) {}
}

This will trigger the request when the app first loads.

The product service should store the retrieved products. Look up Angular RxJS observable services - generally they will be set up with a BehaviorSubject. The service can have an additional method to return a product by id (or slug in your case)

An Angular resolver would be useful here too, to make sure all the products are fetched before the page loads - that's a common approach

Update your service to store the list of products so you would be able to run normal array functions on them.

Your service should be something like this:

import {Observalbe, BehaviorSubject, Of} from 'rxjs';
import {pipe, tap} from 'rxjs/operators';

@Injectable()
class ProductService {
 private _products = [];
 private product$ = new BehaviorSubject(_products);

 getProducts(): Observable(IProduct[]) {
  // here you can check if the `_products` already exist,
  // so return this.product$.asObservable
  // Or you can use `switchMap` to return the `protuct$`
  // We used tap, to store products every time we fetch it.
  return this.http.get('../../../assets/products.json')
              .pipe(tap(products => this._products = products));
 }

 getProduct(id: string): Observalbe(IProduct) {
  const product = this._products.find(product => product.id === id);
  return of(product);
 }
}
Related