3

This is the parent component: I passed all the data from the parentComponent to main-page Component.

import { Component, OnInit } from '@angular/core';
import { Product } from '../Model/Product';
import { ProductService } from '../ProductsService/product.service';

@Component({
    selector: 'app-parent',
    templateUrl: './parent.component.html',
    styleUrls: ['./parent.component.css'],
})
export class ParentComponent implements OnInit {
    products: Product[] = [];
    cartList: Product[] = [];
    constructor(private productService: ProductService) {
        this.products = this.productService.getProducts();
    }

    ngOnInit(): void {}

    addCart(product: Product) {
        this.cartList.push(product);
    }
}

(TEMPLATE)

<app-main-page [products]="products" (addCart)="addCart($event)"></app-main-page>
<app-cart-list [cartList]="cartList"></app-cart-list>
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { ProductService } from '../../ProductsService/product.service';
import { Product } from '../../Model/Product';

@Component({
    selector: 'app-main-page',
    templateUrl: './main-page.component.html',
    styleUrls: ['./main-page.component.css'],
})
export class MainPageComponent {
    @Input() products: Product[] = [];
    @Output() addCart: EventEmitter<Product> = new EventEmitter();
    constructor(private productService: ProductService) {
        this.products = this.productService.getProducts();
    }

    addToCartList(product: Product) {
        this.addCart.emit(product);
        console.log(product);
    }
}

(TEMPLATE) As you can notice that there's a click button in which I emitted this method to the parent so I can pass its value to another child component.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <section>
            <div class="products">
                <ul *ngFor="let product of products">
                    <img src="{{ product.img }}" alt="store pictures" />
                    <li>{{ product.name }}</li>
                    <li>{{ product.type }}</li>
                    <li>{{ product.available }}</li>
                    <li>{{ product.price }}</li>
                    <button (click)="addToCartList(product)">Add to Cart</button>
                </ul>
            </div>
        </section>
    </body>
</html>
import { Component, Input, OnInit } from '@angular/core';
import { Product } from 'src/app/Model/Product';
@Component({
    selector: 'app-cart-list',
    templateUrl: './cart-list.component.html',
    styleUrls: ['./cart-list.component.css'],
})
export class CartListComponent implements OnInit {
    constructor() {
        console.log(this.cartList);
    }
    @Input() cartList: Product[] = [];
    ngOnInit(): void {}
}

I cannot use any value in cartList, why?

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
  • A good example on how to solve this with subjects, I would suggest you look into this https://stackoverflow.com/a/72187511/9349240 – ak.leimrey Jun 16 '22 at 01:41
  • It get the data inside of the Service class component, it contains data and I use getProducts() method to get it. I tried to remove the service classs from the main component and it gave me an error. –  Jun 16 '22 at 03:23
  • @Omar, sorry, I deleted my previous comment as I answered your question below. – Chris Hamilton Jun 16 '22 at 03:27

4 Answers4

1

Input variables, event emitters, and RxJS just complicate the problem here. All you need is a simple Angular service.

Here is a stackblitz: https://stackblitz.com/edit/angular-ivy-a6ub1h?file=src/app/app.component.html


Your parent component doesn't need any typescript, all it needs to do is instantiate the other components via html:

Parent Component

<app-main-page></app-main-page>
<app-cart-list></app-cart-list>

I'll make a product service to simulate your app, although I'm not sure exactly what your service looks like. Products will just have a name for simplicity.

Product Service

export type Product = {
  name: string;
};

@Injectable({ providedIn: 'root' })
export class ProductService {
  getProducts(): Product[] {
    return [
      { name: 'product1' },
      { name: 'product2' },
      { name: 'product3' },
      { name: 'product4' },
      { name: 'product5' },
      { name: 'product6' },
      { name: 'product7' },
      { name: 'product8' },
    ];
  }
}

And I'll make a service to hold the cart list items, we'll have add and delete functionality.

Cart Service

@Injectable({ providedIn: 'root' })
export class CartService {
  cartList: Product[] = [];

  addToCart(product: Product) {
    this.cartList.push(product);
  }

  deleteFromCart(index: number) {
    this.cartList.splice(index, 1);
  }
}

Main page just gets the products and can add them to the cart.

Main Page

export class MainPageComponent implements OnInit {
  products: Product[] = [];

  constructor(
    private prodService: ProductService,
    private cartService: CartService
  ) {}

  ngOnInit() {
    this.products = this.prodService.getProducts();
  }

  addToCart(product: Product) {
    this.cartService.addToCart(product);
  }
}
<h1>Main Page</h1>
<ng-container *ngFor="let product of products">
  <span>{{ product.name }}&nbsp;</span>
  <button (click)="addToCart(product)">Add to Cart</button>
  <br />
</ng-container>

Cart component shows the cart items and can delete them

Cart List

export class CartListComponent {
  constructor(private cartService: CartService) {}

  get cartList() {
    return this.cartService.cartList;
  }

