Building a Product Rating and Review with Angular5 and Cloud Firestore

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.


Setting up our Firebase Project

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.



Firestore Data Structure



The data structure is very simple, we have products, ratings and reviews collections with their corresponding documents.

Setting up the Angular Project

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

Add Firebase config to environments variable

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.

getProducts Method

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.

getSingleProduct Method

This method, as the name implies get a product document taking the Id of the product as argument.

getProductReviews Method

This also takes productId as an argument to return lists of product reviews.

postReview Method

This method is responsible for customer review's submission.

postRating Method

This method allows customer rate a particular product.

getProductRating Method

This returns the list of product rating taking productId as an argument.


setProductRating Method

This method update the product's average rating.



ProductList Component

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.

ProductList Component Html



          

Featured Products


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.


{{ product?.name}}

{{ product?.description}}
{{ product?.price | currency:'USD':true}}

Customer Reviews

{{review?.summary}} {{review?.username}}
{{review?.review}}


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

No comments

Powered by Blogger.