Having trouble transferring information between two components in Angular version 14

Having recently delved into the world of Angular, I'm grappling with the challenge of passing data from a parent component to a child component. On my product listing page, clicking on a product should route to the product detail page, but I can't seem to get the product details to display.

In Component 1, all the product data is stored, whereas Component 2 is responsible for routing to a new page where the product data is displayed.

Within the 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 structure:

<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>

The 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 structure:

<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>

A snippet of product data from a JSON file:

[
    {
      "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"
    },

The ProductService TypeScript 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 am just beginning my journey of learning, so please overlook any errors.

Answer №1

Implement a property in the ProductService to store the data of the currently selected row. Access this data in the child component using the same service to display the output.

In the parent component, if there is an event that triggers navigation to the child component, ensure to update the value in the service as shown below:

Parent Component Code

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 Code

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}`;
    // Retrieve and display the data from the service.
    this.product = this.productService.product;
  }
    
  onBack(): void {
    this.router.navigate(['/products']);
  }
}

Product Service Code

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: It's important to note that this solution may not work when refreshing the detail page. It is recommended to create a separate API call to fetch details of a single product based on the ID and make this call within the ngOnInit of the child component!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Best practices and distinctions when it comes to typing in TypeScript functions

Do the typings below differ in any way, or are they essentially the same with personal preference? interface ThingA{ myFunc(): number; } interface ThingB{ myFunc: () => number; } ...

Trigger a class method in an event using Angular with Typescript

I am completely new to TypeScript and Angular, and I am attempting to create a basic drawing component on a canvas. However, I have reached a point where I feel lost and confused about my code. The concept of "this" in TypeScript has been a major stumbling ...

Navigating with firebase authentication and angular routing

Currently, I am in the process of developing an ionic app using version 4. For this project, I have decided to utilize firestore as my database and implement firebase-authentication for user login and registration functionalities. However, I have encounter ...

Defining optional parameters in TypeScript

Currently, I am working on implementing strong typing for a flux framework (specifically Vuex). Here is my current code: const actions = { first(context: Context, payload: string) { return doSomething(context, payload); }, second(context: Context) { r ...

Unable to delete event listeners from the browser's Document Object Model

Issue at hand involves two methods; one for initializing event listeners and the other for deleting them. Upon deletion, successful messages in the console confirm removal from the component's listener array. However, post-deletion, interactions with ...

There are several InputBase elements nested within a FormControl

There seems to be an issue: Material-UI: It appears that there are multiple InputBase components within a FormControl, which is not supported. This could potentially lead to infinite rendering loops. Please only use one InputBase component. I understand ...

Exploring the DynamoDB List Data Type

Currently, I am working on an angular 8 application where I have chosen to store JSON data in a list data type within DynamoDB. Inserting records and querying the table for data has been smooth sailing so far. However, I have run into some challenges when ...

The 'substr' property is not found in the type 'string | string[]'

Recently, I had a JavaScript code that was working fine. Now, I'm in the process of converting it to TypeScript. var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; if (ip.substr(0, 7) == "::ffff ...

Unable to attach to 'aria-valuenow' as it's not recognized as a property of 'div'

I encountered an issue when attempting to assign an expression to an element. The problem arose with the following code: <div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="{{MY_PREC}}" aria-valuemin="0" aria-value ...

What is the recommended TypeScript type for the NextJS _app.tsx Component and pageProps?

Take a look at the default _app.tsx code snippet from NextJS: function MyApp({ Component, pageProps }) { return ( <Component {...pageProps} /> ) } The issue arises when transitioning to TypeScript, as ES6Lint generates a warning indicating t ...

Oops! Issue encountered while trying to read the file "src/core/database/config.ts"

Need help with migrating a database in a Node Nest.JS application. When running the npx sequelize-cli db:migrate shell command, I encountered the following exception: Error details: Error: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".t ...

Efficient method to access two arrays simultaneously and combine them into an associative array in JavaScript

When using Ajax to return a table, you have the option of separating column names and row values. Here are two ways to do it: let columns = ["col1", "col2", "col3"]; let rows = [ ["row 1 col 1", "row 1 col 2", "row 1 col 3"] , ["row 2 col 1", "r ...

Union does not contain the specified property in Typescript

Here are the types that I have: Foo { foobar: any } Bar { fooBarBar: any; } I want to use a function defined like this: this.api.submit(param: Foo | Bar) When trying to use it, I encountered an issue: this.api.submit(param.foobar) // does no ...

What could be the reason behind the error related to react-router-dom?

index.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <React.S ...

Storing Array Data in Ionic Using Native Storage - A Step-by-Step Guide

I want to implement a feature in my app where I can store translation history using Ionic Native Storage. Every time a word is translated, the translation action (date, translated word) will be saved in the storage. Then, when I navigate to the history pag ...

The 'Component' binding element is assumed to have an unspecified 'any' type in TypeScript

Hello, I'm new to coding and encountering an issue with the code below. My goal is to create a protected "Dashboard" page, but I keep getting a binding error that says: Binding element 'Component' implicitly has an 'any' type.ts( ...

The child component is receiving null input data from the Angular async pipe, despite the fact that the data is not null in the

I encountered a strange scenario that I'm unable to navigate through and understand how it occurred. So, I created a parent component called SiteComponent. Below is the TypeScript logic: ngOnInit(): void { this.subs.push( this.route.data.subscribe( ...

Looking for a Webpack setup for a React Components Library that includes Typescript, SASS, CSS Modules, and SASS support

I'm on the lookout for a functional Webpack configuration tailored for a React Components Library that includes TypeScript, SASS, and CSS Modules with SASS support. If anyone has one they'd like to share, I would be extremely grateful! ...

The attribute 'X' is not found in the type 'HTMLAttributes<HTMLDivElement>'.ts(2322)

Encountered an issue using JSX sample code in TSX, resulting in the following error: (property) use:sortable: true Type '{ children: any; "use:sortable": true; class: string; classList: { "opacity-25": boolean; "transition-tr ...

What steps can I take to allow a third-party JavaScript to access my TypeScript object?

I am working on an Angular application and I have a requirement to develop an API for a third-party JavaScript that will be injected dynamically. public class MyApi{ public callSomeFunction():void{...} public getSomeValue():any {...} } var publicA ...