  delete(index: number) {
    this.cartService.deleteFromCart(index);
  }
}
<h1>Cart</h1>
<ng-container *ngFor="let product of cartList; index as i">
  <span>{{ product.name }}&nbsp;</span>
  <button (click)="delete(i)">Delete</button>
  <br />
</ng-container>
Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
0

The best way to communicate data between components is using a service, it will help a lot with avoiding this type of issue you are facing

I suggest you create a CartService:

@Injectable()
export class CartService {
  cart$ = new BehaviorSubject<Product[]>([]);

  add(product: Product) {
    this.cart$.pipe(take(1)).subscribe(items => {
      this.cart$.next([...items, product])
    })
  }

  clear() { 
   this.cart$.next([])
  }
}

From the cart component you then port the cart to its view:

cart$ = this.cartService.cart$

and update your view to handle this for the cart items:

<ul *ngFor="let product of cart$ | async">
    <img src="{{ product.img }}" />
    <li>{{ product.name }}</li>
    <li>{{ product.type }}</li>
    <li>{{ product.available }}</li>
    <li>{{ product.price }}</li>
</ul>

From the products component you should change your addToCartList function to:

addToCartList(product) {
  this.cartService.add(product)
}
The Fabio
  • 5,369
  • 1
  • 25
  • 55
  • Not sure why you use a behavior subject here and the way you use it is a headscratcher. That way a component that might use the service elsewhere will keep subscribing when calling the add method with no way to eventually unsubscribe from it. If you don't want to hold data, then you should use a Subject. The async pipe unwraps an observable anyway. Also, it's better to expose a subject as an observable for the component that wants to listen to the changes happening. This looks like codesmell to me – ak.leimrey Jun 16 '22 at 01:11
  • 1. You're half wrong with your answer. Yes, while the subject complies with the observable interface, your implementation still exposes the subject itself, which according to RxJs best practises is a nono and has been discussed in the last plenty of time. Yes, you don't have to do it, but most developers chose to not expose subjects themselves. https://stackoverflow.com/a/52223313/9349240 – ak.leimrey Jun 16 '22 at 01:29
0

If you're wondering why you're seeing an empty array inside your CartListComponent console log, that is because the input of a component is set during component initialization and the constructor runs before that. So if you want to check if the @Input() cartList is being set properly or not, you should log it out inside OnInit hook:

export class CartListComponent implements OnInit {
    @Input() cartList: Product[] = [];
    ngOnInit(): void {
      console.log(this.cartList);
    }
}

You can use a setter to log it as well

And I think there's a good chance that your component won't re-render properly when you try to add an item to a cart because when it comes to array or object as an @Input, Angular detect changes by comparing their references and not value. So when you perform this.cartList.push(product), you're not changing the array reference but rather its value. To get around this, you can try assigning a brand new array to the variable by copying over the old array

this.cartList.push(product);
this.cartList = [...this.cartList];

A better approach to solve this problem would be to create a CartService. This would simplify everything by centralizing the logic around cart to a single place rather than trying to create a messy Parent -> Child -> Parent communication.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

interface Product {}

interface Cart {
  items: Product[];
  someOtherProperty?: any;
}

@Injectable({
  providedIn: 'root',
})
export class CartService {
  private _cartSubject: BehaviorSubject<Cart> = new BehaviorSubject<Cart>({
    items: [],
  });
  cart$: Observable<Cart> = this._cartSubject.asObservable();

  addToCart(item: Product) {
    const oldCart: Cart = this._cartSubject.getValue();

    const newCart: Cart = { ...oldCart, items: [...oldCart.items, item] };

    this._cartSubject.next(newCart);
  }
}

The next step is just to inject and use the service in your component: constructor(private cartService: CartService){}

  • To get access the list of cart items:
cartItems$ = this.cartService.cart$.pipe(map((cart) => cart.items));
  • To add something to the cart:
this.cartService.add(someProduct);
Nam
  • 554
  • 4
  • 11
0

I also suggest a CartService, and am taking The Fabio up on his suggestion to provide a better answer that doesn't expose Subject.next outside of allowed logic. For example, you might want to change CartService to ensure the array of products never contain duplicates, and you can't guarantee that when any consuming code can sidestep it via Subject.next!

// Imutable array of Product objects
type Products = ReadonlyArray<Readonly<Product>>;

@Injectable()
export class CartService {
  private _products$: BehaviorSubject<Products> = new BehaviorSubject([]);

  readonly products$ = this._products$.asObservable();

  addProduct(product: Product) {
    // A new, readonly array object
    const products: Products = [
      ...this._products$.getValue(),
      product
    ];
    this._products$.next(products);
    return products;
  }

  clear(): void { 
    this._products$.next([]);
  }
}

Also, to help ensure that nobody gets the array of products and changes it everywhere, it's typed as a read only array. You will find this annoying when you just want to be quick, but making your arrays immutable will be protecting yourself from entire categories of potential bugs.

JSmart523
  • 2,069
  • 1
  • 7
  • 17