Reputation: 33
I'm very new to Angular and I'm having difficulty passing data from a parent to child component. I have a product listing page where you click on a product and it routes to the product detail page. But I can't seem to get the product details to show.
Component 1: Contains all the product data. Component 2: Routes to a new page where said product data is displayed.
product list (parent) component file:
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Subscription } from "rxjs";
import { IProduct } from "./product";
import { ProductService } from "./product.service";
@Component({
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit, OnDestroy {
pageTitle = 'Anthony\'s Product List';
imageWidth = 50;
imageMargin = 2;
showImage: boolean = true;
errorMessage: string = '';
sub!: Subscription;
private _listFilter: string = '';
get listFilter(): string {
return this._listFilter
}
set listFilter(value: string) {
this._listFilter = value;
console.log('In setter:', value);
this.filteredProducts = this.performFilter(value);
}
filteredProducts: IProduct[] = [];
products: IProduct[] = [];
//defining the injector so it can be used in this class
constructor(private productService: ProductService) {}
//this is a method
performFilter(filterBy: string): IProduct[] {
filterBy = filterBy.toLocaleLowerCase();
return this.products.filter((product: IProduct) =>
product.productName.toLocaleLowerCase().includes(filterBy));
}
toggleImage(): void {
this.showImage = !this.showImage;
}
//this lifecycle hook retrives the data we need
ngOnInit(): void {
this.productService.getProducts().subscribe({
next: products => {
this.products = products;
this.filteredProducts = this.products;
},
error: err => this.errorMessage = err
});
}
ngOnDestroy(): void {
//this.sub.unsubscribe();
}
onRatingClicked(message: string): void {
this.pageTitle = 'Product List: ' + message;
}
}
Parent component HTML:
<div class='card'>
<div class='card-header'>
{{pageTitle}}
</div>
<div class='card-body'>
<div class='row'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text'
[(ngModel)]='listFilter' />
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h4>Filtered by: {{listFilter}} </h4>
</div>
</div>
<div class='table-responsive'>
<table class='table' *ngIf='products.length'>
<thead>
<tr>
<th>
<button class='btn btn-primary'
(click)='toggleImage()'>
{{showImage ? 'Hide' : 'Show'}} Image
</button>
</th>
<th>Product</th>
<th>Code</th>
<th>Available</th>
<th>Price</th>
<th>5 Star Rating</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let product of filteredProducts'>
<td>
<img *ngIf='showImage'
[src]='product.imageUrl'
[title]='product.productName'
[style.width.px]='imageWidth'
[style.margin.px]='imageMargin'>
</td>
<td>
<a [routerLink]="['/products', product.productId]">
{{product.productName}}
</a>
</td>
<td>{{product.productCode | convertToSpaces:'-' }}</td>
<td>{{product.releaseDate}}</td>
<td>{{product.price | currency:'AUD'}}</td>
<td><pm-star [rating]='product.starRating'
(ratingClicked)='onRatingClicked($event)'>
</pm-star></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
Product detail component file:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { IProduct } from '../product/product';
@Component({
templateUrl: './product-detail.component.html',
styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
product: IProduct | undefined;
pageTitle: any = 'Product Detail';
constructor(private route:ActivatedRoute,
private router: Router) { }
ngOnInit(): void {
const id = Number(this.route.snapshot.paramMap.get('id'));
this.pageTitle += `: ${id}`;
}
onBack(): void {
this.router.navigate(['/products']);
}
}
Child component HTML file:
<div class='card'>
<div class='card-header'>
{{pageTitle + ': ' + product?.productName}}
</div>
<br>
<div class='card-body'>
<div class='col-md-4'>Code:</div>
<div class='col-md-8'>{{product?.productCode}}</div>
</div>
<br>
<div class='card-footer'>
<button class='btn btn-outline-secondary'
style='width:80px'
(click)='onBack()'>
<i class='fa fa-chevron-left'></i> Back
</button>
</div>
</div>
Json file with a snippet of product data:
[
{
"productId": 1,
"productName": "Leaf Rake",
"productCode": "GDN-0011",
"releaseDate": "March 19, 2019",
"description": "Leaf rake with 48-inch wooden handle.",
"price": 19.95,
"starRating": 3.2,
"imageUrl": "assets/images/leaf_rake.png"
},
{
"productId": 2,
"productName": "Garden Cart",
"productCode": "GDN-0023",
"releaseDate": "March 18, 2019",
"description": "15 gallon capacity rolling garden cart",
"price": 32.99,
"starRating": 4.2,
"imageUrl": "assets/images/garden_cart.png"
},
Produce service TS file:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { catchError, Observable, tap, throwError } from "rxjs";
import { IProduct } from "./product";
@Injectable({
providedIn: 'root'
})
export class ProductService {
private productUrl = 'api/products/products.json';
constructor(private http: HttpClient) {}
getProducts(): Observable<IProduct[]> {
return this.http.get<IProduct[]>(this.productUrl).pipe(
tap(data => console.log('All: ', JSON.stringify(data))),
catchError(this.handleError)
);
}
I've just started learning, so please ignore any mistakes.
Upvotes: 1
Views: 605
Reputation: 56600
add a property in ProductService
and assign the currently clicked row data, then in the child component access the data using the same service then render the output.
in the parent component. Suppose we have a event that navigates to child. You need to set the latest value in the service like so.
parent component
openProduct(product): void {
this.productService.product = product;
this.router.navigate(['/products', product.productId]);
}
parent component html
<div class='card'>
<div class='card-header'>
{{pageTitle}}
</div>
<div class='card-body'>
<div class='row'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text'
[(ngModel)]='listFilter' />
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h4>Filtered by: {{listFilter}} </h4>
</div>
</div>
<div class='table-responsive'>
<table class='table' *ngIf='products.length'>
<thead>
<tr>
<th>
<button class='btn btn-primary'
(click)='toggleImage()'>
{{showImage ? 'Hide' : 'Show'}} Image
</button>
</th>
<th>Product</th>
<th>Code</th>
<th>Available</th>
<th>Price</th>
<th>5 Star Rating</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let product of filteredProducts'>
<td>
<img *ngIf='showImage'
[src]='product.imageUrl'
[title]='product.productName'
[style.width.px]='imageWidth'
[style.margin.px]='imageMargin'>
</td>
<td>
<a (click)="openProduct(product)">
{{product.productName}}
</a>
</td>
<td>{{product.productCode | convertToSpaces:'-' }}</td>
<td>{{product.releaseDate}}</td>
<td>{{product.price | currency:'AUD'}}</td>
<td><pm-star [rating]='product.starRating'
(ratingClicked)='onRatingClicked($event)'>
</pm-star></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
child component
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { IProduct } from '../product/product';
@Component({
templateUrl: './product-detail.component.html',
styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
product: IProduct | undefined;
pageTitle: any = 'Product Detail';
constructor(private route:ActivatedRoute,
private router: Router,
private productService: ProductService) { }
ngOnInit(): void {
const id = Number(this.route.snapshot.paramMap.get('id'));
this.pageTitle += `: ${id}`;
// get the data from the service.
this.product = this.productService.product;
}
onBack(): void {
this.router.navigate(['/products']);
}
}
product service.
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { catchError, Observable, tap, throwError } from "rxjs";
import { IProduct } from "./product";
@Injectable({
providedIn: 'root'
})
export class ProductService {
private productUrl = 'api/products/products.json';
product: IProduct | undefined;
constructor(private http: HttpClient) {}
getProducts(): Observable<IProduct[]> {
return this.http.get<IProduct[]>(this.productUrl).pipe(
tap(data => console.log('All: ', JSON.stringify(data))),
catchError(this.handleError)
);
}
Note: this solution wont work when you refresh the detail page, so ideally you need to create a separate api call, that will fetch the details of a single product based on the ID and call it on the ngOnInit
of the child component!
Upvotes: 1