Search code examples
angularangular-changedetection

Getting wrong total value because of multiple change deduction how to prevent?


I am getting wrong value, because of multiple change deduction. how to prevent?

app.component.ts :

import { Component, OnInit } from "@angular/core";

interface PropsData {
  productName: string;
  value: number;
  count: number;
  details: (value1: number, value2: number) => number;
}

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  products: PropsData[];
  allTotal: number = 0;

  ngOnInit() {
    this.products = [
      {
        productName: "sugar",
        value: 20,
        count: 1,
        details: (value1, value2) => {
          return this.grandTotal(value1, value2);
        }
      },
      {
        productName: "salt",
        value: 40,
        count: 1,
        details: (value1, value2) => {
          return this.grandTotal(value1, value2);
        }
      },
      {
        productName: "jackery",
        value: 70,
        count: 1,
        details: (value1, value2) => {
          return this.grandTotal(value1, value2);
        }
      }
    ];
  }

  updateCount(product: PropsData) {
    product.count++;
  }

  updateAllTotal(itemTotal: number): void {
    this.allTotal = this.allTotal + itemTotal;
  }

  grandTotal(value1, value2): number {
    const total = value1 * value2;
    this.updateAllTotal(total);
    return total;
  }
}

html:

<div>
  <div *ngFor="let product of products">
    <h1>{{product.productName}}</h1>
    <p>{{product.count}}</p>
    <p>Total: {{product.details(product.count, product.value)}}</p>
    <p><button (click)="updateCount(product)">Add Product</button></p>
  </div>
  <h2>Grand Total: {{allTotal}}</h2>
</div>

Live Demo


Solution

  • As a general rule binding to a method should be used sparingly: in your case I would refactor the code in order to avoid the binding to product.details().

    First solution

    A simple way to do that is to declare an event emitter notifying when you're updating product.count: product.details could become a simple property (instead of a method) and gets updated in the event handler.

        interface PropsData {
          productName: string;
          value: number;
          count: number;
          details: number;
        }
    

    This is how your component might look like:

    products: PropsData[];
      allTotal: number = 0;
      onProductUpdate = new EventEmitter<PropsData>();
    
      ngOnInit() {
        this.products = [
          {
            productName: "sugar",
            value: 20,
            count: 1,
            details: 20 // Needs to be manually initialized, as it's not a function anymore
          },
          {
            productName: "salt",
            value: 40,
            count: 1,
            details: 40
          },
          {
            productName: "jackery",
            value: 70,
            count: 1,
            details: 70
          }
        ];
    
        this.updateAllTotal(); // Compute allTotal the first time
        
        // On every product update compute product.details and allTotal
        this.onProductUpdate.subscribe((product) => {
          product.details = this.getProductDetails(product);
          this.updateAllTotal();
        });
      }
    
      updateCount(product: PropsData) {
        product.count++;
        this.onProductUpdate.next(product);
      }
    
      updateAllTotal(): void {
        this.allTotal = 0;
        this.products.forEach(
          (item) => (this.allTotal = this.allTotal + item.details)
        );
      }
    
      getProductDetails(product): number {
        return product.count * product.value;
      }
    

    And this could be your template now (no function binding anymore):

    <div>
      <div *ngFor="let product of products">
        <h1>{{product.productName}}</h1>
        <p>{{product.count}}</p>
        <p>Total: {{product.details}}</p>
        <p><button (click)="updateCount(product)">Add Product</button></p>
      </div>
      <h2>Grand Total: {{allTotal}}</h2>
    </div>
    

    CodeSandbox

    Alternative solution

    Depending on how complex your real use case is, a different approach would be to encapsulate in the product itself the logic for updating its details every time value or count changes. For this you need to change PropsData to a class:

    class PropsData {
      productName: string;
      private _value: number;
      private _count: number;
      public get value() {
        return this._value;
      }
      public set value(value) {
        this._value = value;
        this.details = this._value * this._count;
      }
      public get count() {
        return this._count;
      }
      public set count(value) {
        this._count = value;
        this.details = this._value * this._count;
      }
      details: number;
    
      constructor(name: string, value: number, count: number) {
        this.productName = name;
        this.count = count;
        this.value = value;
      }
    }
    

    As you can see, value and count setters take care of updating details as well. This simplifies the component's logic:

    products: PropsData[];
      allTotal: number = 0;
    
      ngOnInit() {
        this.products = [
          new PropsData("sugar", 20, 1),
          new PropsData("salt", 40, 1),
          new PropsData("jackery", 70, 1)
        ];
        this.updateAllTotal();
      }
    
      updateCount(product: PropsData) {
        product.count++;
        this.updateAllTotal();
      }
    
      updateAllTotal(): void {
        this.allTotal = 0;
        this.products.forEach(
          (item) => (this.allTotal = this.allTotal + item.details)
        );
      }
    

    CodeSandbox