n
nIn this article, I will walk you through how to create a product rating and review app using Angular5 and Cloud Firestore.
n
nn
nn
n
nSetting up our Firebase Project
nIn 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.
n
n
n
n
n
nFirestore Data Structure
n
n
n
nThe data structure is very simple, we have products, ratings and reviews collections with their corresponding documents.
n
n
nSetting up the Angular Project
nStart by creating a new angular app using angular-cli command like so:n
n
ng new ng5-firestore-product-review-rating --routing=truen
n
nThe –route tells angular -cli that we want to make use of route in our application.
n
nNext, let’s install firebase and angularfire2 like so:
n
n
npm install firebase angularfire2 --save
n
n
nAdd Firebase config to environments variablen
nOpen /src/environments/environment.ts and add your Firebase configuration:n
n
n
nnexport const environment = {n production: false,n firebase:{n apiKey: "AIzaSyA5pDbhYvXmmP3cfM1sFdkpCi9lRrebmb0",n authDomain: "product-review-rating.firebaseapp.com",n databaseURL: "https://product-review-rating.firebaseio.com",n projectId: "product-review-rating",n storageBucket: "",n messagingSenderId: "565246664152"n }n};nn
n
nnn
nnnSetup @NgModule for the AngularFireModulen
n
nOpen /srcc/app/app.module.ts and inject necessary firebase providers like so:n
n
nnimport { AngularFireModule } from 'angularfire2';nimport { AngularFirestoreModule } from 'angularfire2/firestore';nnn@NgModule({n imports: [n BrowserModule,n AngularFireModule.initializeApp(environment.firebase,'ngproductreview'),n AngularFirestoreModulen ],n declarations: [ AppComponent ],n bootstrap: [ AppComponent ]n})nexport class AppModule {}nn
n
nProduct Service
nWe are going to be making use of three models in this project, so let’s start by creating those models first.
nCreate 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:
n
n
nnnexport interface Product {n id: string;n name: string;n slug: string;n images: any[],n price: number,n avRating:numbern}nnexport interface ProductRating { n productId: string; n ratingValue:number; n}nnexport interface ProductReview { n productId:string, n username: string;n summary:string; n review:string n}nn
n
nCreate a folder in \src\app\shared\services and add a file named product.service.ts and add the following code:
n
nnimport { Injectable } from '@angular/core';nimport { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';nimport { Observable } from 'rxjs/Observable';nimport { Product } from '../models/product';nnn@Injectable()nexport class ProductService {nimport { Injectable } from '@angular/core';nimport { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';nimport { Observable } from 'rxjs/Observable';nimport { Product } from '../models/product';nimport { ProductReview } from '../models/product-review';nimport { ProductRating } from '../models/rating';nn@Injectable()nexport class ProductService {n products: AngularFirestoreCollection;n product: Product;nn constructor(private fs: AngularFirestore) { }nnn getProducts() {n return this.fs.collection("products").snapshotChanges().map(actions => {n return actions.map(a => {n const data = a.payload.doc.data() as Product;n data.id = a.payload.doc.id;n data.avRating = data.avRating/5*100;n data.slug = data.name.toLowerCase().replace('/\s/g', '-').replace(' ', '-');n return data;n });n });n }nnnn getSingleProduct(productId) {n var docPath = `products/${productId}`;n return this.fs.doc(docPath).snapshotChanges().map((actions) => {n const data = actions.payload.data() as Product;n data.avRating = data.avRating/5*100;n data.id = actions.payload.id;n return data;n });n }nn getProductReviews(productId) {nn return this.fs.collection("reviews", ref => ref.where('productId', '==', productId)).snapshotChanges().map(actions => {n return actions.map(a => {n const data = a.payload.doc.data() as ProductReview;n return data;n });n })n }nn postReview(comment: ProductReview) {n this.fs.collection("reviews").add({n username: comment.username,n summary: comment.summary,n review: comment.review,n productId: comment.productIdn });n }nn postRating(rating: ProductRating) {n this.fs.collection("ratings").add({n productId: rating.productId,n ratingValue: rating.ratingValuen });n }nn getProductRating(productId) {nn return this.fs.collection("ratings", ref => ref.where('productId', '==', productId)).snapshotChanges().map(actions => {n return actions.map(a => {n const data = a.payload.doc.data() as ProductRating;n return data;n });n })n }nn setProductRating(productId, rating) {n var docPath = `products/${productId}`;n let productDoc = this.fs.doc(docPath);n productDoc.update({ avRating: rating });n }n}n nn
n
nLet’s walkthrough what we have in the product service by discussing what each method does.
n
n
ngetProducts Method
nThis method simply get list of products from firestore. This method can be written like so:
n
nreturn this.fs.collection(“products”).valueChanges();
n
nBut 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.
n
n
ngetSingleProduct Methodn
nThis method, as the name implies get a product document taking the Id of the product as argument.
n
n
ngetProductReviews Method
nThis also takes productId as an argument to return lists of product reviews.
n
n
npostReview Method
nThis method is responsible for customer review’s submission.
n
n
n
npostRating Method
nThis method allows customer rate a particular product.
n
n
ngetProductRating Method
nThis returns the list of product rating taking productId as an argument.
n
n
setProductRating Method
nThis method update the product’s average rating.
n
nnn
nnn
n
nProductList Component
nAdd a folder name products in /src/app/products, and modify the productListComponent like so:
n
n
nnnimport { Component, OnInit } from '@angular/core';nimport { ProductService } from '../shared/services/product.service';nnn@Component({n templateUrl: 'product-list.component.html'n})nnexport class ProductListComponent implements OnInit {n products: any;nn constructor(private productService: ProductService) { }nn ngOnInit() {n this.getProducts();n }nnnn getProducts() {n n this.productService.getProducts().subscribe((data) => {n this.products = data;nn });n }nnn}nn
n
n
nAs you can see, productlistcomponent has only one method that fetches all product.
n
n
nProductList Component Html
n
n
nnn nnnnn Featured Productsn
nnnnnnnnnnnnn
n
nSingleComponent.ts Filen
n
nnimport { ProductReview } from './../shared/models/product-review';nimport { Component, OnInit } from '@angular/core';nimport { ActivatedRoute } from '@angular/router';nimport { ProductService } from '../shared/services/product.service';nimport { AngularFirestoreDocument } from 'angularfire2/firestore';nimport { Product } from '../shared/models/product';nimport { FormControl, FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';nimport { Observable } from 'rxjs/Observable';nnn@Component({nn templateUrl: 'single.component.html'n})nnexport class SingleComponent implements OnInit {n product: Product;n reviewFormGroup: FormGroup;n reviews: Array;n selectedProductId: string;n productLoaded: boolean = false;n ratings: Observable;n avgRating: Observable;n constructor(private route: ActivatedRoute, private productService: ProductService,n private fb: FormBuilder) {n this.selectedProductId = this.route.snapshot.params.productid;n this.initializeForm();n }nn ngOnInit() {nn this.productService.getSingleProduct(this.selectedProductId).subscribe((data) => {n this.product = data;n this.productLoaded = true;n });n this.getReviews();n }nn getReviews() {n this.productService.getProductReviews(this.selectedProductId).subscribe((data) => {n this.reviews = data;n });n }nn initializeForm() {n this.reviewFormGroup = this.fb.group({n username: ['', Validators.required],n summary: ['', Validators.required],n review: ['', Validators.required],n productId: [this.selectedProductId]n });n }nn submitReview() {n let body: ProductReview = this.reviewFormGroup.value;n this.productService.postReview(body);n this.initializeForm();n }nn rateProduct(val) {nn this.productService.postRating({n productId: this.selectedProductId,n ratingValue: valn });nn this.productService.getProductRating(this.selectedProductId).subscribe((retVal) => {n const ratings = retVal.map(v => v.ratingValue);n let avRating = (ratings.length ? ratings.reduce((total, val) => total + val) / retVal.length : 0);nn this.productService.setProductRating(this.selectedProductId,avRating.toFixed(1));n });nnnn }nnn n}n n
n
nAll the methods we have in the Single component is self explanatory. Let see what we have in the single component html.n
n
nnnnnnnnnnnn
nnnnnnnnn {{ product?.name}}
nnn{{ product?.description}}nnnnnnnnnn {{ product?.price | currency:'USD':true}}nnnnnnnnnnnnnnCustomer Reviews
nnnnnn{{review?.summary}}n n n {{review?.username}}n nnn{{review?.review}}nnnnnnnnnnn
nFeel free to download the source code and play around with it. Your comments are welcome.
