The newline Guide to Angular Universal - Part 2

Building the Products Service

It’s time to enable displaying products in your application. In this step, you are going to use product images that are available for download here. Place the images inside the src/assets/images folder in your project.

To generate the component and the service that you are going to implement, type the following commands in the terminal:

ng g s products --skipTests
ng g c product-details --skipTests

Products service

Before digging into the service, let’s declare the Product interface that you will use across the application. Create a new file called product.model.ts in the src/model directory and copy the following code:

export interface Product {
id: string;
name: string;
description: string;
price: number;
image: string;
}

To implement ProductsService, add the following code to src/products.service.ts:

import { Injectable } from ‘@angular/core’;
import { HttpClient } from ‘@angular/common/http’;
import { Product } from ‘…/model/product.model’;
import { Observable } from ‘rxjs’;

@Injectable({
providedIn: ‘root’,
})
export class ProductsService {
private API_URL = ‘/api’;

constructor(private httpClient: HttpClient) {}

public getProducts(): Observable<Product[]> {
return this.httpClient.get<Product[]>(
${this.API_URL}/products
);
}

public getProduct(
productId: string
): Observable {
return this.httpClient.get(
${this.API_URL}/products/${productId}
);
}
}

ProductsService is consuming two API endpoints:

GET /products that returns the list of all products (without product descriptions).

GET /products/:id that returns the full description of the product specified by the :id query parameter.

Notice that you’ve assigned the value /api to the API_URL constant. From now on, you are going to run your frontend together with the backend. You don’t need to specify the whole URL of the API anymore because it will be served from the same domain as the frontend. Apply the same modification in src/app/user.service.ts and change API_URL accordingly.

Product details component

You now need to add ProductDetailsComponent to the application routing. Change routes in src/app-routing.module.ts to the following:

import { ProductDetailsComponent } from ‘./product-details/product-details.component’;

const routes: Routes = [
{ path: ‘’, redirectTo: ‘products’, pathMatch: ‘full’ },
{ path: ‘products’, component: ProductsListComponent },
{
path: ‘product/:id’,
component: ProductDetailsComponent,
},
{ path: ‘login’, component: LoginComponent },
{
path: ‘favorites’,
component: FavoritesComponent,
canActivate: [AuthGuardService],
},
];

To consume ProductsService in ProductDetailsComponent, replace the content of src/app/product-details/product-details.component.ts with the following TypeScript code:

import { Component, OnInit, Input } from ‘@angular/core’;
import { Product } from ‘src/model/product.model’;
import { ActivatedRoute, ParamMap } from ‘@angular/router’;
import { ProductsService } from ‘…/products.service’;
import {
switchMap,
tap,
mergeMap
} from ‘rxjs/operators’;
import { Observable, of } from ‘rxjs’;
import { UserService } from ‘…/user.service’;

@Component({
selector: ‘app-product-details’,
templateUrl: ‘./product-details.component.html’,
styleUrls: [’./product-details.component.scss’],
})
export class ProductDetailsComponent implements OnInit {
@Input() product: Product;
@Input() isFavorite: boolean;

public product$: Observable;

constructor(
private route: ActivatedRoute,
private ps: ProductsService,
private us: UserService
) {}

ngOnInit(): void {
if (this.product) {
this.product$ = of(this.product);
} else {
this.product$ = this.us.getFavorites().pipe(
mergeMap((favorites) => {
return this.route.paramMap.pipe(
switchMap((params: ParamMap) =>
this.ps.getProduct(params.get(‘id’))
),
tap(
(product) =>
(this.isFavorite = favorites.includes(
product.id
))
)
);
})
);
}
}

public addToFavorites(id: string) {
this.us.addToFavorites(id).subscribe();
}
}

ProductDetailsComponent expects two inputs:

product: defines which details should be displayed.

isFavorite: defines if a product is one of the user’s favorite products.

This component will be used in two ways:

As a sub-component of FavoritesComponent or ProductsListComponent.

As a standalone component to display a given product landing page.

You have to handle both situations. In the ngOnInit() method, you are checking if @Input() product has been provided. If not, you retrieve product and determine if it is the user’s favorite using ProductsService and UserService.

To implement a template for ProdcutDetailsComponent, add the following HTML to src/app/product-details/product-details.component.html:

<ng-container *ngIf=“product$ | async as p”>

pictogram of {{p.name}}

{{p.name}}

{{p.price | currency}}

{{p.description}}

{{p.name}} description add to favorites

To add styling to the component, add the following CSS into src/app/product-details/product-details.component.scss:

.bd-placeholder-img {
background: #eeeeee;
width: 100%;
height: 250px;
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
}

Products list and favorites

You can now use ProductDetailsComponent to display products retrieved via the API. Replace the content of src/app/products-list/products-list.component.ts with the following:

import { Component, OnInit } from ‘@angular/core’;
import { ProductsService } from ‘…/products.service’;
import { Observable } from ‘rxjs’;
import { Product } from ‘src/model/product.model’;
import { UserService } from ‘…/user.service’;
import { ActivatedRoute } from ‘@angular/router’;
import { mergeMap, filter } from ‘rxjs/operators’;

@Component({
selector: ‘app-products-list’,
template: <h1 class="mb-3">All products</h1> <div class="row"> <app-product-details *ngFor="let product of products$ | async" [product]="product" [isFavorite]=" (userFavorites$ | async)?.includes(product.id) " class="col-md-4" ></app-product-details> </div> ,
styles: [],
})
export class ProductsListComponent implements OnInit {
public products$: Observable<Product[]>;
public userFavorites$: Observable<string[]>;

constructor(
private ps: ProductsService,
private us: UserService,
private route: ActivatedRoute
) {}

ngOnInit(): void {
this.route.paramMap
.pipe(
filter((params) => params.keys.length > 0),
mergeMap((params) =>
this.us.addToFavorites(params.get(‘pid’))
)
)
.subscribe();
this.products$ = this.ps.getProducts();
this.userFavorites$ = this.us.getFavorites();
}
}

Let’s see what’s inside ngOnInit(). If you recall, UserService contains a method called addToFavorites(). If this method is called by an unauthenticated user, the application redirects them to the login page. After successful authentication, the user is redirected to the /products page. The ID of the product (pid) that the user wants to add to favorites is passed as a query parameter. You are checking if this parameter appears in the URL. If it does, it means you should call the addToFavorites() method.

To use ProductDetailsComponent inside FavoritesComponent, replace the content of src/app/favorites/favorites.component.ts with the following code:

import { Component, OnInit } from ‘@angular/core’;
import { UserService } from ‘…/user.service’;
import { ProductsService } from ‘…/products.service’;
import { map, mergeMap } from ‘rxjs/operators’;

@Component({
selector: ‘app-favorites’,
template: <h1 class="mb-3">Your favorite</h1> <div class="row"> <app-product-details *ngFor="let product of favorite$ | async" [product]="product" [isFavorite]="true" class="col-md-4" > </app-product-details> </div> ,
styles: [],
})
export class FavoritesComponent implements OnInit {
public favorite$;

constructor(
private us: UserService,
private ps: ProductsService
) {}

ngOnInit(): void {
this.favorite$ = this.us.getFavorites().pipe(
mergeMap((favoriteProducts) => {
return this.ps.getProducts().pipe(
map((allProducts) => {
return allProducts.filter((product) =>
favoriteProducts.includes(product.id)
);
})
);
})
);
}
}

Testing the app

Build your application:

npm run build
npm start

Navigate to your application and verify that it works as expected: