===================================== Angular Integration with Spring Boot ===================================== => Angular is a client side framework => Angular framework developed by Google company => Angular developed by using TypeScript => Angular is free & open source => Angular supports multiple browsers => Angular is mainley used for SPA (single page app) Note: Angular JS & Angular framework both are not same. => Angular JS developed using Java Script. (Angular 1.x) => Google identified some performance issues in Angular JS 1.x version then they re-developed angular using Typescript which is called as Angular framework. Note: From 2.x version onwards it is called as Angular Framework. =================== Environment Setup =================== Step-1 : Download and Install Node URL : https://nodejs.org/en/ Step-2 : Install Type Script $ npm install -g typescript $ tsc -v Step-3 : Install Angular CLI $ npm install @angular/cli -g $ ng v Step-4 : Download and Install VS Code IDE URL : https://code.visualstudio.com/download ================================ Step-1 : Angular Project Setup ================================ 1) Create Angular Application $ ng new ecomm_ui 2) Run the application $ ng serve --open Note: By default app-component will be loaded. 3) Remove everything from "app.component.html" file and add your message Note: It should reflect in web page 4) Create AppConstants to declare backend api url endpoints $ ng generate class appconstants ``` export class AppConstants { static PRODUCTS_BY_CATEGORY_ID = "http://localhost:8080/products/1"; } ``` ================================================================= Step-2 : Retrieve Products From Backend and Display In Frontend ================================================================= @@@@ (1) Create Product class to bind backend-api json response in frontend-app as a object. $ ng generate class dto/product ``` export class Product { constructor( public productId: number, public name: string, public description: string, public title: string, public unitPrice: number, public imageUrl: string, public active: boolean, public unitsInStock: number, public dateCreated: Date, public lastUpdated: Date ) { } } ``` @@@ (2) Create Service class to make HTTP call to backend app ``` export class ProductService { constructor(private http: HttpClient) { } getProducts() : Observable{ return this.http.get(`${AppConstants.PRODUCTS_BY_CATEGORY_ID}`); } } ``` @@@ (3) Create "Product-List" Component then fetch products and display in template $ ng generate component components/product-list ``` export class ProductListComponent implements OnInit { products: Product[] = []; constructor(private productService: ProductService) { } ngOnInit(): void { this.getAllProducts(); } getAllProducts() { this.productService.getProducts().subscribe(res => { this.products = res.data; }) } } ``` @@@@ (4) Write Presentation Logic to Display Products Data in template (HTML file) ```

{{tempProduct.name}} :: {{tempProduct.unitPrice}}

``` @@@@ (5) invoke product-list component from AppComponent using selector in html page. @@@@ (6) When we run angular app, we should see products data in browser. ------------------ 2 issues identified in browser console and fixed --------------------- Issue-1 :: CORS Issue (Add @CrossOrigin annotation at Rest Controller to resolve this) Issue-2 :: HttpClient Provider issue (in NG app, configure httpclient as provider in app.config.ts file like below to resolve this) ``` import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient() ] }; ``` ====================================================== Step-3 : Beautify Products Display in Table Format ====================================================== @@@@ (1) Add bootstap css link in index.html file @@@@ (2) Add div container in app.component.html file like below ```

Ashok IT - Ecommerce Application

``` @@@@ (3) Make below changes in "product.list.component.html" file to display products in table format. ```
Name Price Units in Stock
No Records Available
{{tempProduct.name}} {{tempProduct.unitPrice}} {{tempProduct.unitsInStock}}
``` =========================================== Step-4 : eCommerce Template Integration =========================================== @@@@ (1) Download below zip file which contains images and styles.css URL : https://github.com/ashokitschool/Images.git @@@@ (2) Keep images folder in "angular project public folder" and keep "styles.css" file in "src" folder. @@@@ (3) Install bootstrap in angular project $ npm install bootstrap@5.2.0 @@@@ (4) Install FontAwesome in angular project $ npm install @fortawesome/fontawesome-free @@@@ (5) Add below content in "app.component.html" file ``` ``` @@@@ (6) Modify product-list.component.html file to display products in grid format. ```

