I have an Angular 17 online store and I'm having problemas with BehaviourSubject not updating a components template.
Clients choose products and they are stored on a CartProduct class: id, name, num (number of elements of that product). Then everything is managed in a CartService that has a BehaviourSubject to store them:
@Injectable({
providedIn: "root",
})
export class CartService {
cartSubject: BehaviorSubject<CartProduct[]> = new BehaviorSubject<CartProduct[]>([]);
get cart$(): Observable<CartProduct[]> {
return this.cartSubject.asObservable();
}
...
}
It also has an "add" method who receives a CartProduct item. This method checks if the product is already on the cart, if the product is already on the list it just increases the "num" counter and if it is not, it adds it:
add(cartProd: CartProduct) {
const currentCart: CartProduct[] = this.cartSubject.getValue();
const ind: number = currentCart.findIndex((x: CartProduct): boolean => x.id === cartProd.id);
if (ind != -1) {
currentCart[ind].num += cartProd.num;
else {
}
currentCart.push(cartProd);
}
this.cartSubject.next(currentCart);
}
The store is composed of three components (quite a lot more, but it's not the issue :P): HomeComponent, HeaderComponent and OneProductComponent.
The HomeComponent has a header that shows a sidebar with the list of elements in the cart:
import { Component, OnInit } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-header',
template: `
<ul>
@for (product of cart; track product.id) {
<li>
{{ product.name }}
</li>
}
</ul>
`,
styleUrls: ['./header.component.css'],
})
export class HeaderComponent implements OnInit {
cart: CartProduct[] = [];
constructor(private cartService: CartService) {}
ngOnInit() {
this.cartService.cart$.subscribe(() => {
this.cart = this.cartService.cart$.value;
});
}
}
There is a list of products on the home that are called OneProductComponent, basically a picture, a name and a button to add it to the cart.
import { Component } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-one-product',
template: `
<h2>{{ product.name }}</h2>
<p>{{ product.price }} €</p>
<button (click)="addProduct()">Add product</button>
`,
styleUrls: ['./one-product.component.css'],
})
export class OneProductComponent {
constructor(private cartService: CartService) {}
addProduct() {
this.cartService.add({
id: 1,
name: 'Product 1',
price: 10,
num: 1,
});
}
}
The problem is that when I click the button on the component, it doesn't reflect on the header. If I navigate somewhere else the header updates correctly and shows the cart, but the moment I hit the add button it just does nothing.
I have also added this after the "add" call to the service:
console.log(this.cartService.cartSubject.value);
And I see that the product is correctly added to the cart. But nothing happens. I also tried adding an EventEmitter on the cart's add method and subscribing to it on the header:
CartService:
public productAdded$: EventEmitter<void>;
add(product: CartProduct) {
...
this.cartSubject.next(currentCart);
this.productAdded$.emit();
}
HeaderComponent:
constructor(cartService: CartService) {
this.cartService.productAdded$.subscribe((): void => { console.log('product added'); });
}
And also nothing happens. It looks like it subscribes correctly on load and the cart is shown, but when I do any change the template doesn't update.
Edit: I have added an "x" on each product on the list to remove them. I see the list, I add something (the list is not updated), I remove something and the list updates removing the element and showing the one I added! Looks like headers template is only being refreshed when actions are done on the template!
Thanks!
Found it!! The problem was that I was loading the CartService as a provider globally on startup, but I also had a "providers" array on HeaderComponent and on the OneProductComponent. I just removed that array from the components... and now it works flawlessly :)
Thanks for the help bmtheo!