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

Adding a method to an object with TypeScript: A step-by-step guide

In my current scenario, I am faced with a challenge where I need to test a function with a specific use of this. However, typescript poses constraints by either disallowing me from adding the method to the object, or if I define it as any, then my interfac ...

Troubleshoot Issue with Installation of angular2-jwt.js and Provide Solutions for Error Code

I am currently following the installation guidelines to install Respond CMS from Github. My progress has hit a snag while attempting to copy files to the public folder using gulp. and it's the third command in step 2. This brief error message aris ...

Angular's HostListener triggers twice when the browser's back button is clicked

In my Angular application, I have implemented a @HostListener that triggers when the back button on the browser is clicked. However, I have noticed that the event handler seems to be firing twice - prompting two dialogue boxes when clicking 'Ok'. ...

In Angular, upon submitting the form, redirect to a separate component and display a message indicating the successful

I am facing an issue with a component that sends data to a service in order to save it in the database. Following this, I want to redirect or navigate to a different component and display a success message there. While I have implemented this functionality ...

Is the component not being initialized when navigating through the router, but only when the browser is refreshed?

I have noticed that when I navigate using the router, the data in the page does not update. However, if I refresh the browser, the updated data is shown in the router page. I am looking for a way to reload only the component without refreshing the entire ...

Tips for customizing the default name of the 'Page View' feature

After incorporating Angulartics2GoogleTagManager into the app.component, is it feasible to modify the default name of the tracked page view event from "Page View"? I require a different name than the current one. ...

Protecting a client's encryption key

I was given the task of enhancing the security of a website that utilizes Angular v15 + JWT. The first step was to alter the login POST-request (HTTPS) from this format: /api/login?username=user_name&password=pass123 to this format: /api/login?credent ...

Is there a method to define an 'internal' property within a TypeScript type?

I created a custom 'library' in Angular and TypeScript. This library is distributed as a *.ts package within other Angular workspaces. Within this library, I have an exported class that contains various properties. One specific property in thi ...

The call to the hooks is not valid. Hooks must be called within the body of a functional component

Could you please take a moment to review the validate method within the elfe-if condition in the code snippet below? I am encountering an issue when trying to invoke the useLocation method from react-router-dom. Upon researching online, I came across simil ...

Is the slow loading time of the Dev browser due to the large size of the vendor.js file

When using Angular 8.0, my browser takes around 15 seconds to load with ng serve. However, the browser only takes about 4 seconds to load when using ng serve --prod. One of the main reasons for the slow loading time in development is a vendor.js file tha ...

Navigating nested data structures in reactive forms

When performing a POST request, we often create something similar to: const userData = this.userForm.value; Imagine you have the following template: <input type="text" id="userName" formControlName="userName"> <input type="email" id="userEmail" ...

The successful operation of 'Ionic serve --lab' may involve the need to manually save the app.module

We are currently working on an Ionic project that consists of several pages and a single custom provider named request.ts. The issue we are facing is that whenever we launch it using the command ionic serve --lab, the compilation fails (with the error poin ...

How to access data within nested objects using ngFor in Ionic 4

How can I access the data I need from an API when it is nested within multiple objects? I am attempting to retrieve the full_url from the data object, which is nested inside the avatar object. I have already attempted the following: <ion-list> ...

Exploring the Power of Angular 6 Library Components with Stylish SCSS

I have recently been working on expanding my Angular 6 library, and I must say, the new process is quite impressive. One of the tasks involved in this project was taking modules and components from a previous version and updating them. These components we ...

Unable to pass response from httpclient post method to another custom function in Angular 4

I've implemented the addUser(newUser) function in my sign-in.service.ts file like this: addUser(newUser) { const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; let body = JS ...

Issue encountered with Typescript and Mongoose while operating within a Kubernetes cluster environment with Skaffold configuration

Here is the code snippet, const userSchema = new mongoose.Schema({ email: { type: String, required: true, }, password: { type: String, required: true, }, }); console.log(userSchema); userSchema.statics.build = (user: UserAttrs) =& ...

Getting parameter names (or retrieving arguments as an object) within a method decorator in TypeScript: What you need to know

I am currently working on creating a method decorator that logs the method name, its arguments, and result after execution. However, I want to implement a filter that allows me to choose which parameters are logged. Since the number and names of parameter ...

Angular's HTTP client allows developers to easily make requests to

I am struggling with grasping the concept of async angular http client. In my Java backend, I have endpoints that return data in the same format but with different meanings. To handle this, I created the following code: export class AppComponent implement ...

Tips for utilizing array.items in joiful validation?

Can someone provide an example code or a link on how to correctly use the joyful validation for array items? I attempted the array.items validation code using joyful, but I am not sure how to specify the items. Thanks in advance! ...

Mastering the incorporation of Context in React with Typescript

I am currently in the process of setting up a context provider for my Next.js application using TypeScript. Although I have previously set up a context provider in React using plain JavaScript, this time I am delving into learning TypeScript. In the code ...