1

I have 2 classes:

export class Enrollment {
    private _productId = signal(0);
    private _price = signal(0);
    
    public productId = this._productId.asReadonly();
    public price = this._price.asReadonly();

    public setProductId(productId: number): void {
        this._productId.set(productId);
    }

    public setPrice(price: number): void {
        this._price.set(price);
    }
}

export class Cart {
    private _enrollments = signal<Enrollment[]>([]);
    
    public enrollments = this._enrollments.asReadonly();

    public addToCart(enrollment: Enrollment): void {
        this._enrollments.set([...this._enrollments(), enrollment]);
    }

    public removeFromCart(enrollment: Enrollment): void {
        this._enrollments.set(this._enrollments().filter(e => e !== enrollment));
    }

    public emptyCart(): void {
        this._enrollments.set([]);
    }
}

I'm looking for the best way to update data within a specific enrollment in the _enrollments array.

There are two options I see:

  • Mutate Enrollment objects directly: I could define an equals function in the Enrollment class. This would allow me to modify enrollments directly using public methods, which would trigger any dependent signals relying on enrollment data changes.
  • Create immutable Enrollment updates: I could write all enrollment manipulation logic within the Cart service and make those methods immutable. This wouldn't directly mutate existing enrollments, but it feels less ideal.

Is there a better approach that maintains immutability and adheres to separation of concerns principles?

1 Answer 1

1

This tutorial Update an Angular Signal's Value and Make Computed Signal Emit Updates really helped me work on this problem.

Key points to keep in mind:

  • Use the set method for updating primitive values like strings or numbers
  • Use the update method with a callback that returns a new object or array when updating complex values
  • Avoid mutating signal values directly

So, we can use update when updating complex objects inside a signal and the Html will be updated with the changes. When working with simple primitives, set method is enough to notify the change.

After much effort, the update method can be structured like so.

from component it will be

  updateCartItem(enrollment: Enrollment, name: string, price: number) {
    enrollment.setName(name);
    enrollment.setPrice(price);
    enrollment.setEdit(false);
    this.cart.updateCartItem(enrollment);
  }

THen the class can be used to update the signal list using update

  public updateCartItem(enrollment: Enrollment) {
    this._enrollments.update((enrollments: Enrollment[]) => {
      const foundEnrollmentIndex: number = enrollments.findIndex(
        (item: any) => item.productId === enrollment.productId()
      );
      enrollments[foundEnrollmentIndex] = enrollment;
      return enrollments;
    });
  }

Full Code:

import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';

export class Enrollment {
  private _productId = signal(0);
  private _price = signal(0);
  private _name = signal('');
  private _edit = signal(false);

  public productId = this._productId.asReadonly();
  public price = this._price.asReadonly();
  public name = this._name.asReadonly();
  public edit = this._edit.asReadonly();

  public setProductId(productId: number): void {
    this._productId.set(productId);
  }

  public setPrice(price: number): void {
    this._price.set(price);
  }

  public setName(name: string): void {
    this._name.set(name);
  }

  public setEdit(edit: boolean): void {
    this._edit.set(edit);
  }
}

export class Cart {
  private _enrollments = signal<Enrollment[]>([]);

  public enrollments = this._enrollments.asReadonly();

  public addToCart(enrollment: Enrollment): void {
    this._enrollments.update((prev: Enrollment[]) => [...prev, enrollment]);
  }

  public removeFromCart(enrollment: Enrollment): void {
    this._enrollments.update((prev: Enrollment[]) =>
      this._enrollments().filter((e) => e !== enrollment)
    );
  }

  public emptyCart(): void {
    this._enrollments.set([]);
  }

  public updateCartItem(enrollment: Enrollment) {
    this._enrollments.update((enrollments: Enrollment[]) => {
      const foundEnrollmentIndex: number = enrollments.findIndex(
        (item: any) => item.productId === enrollment.productId()
      );
      enrollments[foundEnrollmentIndex] = enrollment;
      return enrollments;
    });
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule],
  template: `
  <form>
    <div><input #name/></div>
    <div><input #price type="number"/></div>
    <div><button (click)="addToCart($any(name).value, $any(price).value)">Add New Item</button></div>
</form>
    @for(enrollment of cart.enrollments(); track $index) {
        <div> 
      @if(!enrollment.edit()) {
        productId: {{enrollment.productId()}} | name: {{enrollment.name()}} | price: {{enrollment.price()}}$     
        <button (click)="editEnrollment(enrollment)">edit</button>
        <button (click)="removeFromCart(enrollment)">remove</button>
      } @else {
        <div><input [value]="enrollment.name()" #nameEdit/>
        <input [value]="enrollment.price()" #priceEdit type="number"/></div>
        <button (click)="updateCartItem(enrollment, nameEdit.value, +priceEdit.value)">update</button>
      }
      </div>
    }
  `,
})
export class App {
  cart = new Cart();
  name = '';
  price = 0;

  removeFromCart(enrollment: Enrollment) {
    this.cart.removeFromCart(enrollment);
  }

  addToCart(name: string, price: number) {
    console.log(name, price);
    if (!name && !price) {
      alert('name and price needed');
      return;
    }
    const newEnrollment = new Enrollment();
    newEnrollment.setProductId(Math.round(Math.random() * 10000));
    newEnrollment.setName(name);
    newEnrollment.setPrice(price);
    this.cart.addToCart(newEnrollment);
    this.name = '';
    this.price = 0;
  }

  editEnrollment(enrollment: Enrollment) {
    enrollment.setEdit(true);
    this.cart.updateCartItem(enrollment);
  }

  updateCartItem(enrollment: Enrollment, name: string, price: number) {
    enrollment.setName(name);
    enrollment.setPrice(price);
    enrollment.setEdit(false);
    this.cart.updateCartItem(enrollment);
  }
}

bootstrapApplication(App);

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

1 Comment

This updateCartItem directly mutates an element of the array, this is a bad practice when working with signals.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.