{{ tempProduct.name }}

{{ tempProduct.unitPrice }}
Add to cart
``` ========================================= Step - 5 : Filter Products By Category ========================================= 1) Configure Routings in app.routes.ts file ``` export const routes: Routes = [ {path : 'category/:id', component: ProductListComponent}, {path : 'products', component: ProductListComponent}, {path: '', redirectTo: '/products', pathMatch: 'full'} ]; ``` 2) Configure routerlink in "app.component.html" file (hardcoded links only) ``` ``` 3) Configure in app.component.html file (remove selector to invoke product-list component). Note: Import RouterModule in app.component.ts file 4) Create a method in product.service.ts file to get products based on given category id. ``` getProductsByCategoryId(theCategoryId: number): Observable { return this.http.get(`${AppConstants.PRODUCTS_ENDPOINT}/${theCategoryId}`); } ``` 5) Make changes to ProductList component ts file to read categoryId from routerLink ``` export class ProductListComponent implements OnInit { products: Product[] = []; currentCategoryId = 1; constructor(private productService: ProductService, private route: ActivatedRoute ) { } ngOnInit(): void { this.route.paramMap.subscribe(() => { this.getAllProducts(); }) } getAllProducts() { const hasCategoryId = this.route.snapshot.paramMap.has("id"); if (hasCategoryId) { this.currentCategoryId = +this.route.snapshot.paramMap.get('id')!; } else { this.currentCategoryId = 1; } this.productService.getProductsByCategoryId(this.currentCategoryId).subscribe(res => { this.products = res.data; }) } } ``` ========================================================= Step - 6 : Build Product Category Menu Dynamically ========================================================= @@@@ (1) Create ProductCategory class to map backend-api response $ ng g class dto/product-category ``` export class ProductCategory { constructor( public categoryId: number, public categoryName: string) { } } ``` @@@@ (2) Create a method in product.service to fetch categories from backend. ``` getCategories() : Observable{ return this.http.get(`${AppConstants.CATEGORIES_ENDPOINT}`); } ``` @@@@ (3) Create product-category-menu component $ ng g c components/product-category-menu ``` export class ProductCategoryMenuComponent implements OnInit { productCategories: ProductCategory[] = []; constructor(private productService: ProductService) { } ngOnInit(): void { this.listProductCategories(); } listProductCategories() { this.productService.getCategories().subscribe(res => { this.productCategories = res.data; }) } } ``` Note: Import CommonModule and RouterModule in above component. @@@@ (4) Write Product-Category template logic to display menu @@@@ (5) Remove static menu from app-component.html file and invoke product-category-menu using selector. ========================================= Step - 7 : Build Search Functionality ========================================= @@@@ (1) Configure routing in app.routes.ts file for search functionality {path: 'search/:keyword', component: ProductListComponent}, @@@@ (2) Create search component $ ng generate component components/search ``` export class SearchComponent { constructor(private router: Router) { } doSearch(value: string) { this.router.navigateByUrl(`/search/${value}`); } } ``` @@@@ (3) Write presentation logic for search component ```
``` @@@@ (4) Remove hardcoded search and Invoke search component from app.component.html file using selector like below. @@@@ (5) Create method in service class to handle search functionality backend api call ``` searchProducts(theKeyword: string): Observable { return this.http.get(`${AppConstants.PRODUCTS_SEARCH_ENDPOINT}/${theKeyword}`); } ``` @@@@ (6) Handle search operation in product-list-component.html // check keyword presence in url // if keyword present in url, call handle-search-products() with keyword // if keyword not present, call handle-list-products() with category_id ``` export class ProductListComponent implements OnInit { products: Product[] = []; currentCategoryId = 1; searchMode: boolean = false; constructor(private productService: ProductService, private route: ActivatedRoute ) { } ngOnInit(): void { this.route.paramMap.subscribe(() => { this.getAllProducts(); }) } getAllProducts() { this.searchMode = this.route.snapshot.paramMap.has("keyword"); if (this.searchMode) { this.handleSearchProducts(); } else { this.handleListProducts(); } } handleListProducts() { const hasCategoryId = this.route.snapshot.paramMap.has("id"); if (hasCategoryId) { this.currentCategoryId = +this.route.snapshot.paramMap.get('id')!; } else { this.currentCategoryId = 1; } this.productService.getProductsByCategoryId(this.currentCategoryId).subscribe(res => { this.products = res.data; }) } handleSearchProducts() { const theKeyword: string = this.route.snapshot.paramMap.get('keyword')!; this.productService.searchProducts(theKeyword).subscribe(res => { this.products = res.data; }) } } ``` ================================= Step - 8 : Build Shopping Cart ================================= @@@@ (1) Create CartItem class $ ng g class dto/cartitem ``` export class Cartitem { productId: number; name: string; imageUrl: string; unitPrice: number; quantity: number; constructor(product: Product){ this.productId = product.productId; this.name = product.name; this.imageUrl = product.imageUrl; this.unitPrice = product.unitPrice; this.quantity = 1; } } ``` @@@@ (2) Create CartStatus component $ ng g c components/cartstatus @@@@ (3) Remove hard coded cart details and invoke cart-status component from app.component.html ```
``` @@@@ (4) Create CartService to handle cart related business logic $ ng generate service services/cart ``` export class CartService { totalPrice: Subject = new Subject(); totalQuantity: Subject = new Subject(); cartItems: Cartitem[] = []; constructor() { } addToCart(theCartItem: Cartitem) { let alreadyExistsInCart: boolean = false; let existingCartItem!: Cartitem; if (this.cartItems.length > 0) { // check items presence in cart based on item id for (let tempCartItem of this.cartItems) { if (tempCartItem.productId == theCartItem.productId) { existingCartItem = tempCartItem; alreadyExistsInCart = true; break; } } } if (alreadyExistsInCart) { existingCartItem.quantity++; } else { this.cartItems.push(theCartItem); } this.computeCartTotals(); } computeCartTotals() { let totalPriceValue: number = 0; let totalQuantityValue: number = 0; for (let currentCartItem of this.cartItems) { totalPriceValue += currentCartItem.quantity * currentCartItem.unitPrice; totalQuantityValue += currentCartItem.quantity; } // publish new values for all subscribers this.totalPrice.next(totalPriceValue); this.totalQuantity.next(totalQuantityValue); } } ``` @@@@ (5) add a function to handle 'Add To Cart' button in "product-list-component" // change in ts file addToCart(theProduct: Product) { console.log(`Adding to cart: ${theProduct.name}, ${theProduct.unitPrice}`); const theCartItem = new CartItem(theProduct); this.cartService.addToCart(theCartItem); } // change in template Add to cart @@@@ (6) Make changes in cart-status-component to display latest cart values (price & quantity) // component ts file change export class CartStatusComponent implements OnInit { totalPrice: number = 0; totalQuantity: number = 0; constructor(private cartService: CartService) { } ngOnInit(): void { this.updateCartStatus(); } updateCartStatus() { this.cartService.totalPrice.subscribe( data => this.totalPrice = data ); this.cartService.totalQuanity.subscribe( data => this.totalQuantity = data ); } } // template change
{{ totalPrice }} {{ totalQuantity }}
============================== Step - 9 : Cart Details Page ============================== @@@@ (1) Create cart-details component $ ng g c components/cartdetails @@@@ (2) Configure routing {path: 'cart-details', component:CartDetailsComponent}, @@@@ (3) Change cart-status template to invoke cart-details page on click of cart-status @@@@ (4) Write logic in cart-details component to access existing cart-items ``` export class CartdetailsComponent implements OnInit { cartItems: Cartitem[] = []; totalPrice: number = 0; totalQuantity: number = 0; constructor(private cartService: CartService) { } ngOnInit(): void { this.getCartItems(); } getCartItems() { this.cartItems = this.cartService.cartItems; // subscribe to total price this.cartService.totalPrice.subscribe( data => this.totalPrice = data ); this.cartService.totalQuantity.subscribe( data => this.totalQuantity = data ); this.cartService.computeCartTotals(); } } ``` @@@@ (4) Write presentation logic to display cart-details
Product Image Product Detail

{{ tempCartItem.name }}

{{ tempCartItem.unitPrice }}

{{ tempCartItem.quantity }}

Subtotal: {{ tempCartItem.quantity * tempCartItem.unitPrice }}

Total Quantity: {{ totalQuantity }}

Shipping: FREE

Total Price: {{ totalPrice }}

@@@@ (5) Add functionality to increment & Decrement item quantity in cart-service ``` decrementQuantity(theCartItem: Cartitem) { theCartItem.quantity--; if (theCartItem.quantity == 0) { this.remove(theCartItem); } else { this.computeCartTotals(); } } remove(theCartItem: Cartitem) { // get index of item in the array const itemIndex = this.cartItems.findIndex(tempCartItem => tempCartItem.productId == theCartItem.productId); if (itemIndex > -1) { this.cartItems.splice(itemIndex, 1); this.computeCartTotals(); } } ``` @@@@ (6) Write functions in "cart-details component" to handle increment, decrement and remove buttions ``` incrementQuantity(theCartItem: Cartitem) { this.cartService.addToCart(theCartItem); } decrementQuantity(theCartItem: Cartitem) { this.cartService.decrementQuantity(theCartItem); } remove(theCartItem: Cartitem) { this.cartService.remove(theCartItem); } ``` @@@@ 7) Make changes in cart-details template page to display +, - and remove options for cart items ```
Product Image Product Detail

{{ tempCartItem.name }}

{{ tempCartItem.unitPrice }}

{{ tempCartItem.quantity }}

Subtotal: {{ tempCartItem.quantity * tempCartItem.unitPrice | currency: 'USD' }}

Cart Summary

Total Quantity: {{ totalQuantity }}

Total Price: {{ totalPrice | currency: 'USD'}}

``` ============================== Step - 10 : Checkout Page ============================== @@@@ (1) Generate checkout component $ ng generate component components/checkout @@@@ (2) Configure router for checkout component {path: 'checkout', component:CheckoutComponent}, @@@@ (3) Import RouterModule in cart-details component Note: Make sure checkout button having router link to load checkout component. @@@@ (4) Configure Form Group in checkout component ts file to load form fields ``` checkoutFormGroup!: FormGroup; totalPrice: number = 0; totalQuantity: number = 0; constructor(private formBuilder: FormBuilder) { } ngOnInit(): void { this.checkoutFormGroup = this.formBuilder.group({ customer: this.formBuilder.group({ name: [''], email: [''], phno: [''] }), shippingAddress: this.formBuilder.group({ street: [''], city: [''], state: [''], hno: [''], zipCode: [''] }) }) } onSubmit() { console.log(this.checkoutFormGroup.get('customer')!.value); console.log(this.checkoutFormGroup.get('shippingAddress')!.value); } } ``` @@@@ (5) Presentation logic to display form in checkout template page ```

Customer

Shipping Address

Review Your Order

Total Quantity: {{ totalQuantity }}

Shipping: FREE

Total Price: {{ totalPrice }}

``` ============================================== Step - 11 : Sending Order details to backend ============================================== 1) Create binding classes $ ng generate class dto/customer $ ng generate class dto/address $ ng generate class dto/order $ ng generate class dto/order-items $ ng generate class dto/purchase-order ``` export class Customer { constructor( public name: string, public email: string, public phno: string ) {} } ``` export class Address { constructor( public houseNum: string, public street: string, public city: string, public state: string, public zipCode: string ) { } } ``` export class Order { constructor( public totalQuantity: number, public totalPrice: number ) { } } ``` export class OrderItems { constructor( public imageUrl: string, public unitPrice: number, public quantity: number, public itemId: number ) { } } ``` export class PurchaseOrder { customer!: Customer; address!: Address; order!: Order; orderItems!: OrderItems; } ``` 2) Create checkout service to handle orders related business logic $ ng generate service services/checkout ``` export class CheckoutService { constructor(private httpClient: HttpClient) { } placeOrder(purchaseOrder: PurchaseOrder): Observable { return this.httpClient.post(AppConstants.PLACE_ORDER_API, purchaseOrder); } } ``` 3) Make changes in checkout component ts file to send order details to backend ``` export class CheckoutComponent implements OnInit { checkoutFormGroup!: FormGroup; totalPrice: number = 0; totalQuantity: number = 0; constructor(private formBuilder: FormBuilder, private cartService: CartService, private checkoutService: CheckoutService ) { } ngOnInit(): void { this.reviewCartDetails(); this.checkoutFormGroup = this.formBuilder.group({ customer: this.formBuilder.group({ name: [''], email: [''], phno: [''] }), shippingAddress: this.formBuilder.group({ street: [''], city: [''], state: [''], hno: [''], zipCode: [''] }) }) } get getName() { return this.checkoutFormGroup.get('customer.name'); } get email() { return this.checkoutFormGroup.get('customer.email'); } get phno() { return this.checkoutFormGroup.get('customer.phno'); } get addressStreet() { return this.checkoutFormGroup.get('address.street'); } get addressCity() { return this.checkoutFormGroup.get('address.city'); } get adressState() { return this.checkoutFormGroup.get('address.state'); } get addressZipCode() { return this.checkoutFormGroup.get('address.zipCode'); } get addressHouseNum() { return this.checkoutFormGroup.get('address.hno'); } onSubmit() { // setup order let order = new Order(this.totalPrice, this.totalQuantity); // setup order items const cartItems = this.cartService.cartItems; let orderItems: OrderItems[] = cartItems.map(tempCartItem => new OrderItems(tempCartItem.imageUrl!, tempCartItem.unitPrice!, tempCartItem.quantity!, tempCartItem.productId)); // setup purchase order let purchase = new PurchaseOrder(); purchase.customer = this.checkoutFormGroup.controls['customer'].value; purchase.address = this.checkoutFormGroup.controls['shippingAddress'].value; purchase.order = order; purchase.orderItems = orderItems; // make backend api call this.checkoutService.placeOrder(purchase).subscribe(response => { const responseData = response.data; console.log(responseData); alert("Order Placed"); // Payment Gateway Integration }) } reviewCartDetails() { this.cartService.totalPrice.subscribe( data => this.totalPrice = data ); this.cartService.totalQuantity.subscribe( data => this.totalQuantity = data ); } } ``` ============================================== Step - 12 : Payment Gateway Integration ============================================== 1) Configure razorpay java script file in index.html like below 2) create a file under src (filename: razorpay.d.ts) to declare razorpay object declare var Razorpay: any; 3) Create payment service to handle razorypay gateway ``` export class PaymentService { private RAZORPAY_KEY = 'rzp_fsyrtef'; // Replace with your Razorpay key constructor() { } processPayment(orderId: string, amount: number, successCallback: (response: any) => void): void { const options: any = { key: this.RAZORPAY_KEY, amount: amount * 100, currency: 'INR', name: 'Ashok IT', description: 'ECommerce Order', order_id: orderId, handler: successCallback, prefill: { name: 'Ashok IT', email: 'ashokit@gmail.com', contact: 9985396677 }, theme: { "color": "#3399cc" } }; const rzp1 = new Razorpay(options); rzp1.open(); } } ``` 4) Create binding object to represent order related ids ``` export class OrderInfo { constructor( public razorPayOrderId: string, public razorPayPaymentId: string, public orderStatus: string ) { } } ``` 5) Add the method in checkout-service.ts to update order status in backend ``` export class CheckoutService { constructor(private httpClient: HttpClient) { } placeOrder(purchaseOrder: PurchaseOrder): Observable { return this.httpClient.post(AppConstants.PLACE_ORDER_API, purchaseOrder); } updateOrder(orderInfo: OrderInfo): Observable { return this.httpClient.put(AppConstants.UPDATE_ORDER_API, orderInfo); } } ``` 6) Create Payment Successs Component to display 'success page' after payment 7) Handle Payment Gateway page in checkout component ``` export class CheckoutComponent implements OnInit { checkoutFormGroup!: FormGroup; totalPrice: number = 0; totalQuantity: number = 0; orderId: string = ""; constructor(private formBuilder: FormBuilder, private cartService: CartService, private checkoutService: CheckoutService, private paymentService: PaymentService, private router: Router ) { } ngOnInit(): void { this.reviewCartDetails(); this.checkoutFormGroup = this.formBuilder.group({ customer: this.formBuilder.group({ name: [''], email: [''], phno: [''] }), shippingAddress: this.formBuilder.group({ street: [''], city: [''], state: [''], hno: [''], zipCode: [''] }) }) } get getName() { return this.checkoutFormGroup.get('customer.name'); } get email() { return this.checkoutFormGroup.get('customer.email'); } get phno() { return this.checkoutFormGroup.get('customer.phno'); } get addressStreet() { return this.checkoutFormGroup.get('address.street'); } get addressCity() { return this.checkoutFormGroup.get('address.city'); } get adressState() { return this.checkoutFormGroup.get('address.state'); } get addressZipCode() { return this.checkoutFormGroup.get('address.zipCode'); } get addressHouseNum() { return this.checkoutFormGroup.get('address.hno'); } onSubmit() { // setup order let order = new Order(this.totalPrice, this.totalQuantity); // setup order items const cartItems = this.cartService.cartItems; let orderItems: OrderItems[] = cartItems.map(tempCartItem => new OrderItems(tempCartItem.imageUrl!, tempCartItem.unitPrice!, tempCartItem.quantity!, tempCartItem.productId)); // setup purchase order let purchase = new PurchaseOrder(); purchase.customer = this.checkoutFormGroup.controls['customer'].value; purchase.address = this.checkoutFormGroup.controls['shippingAddress'].value; purchase.order = order; purchase.orderItems = orderItems; // make backend api call this.checkoutService.placeOrder(purchase).subscribe(response => { const responseData = response.data; console.log(responseData); // Payment Gateway Integration this.paymentService.processPayment(responseData.razorpayOrderId, this.totalPrice, this.onPaymentSuccess.bind(this)); }) } onPaymentSuccess(response: any) { console.log(response); this.resetCart(); // setup purchase order let orderInfo = new OrderInfo(response.razorpay_order_id, response.razorpay_payment_id, 'CONFIRMED'); this.checkoutService.updateOrder(orderInfo).subscribe(res => { this.router.navigateByUrl("/payment-success"); }); } reviewCartDetails() { this.cartService.totalPrice.subscribe( data => this.totalPrice = data ); this.cartService.totalQuantity.subscribe( data => this.totalQuantity = data ); } resetCart() { // reset cart data this.cartService.cartItems = []; this.cartService.totalPrice.next(0); this.cartService.totalQuantity.next(0); // reset the form this.checkoutFormGroup.reset(); // navigate back to the products page this.router.navigateByUrl("/products"); } } ```