Codementor Events

Building a Product Rating and Review with Angular5 and Cloud Firestore

Published Feb 28, 2020

angular-firestore-product-review-rating
In this article, I will walk you through how to create a product rating and review app using Angular5 and Cloud Firestore.
In one of my previous post, I have shown how to setup a firebase project. The process is the same except that we will be working with cloud firestore instead of the real time database.


The data structure is very simple, we have products, ratings and reviews collections with their corresponding documents. Start by creating a new angular app using angular-cli command like so:

ng new ng5-firestore-product-review-rating --routing=true

The --route tells angular -cli that we want to make use of route in our application.

Next, let's install firebase and angularfire2 like so:

npm install firebase angularfire2 --save

Open /src/environments/environment.ts and add your Firebase configuration:

export const environment = {
  production: false,
  firebase:{
    apiKey: "AIzaSyA5pDbhYvXmmP3cfM1sFdkpCi9lRrebmb0",
    authDomain: "product-review-rating.firebaseapp.com",
    databaseURL: "https://product-review-rating.firebaseio.com",
    projectId: "product-review-rating",
    storageBucket: "",
    messagingSenderId: "565246664152"
  }
};

Setup @NgModule for the AngularFireModule

Open /srcc/app/app.module.ts and inject necessary firebase providers like so:

import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';

@NgModule({
  imports: [
    BrowserModule,
    AngularFireModule.initializeApp(environment.firebase,'ngproductreview'),
    AngularFirestoreModule
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

Product Service We are going to be making use of three models in this project, so let's start by creating those models first. Create a folder like so: \src\app\shared\models, then add three files named product.ts,product-review.ts and rating.ts add the following code:

export interface Product {
          id: string;
          name: string;
          slug: string;
          images: any[],
          price: number,
          avRating:number
}

export interface ProductRating { 
     productId: string; 
     ratingValue:number; 
}

export interface ProductReview { 
     productId:string, 
     username: string;
     summary:string; 
     review:string 
}

Create a folder in \src\app\shared\services and add a file named product.service.ts and add the following code:

import { Injectable } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { Observable } from 'rxjs/Observable';
import { Product } from '../models/product'; @Injectable()
export class ProductService {
import { Injectable } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { Observable } from 'rxjs/Observable';
import { Product } from '../models/product';
import { ProductReview } from '../models/product-review';
import { ProductRating } from '../models/rating'; @Injectable()
export class ProductService { products: AngularFirestoreCollection; product: Product; constructor(private fs: AngularFirestore) { } getProducts() { return this.fs.collection("products").snapshotChanges().map(actions => { return actions.map(a => { const data = a.payload.doc.data() as Product; data.id = a.payload.doc.id; data.avRating = data.avRating/5*100; data.slug = data.name.toLowerCase().replace('/\s/g', '-').replace(' ', '-'); return data; }); }); } getSingleProduct(productId) { var docPath = `products/${productId}`; return this.fs.doc(docPath).snapshotChanges().map((actions) => { const data = actions.payload.data() as Product; data.avRating = data.avRating/5*100; data.id = actions.payload.id; return data; }); } getProductReviews(productId) { return this.fs.collection("reviews", ref => ref.where('productId', '==', productId)).snapshotChanges().map(actions => { return actions.map(a => { const data = a.payload.doc.data() as ProductReview; return data; }); }) } postReview(comment: ProductReview) { this.fs.collection("reviews").add({ username: comment.username, summary: comment.summary, review: comment.review, productId: comment.productId }); } postRating(rating: ProductRating) { this.fs.collection("ratings").add({ productId: rating.productId, ratingValue: rating.ratingValue }); } getProductRating(productId) { return this.fs.collection("ratings", ref => ref.where('productId', '==', productId)).snapshotChanges().map(actions => { return actions.map(a => { const data = a.payload.doc.data() as ProductRating; return data; }); }) } setProductRating(productId, rating) { var docPath = `products/${productId}`; let productDoc = this.fs.doc(docPath); productDoc.update({ avRating: rating }); }
}

Let's walkthrough what we have in the product service by discussing what each method does. This method simply get list of products from firestore. This method can be written like so: return this.fs.collection("products").valueChanges();

But because the above method will return list of products but without productId, so if we need to return all products with the id we need to write the method like that. You can read more about that here and check stackoverflow question here.

This method, as the name implies get a product document taking the Id of the product as argument. This also takes productId as an argument to return lists of product reviews. This method is responsible for customer review's submission.
This method allows customer rate a particular product. This returns the list of product rating taking productId as an argument.
This method update the product's average rating.
Add a folder name products in /src/app/products, and modify the productListComponent like so:

import { Component, OnInit } from '@angular/core';
import { ProductService } from '../shared/services/product.service';

@Component({
     templateUrl: 'product-list.component.html'
})

export class ProductListComponent implements OnInit {
     products: any;

     constructor(private productService: ProductService) { }

     ngOnInit() {
          this.getProducts();
     }

     getProducts() {
          
          this.productService.getProducts().subscribe((data) => {
               this.products = data;

          });
     }

}

As you can see, productlistcomponent has only one method that fetches all product.

## Featured Products 
 

 ![](../assets/img/products/{{product.images[0]}})

SingleComponent.ts File

import { ProductReview } from './../shared/models/product-review';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductService } from '../shared/services/product.service';
import { AngularFirestoreDocument } from 'angularfire2/firestore';
import { Product } from '../shared/models/product';
import { FormControl, FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs/Observable'; @Component({ templateUrl: 'single.component.html'
}) export class SingleComponent implements OnInit { product: Product; reviewFormGroup: FormGroup; reviews: Array; selectedProductId: string; productLoaded: boolean = false; ratings: Observable; avgRating: Observable; constructor(private route: ActivatedRoute, private productService: ProductService, private fb: FormBuilder) { this.selectedProductId = this.route.snapshot.params.productid; this.initializeForm(); } ngOnInit() { this.productService.getSingleProduct(this.selectedProductId).subscribe((data) => { this.product = data; this.productLoaded = true; }); this.getReviews(); } getReviews() { this.productService.getProductReviews(this.selectedProductId).subscribe((data) => { this.reviews = data; }); } initializeForm() { this.reviewFormGroup = this.fb.group({ username: ['', Validators.required], summary: ['', Validators.required], review: ['', Validators.required], productId: [this.selectedProductId] }); } submitReview() { let body: ProductReview = this.reviewFormGroup.value; this.productService.postReview(body); this.initializeForm(); } rateProduct(val) { this.productService.postRating({ productId: this.selectedProductId, ratingValue: val }); this.productService.getProductRating(this.selectedProductId).subscribe((retVal) => { const ratings = retVal.map(v => v.ratingValue); let avRating = (ratings.length ? ratings.reduce((total, val) => total + val) / retVal.length : 0); this.productService.setProductRating(this.selectedProductId,avRating.toFixed(1)); }); } }

All the methods we have in the Single component is self explanatory. Let see what we have in the single component html.

![](../assets/img/products/{{ product?.images[0]}})

Feel free to download the source code and play around with it. Your comments are welcome.

Discover and read more posts from Mark Adesina Omoniyi
get started