liveBook · Manning

Mutate method

As explained in Pro Angular 16

The mutate method changes a writable Angular signal based on its current value.

The mutate method changes a writable Angular signal according to its current value. Unlike set, which replaces the value, mutate supports incremental updates such as adding, removing, or modifying items in an array. section 6.2.1

Use mutate when an update depends on the signal's existing contents:

  • Add product selections by pushing or updating entries in the current cart-line array. section 6.2.1
  • Remove a selection by finding its index and splicing the current array. section 6.2.1
  • Populate a product signal from a data source while retaining controlled write access inside the model. section 10.4.4

Removing a cart line

A cart can locate a product and remove it inside a mutate callback: this.linesSignal.mutate(linesArray => { let index = linesArray.findIndex(l => l.product.id == id); linesArray.splice(index, 1); }); The signal's current array is changed incrementally, allowing dependent summaries to update. section 6.2.1

Listing 6.1. The contents of the cart.model.ts file in the src/app/model folder
import { Injectable, Signal, WritableSignal, computed, signal } 
    from "@angular/core";
import { Product } from "./product.model";
 
@Injectable()
export class Cart {
    private linesSignal: WritableSignal<CartLine[]>;
    public summary: Signal<CartSummary>;
 
    constructor() {
        this.linesSignal = signal([]);
 
        this.summary = computed(() => {
            let newSummary = new CartSummary();
            this.linesSignal().forEach(l => {
                newSummary.itemCount += l.quantity;
                newSummary.cartPrice += l.lineTotal;
            });
            return newSummary;
        })
    }
 
    get lines(): Signal<CartLine[]> {
        return this.linesSignal.asReadonly();
    }
 
    addLine(product: Product, quantity: number = 1) {
        this.linesSignal.mutate(linesArray => {
            let line = linesArray.find(l => l.product.id == product.id);
            if (line != undefined) {
                line.quantity += quantity;
            } else {
                linesArray.push(new CartLine(product, quantity));
            }
        });
    }
 
    updateQuantity(product: Product, quantity: number) {
        this.linesSignal.mutate(linesArray => {
            let line = linesArray.find(l => l.product.id == product.id);
            if (line != undefined) {
                line.quantity = Number(quantity);
            }
        });
    }
 
    removeLine(id: number) {
        this.linesSignal.mutate(linesArray => {
            let index = linesArray.findIndex(l => l.product.id == id);
            linesArray.splice(index, 1);
        });
    }
 
    clear() {
        this.linesSignal.set([]);
    }
}
 
export class CartLine {
    
    constructor(public product: Product,
        public quantity: number) {}
 
    get lineTotal() {
        return this.quantity * (this.product.price ?? 0);
    }
}
 
export class CartSummary {
    itemCount: number = 0;
    cartPrice: number = 0;
}

Inspect the complete cart model to see mutate alongside a computed summary and a read-only public signal. section 6.2.1

Listing 10.12. Using signals in the repository.model.ts file in the src/app folder
import { Product } from "./product.model";
import { SimpleDataSource } from "./datasource.model";
import { Signal, WritableSignal, signal } from "@angular/core";

export class Model {
    private dataSource: SimpleDataSource;
    private products: WritableSignal<Product[]>;
    private locator = (p: Product, id: number | any) => p.id == id;

    constructor() {
        this.dataSource = new SimpleDataSource();
        this.products = signal(new Array<Product>());
        //this.dataSource.getData().forEach(p => this.products.push(p));
        this.products.mutate(prods => 
            this.dataSource.getData().forEach(p => prods.push(p)));
    }

    get Products(): Signal<Product[]> {
        return this.products.asReadonly();
    }

    getProduct(id: number): Product | undefined {
        return this.products().find(p => this.locator(p, id));
    }

    saveProduct(product: Product) {
        if (product.id == 0 || product.id == undefined) {
            product.id = this.generateID();
            this.products.mutate(prods => prods.push(product));
        } else {
            this.products.mutate(prods => {
                let index = prods.findIndex(p => 
                    this.locator(p, product.id));
                prods.splice(index, 1, product);
            });
        }
    }

    deleteProduct(id: number) {
        this.products.mutate(prods => {
            let index = prods.findIndex(p => this.locator(p, id));
            if (index > -1) {
                prods.splice(index, 1);
            }
        });
    }

    private generateID(): number {
        let candidate = 100;
        while (this.getProduct(candidate) != null) {
            candidate++;
        }
        return candidate;
    }
}

Compare repository initialization with mutate and the private writable signal exposed through asReadonly. section 10.4.4

How Pro Angular 16 covers it

How deep it goes
Demonstrates array insertion and removal, but does not detail the mutate implementation internals.
Examples
Uses cart-line addition and removal, product-signal initialization, and dependent product-count recomputation.
What it assumes
Assumes familiarity with Angular signals and distinguishes writable signals from read-only signal views.
What you can do after
Apply it via Angular writable signals to perform controlled incremental updates based on existing array contents.
How it connects
Connects writable signals with read-only signals, computed signals, reactive updates, and dependency injection.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Pro Angular 16. The section links point to the parts of the book it draws on.