javascriptangulartypescriptangular-changedetectionangular-signals

Angular signals: how to update an item within an array which is the property of an object?


Perhaps I'm trying to use too-complex signals, let me know if what I am trying to achieve is just not a good use-case for Angular's Signals and I guess I'll try a different approach, but I feel like this should be do-able!

I have a service which is managing the state of an order - the order class contains some properties, one of those properties is an array of OrderItems.

I have created a signal for the Order PackingOrder = signal<order>(orderFromDB); Which I can access just fine, but I am unable to figure out how to update an OrderItem within the order.

Given a Signal containing an Order object built using classes as below, how would one update, say, a specific OrderItem's 'packed' property (given the OrderItem's id)?

 export class Order {
  id: number; 
  customer: string;
  orderItems: OrderItem[];
}

 export class OrderItem {
  id: number; //USING THIS TO IDENTIFY THE ITEM...
  produceName: string;
  unitQty: number;
  container: string;
  yield: number;
  packed: number; //<--HOW TO UPDATE THIS?
  complete: boolean;
}

I have searched for a solution but everything I have found has been targeting an array itself as the signal, or a simpler object, I've been unable to extend those answers into a working solution for this case, so here I am!


Solution

  • So I am guessing the problem is that when you clone a class using object destructuring the class instance is lost and you want to preserve this.


    PackingOrder = signal<Order>(hardCodedOrder);
    
    constructor() {
      effect(() => {
        console.log('order changed', structuredClone(this.PackingOrder()));
      });
    }
    
    ngOnInit() {
      setTimeout(() => {
        this.PackingOrder.update((order: Order) => {
          const foundItem = order.orderItems.find(
            (item: OrderItem) => item.packed === 2
          );
          if (foundItem) {
            foundItem.unitQty = 10000;
            // if updated trigger a change in the signal using object destructuring.
            return order.clone();
          }
          return order;
        });
      }, 5000);
    }
    

    The approach I follow is when you want to update something inside the signal, you should use update method which gives us the state of the order conveniently.

    Then we find that particular order item using find, check if it exists and update the property (update packed to 10000).


    Note: Before diving in to how we trigger the signal update, just want to highlight that non primitive datatypes like object and array are stored as references in memory, and signal detects a change only when the actual value has changed. So the memory location must change.


    So I have to create a new class memory, without loosing the class prototype. For this I looked up this question:

    How to clone a javascript ES6 class instance

    I took their ideas and created a method called clone inside the class instance. Which creates a new instance with the same data.

    export class Order {
      id: number; //USING THIS TO IDENTIFY THE ITEM...
      customer: string;
      orderItems: OrderItem[];
    
      constructor(id: number, customer: string, orderItems: OrderItem[] = []) {
        this.id = id;
        this.customer = customer;
        this.orderItems = orderItems;
      }
    
      clone() {
        const clone = Object.assign({}, this);
        return Object.setPrototypeOf(clone, Order.prototype);
        // return new Order(this.id, this.customer, this.orderItems);
      }
    

    Now we have the clone method, we can call it inside the update, which triggers the signal refresh after the data is updated.

    I am using structuredClone so that we will get the proper snapshot of the data updated in chrome console.

    Full Code:

    import { Component, effect, signal } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    
    export class Order {
      id: number; //USING THIS TO IDENTIFY THE ITEM...
      customer: string;
      orderItems: OrderItem[];
    
      constructor(id: number, customer: string, orderItems: OrderItem[] = []) {
        this.id = id;
        this.customer = customer;
        this.orderItems = orderItems;
      }
    
      clone() {
        const clone = Object.assign({}, this);
        return Object.setPrototypeOf(clone, Order.prototype);
        // return new Order(this.id, this.customer, this.orderItems);
      }
    }
    
    export class OrderItem {
      id: number;
      produceName: string;
      unitQty: number;
      container: string;
      yield: number;
      packed: number; //<--HOW TO UPDATE THIS?
      complete: boolean;
    
      constructor(
        id: number,
        produceName: string,
        unitQty: number,
        container: string,
        yieldVal: number,
        packed: number,
        complete: boolean
      ) {
        this.id = id;
        this.produceName = produceName;
        this.unitQty = unitQty;
        this.container = container;
        this.yield = yieldVal;
        this.packed = packed;
        this.complete = complete;
      }
    }
    
    const hardCodedOrder = new Order(1, 'tester', [
      new OrderItem(1, 'tester product 1', 1, 'tin', 15, 1, true),
      new OrderItem(2, 'tester product 2', 1, 'tin', 16, 2, false),
    ]);
    
    @Component({
      selector: 'app-root',
      template: `
        <a target="_blank" href="https://angular.dev/overview">
          Learn more about Angular
        </a>
      `,
    })
    export class App {
      PackingOrder = signal<Order>(hardCodedOrder);
    
      constructor() {
        effect(() => {
          console.log('order changed', structuredClone(this.PackingOrder()));
        });
      }
    
      ngOnInit() {
        setTimeout(() => {
          this.PackingOrder.update((order: Order) => {
            const foundItem = order.orderItems.find(
              (item: OrderItem) => item.packed === 2
            );
            if (foundItem) {
              foundItem.unitQty = 10000;
              // if updated trigger a change in the signal using object destructuring.
              return order.clone();
            }
            return order;
          });
        }, 5000);
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo