1

I have an injectable service that retrieves shopping cart items:

@Injectable({
  providedIn: 'root'
})
export class CartService {
  private readonly items: Array<{ product: IProduct, quantity: number }>
  private subject = new Subject<Array<{ product: IProduct, quantity: number }>>()

  constructor() {
    this.items = []
  }

  getItems() {
    return this.subject
  }

  addItem(product: { product: IProduct, quantity: number }) {
    const isProductExists = this.items.find((item => item.product._id === product.product._id))

    if (isProductExists) {
      isProductExists.quantity += product.quantity
    } else {
      this.items.push(product)
    }

    this.subject.next(this.items)
  }
}

In my CartComponent i subscribe to items and iterate them in an *ngFor:

export class CartComponent implements OnInit {
  myItems: { product: IProduct, quantity: number }[]

  constructor(private cartService: CartService) {
  }

  ngOnInit(): void {
    this.cartService.getItems().subscribe(items => {
      this.myItems = items
    })
  }
}

Here is the template:

<div *ngFor="let item of myItems">{{item.product.name + ', ' + item.product.price }}</div>

The thing is that nothing is displayed on the page, even though the subscribe method is invoked and I can clearly see that myItems is updating.

what I do wrong in this code?

Two Horses
  • 1,401
  • 3
  • 14
  • 35
  • do you have 'ChangeDetectionStrategy.OnPush' – Chris Apr 17 '20 at 18:04
  • I wonder if it's an issue related to the `this.myItems = items` is potentially setting the array to the same array it's already set to. In which case, there wouldn't be any change. The only change would be to the nested elements in the array – Taplar Apr 17 '20 at 18:05
  • no, I have the default one – Two Horses Apr 17 '20 at 18:05
  • why not use async pipe? – Chris Apr 17 '20 at 18:06
  • I have created a prototype of your project, but in my case everything is working. Take a look please the code: https://stackblitz.com/edit/angular-enszba – Danil Sabirov Apr 17 '20 at 18:13
  • You can apply for async pipe, here is the link for documentation https://angular.io/api/common/AsyncPipe Also, I recommend you to check this issue https://stackoverflow.com/questions/44921788/what-is-subscribe-in-angular/51935993 – emredmrcn Apr 17 '20 at 18:13
  • if one of the parent components is set to onPush, it won't work here – Chris Apr 17 '20 at 18:24
  • I used an async pipe but still, the problem exists. but I found that if I change the `Subject` to a `BehaviorSubject` it works now, no idea why (this works regardless if I use an async pipe or not). Does anyone know why it works? – Two Horses Apr 17 '20 at 18:38

2 Answers2

1

I added an OrderItem interface to make things a little easier to read:

export interface OrderItem {
  product: IProduct; 
  quantity: number;
}

I've updated the CartService to use BehaviorSubject, and hold the orderItems as an observable, and changed the getItems to return the observable. Also change addItem, to update the observable :

export class CartService {
  private subject = new BehaviorSubject<OrderItem[]>([]);
  private orderItems$: Observable<OrderItem[]> = this.subject.asObservable();

  constructor() {
  }

  getItems(): Observable<OrderItem[]>{
    return this.orderItems$;
  }

  addItem(orderItem: OrderItem) {
    const orderItems = this.subject.getValue();
    const productIndex = orderItems.findIndex(item => item.product._id === orderItem.product._id);
    if(productIndex >= 0){
      const updatedOrderItem = orderItems[productIndex];
      updatedOrderItem.quantity +=1;
      const newOrderItems = orderItems.slice(0);
      newOrderItems[productIndex] = {
        ...orderItems[productIndex],
        ...updatedOrderItem
      }
    } else {
      orderItems.push(orderItem)
    }

    this.subject.next(orderItems);
  }

In the cart component, just returning the observable in the service:

export class CartComponent implements OnInit {
  myOrderItems$: Observable<OrderItem[]>;

  constructor(private cartService: CartService) { }

  ngOnInit() {
    this.myOrderItems$ = this.cartService.getItems();
  }

}

In the HTML, a simple ngFor and Async pipe:

<table>
    <thead>
        <tr>
            <th>Product</th>
            <th>Price</th>
            <th>Quantity</th>
            <th>Total</th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let item of myOrderItems$ | async">
            <td>{{item.product.name}}</td>
            <td>{{item.product.price | currency : 'GBP'}}</td>
            <td>{{item.quantity}}</td>
            <td>{{item.quantity * item.product.price | currency : 'GBP'}}</td>
        </tr>
    </tbody>
</table>

and as a simple test, in the app.component:

export class AppComponent implements OnInit {
  name = 'Angular';

  constructor(private cartService: CartService){}

  ngOnInit(){
    this.addProduct1();
  }

  addProduct1(){
    let product1: IProduct = {_id:1, name: 'Product 1', price:25.00};
    let orderItem: OrderItem = { product: product1, quantity:1};
    this.cartService.addItem(orderItem);
  }

  addProduct2(){
    let product: IProduct = {_id:2, name: 'Product 2', price:15.00};
    let orderItem: OrderItem = { product: product, quantity:1};
    this.cartService.addItem(orderItem);
  }


}

and the HTML:

<app-cart></app-cart>

<button (click)="addProduct1()">Add Product 1</button>
<button (click)="addProduct2()">Add Product 2</button>

I've created a working example on stackblitz: https://stackblitz.com/edit/angular-rxjs-shopping-cart-example

John McArthur
  • 916
  • 1
  • 12
  • 30
0

In order to have no memory leaks and better clean code, use async pipe.

this.myItems$ = this.cartService.getItems();


<div *ngFor="let item of myItems$ | async">
  {{item.product.name }}, {{ item.product.price }}
</div>

you subscribe to the service in component after the event has been emitted in service. Why does it work with BehaviorSubject? because he remembers the last value

Subject

getItems  -----x----x->
component        ---x->

BehaviorSubject

getItems  -----x----x->
component        x--x->

it is correctly to return asObservable()

getItems() {
  return this.subject.asObservable()
}
Chris
  • 2,117
  • 13
  • 18