Snake_Eyes
Snake_Eyes

Reputation: 55

The Input() data passed in child component remains same after changing in the child

So i am passing an object having length of data and the data itself which is an array of objects

Now what is happening is that when I change the data in the child component and reload the page the data is still showing the edited property

To clarify more see the following screenshots:

  1. When we navigate to the route for the first time there would be no modified data: enter image description here

  2. When I edit the data:: enter image description here As you can see on the right side finalAmount property is added and its value is initialised

  3. Now when renavigate to the same component finalAmount is set to 5 enter image description here

The property itself should not be present in the data as I am getting it from the parent

These are the relevant files:

  1. stepper.component.html (Parent Component)
<mat-step [stepControl]="thirdFormGroup" >
        <ng-template matStepLabel>Prepare Invoice</ng-template>
        <ng-template matStepContent>
            <div class="container-fluid">
            
                <app-create-invoice [selectedCustomersInfo]="selectedCustomerData"
                    [selectedProductsData]="{lengthOfData:cartItems.length , selectedProducts:cartItems}"></app-create-invoice>
            
            </div>
        </ng-template>
    </mat-step>
  1. stepper.component.ts:
@Component({
  selector: 'app-stepper',
  templateUrl: './stepper.component.html',
  styleUrls: ['./stepper.component.scss'],
  providers: [
    {
      provide: STEPPER_GLOBAL_OPTIONS,
      useValue: { showError: true }
    }
  ]

})
export class StepperComponent implements OnInit, OnDestroy {

  getProductsSubscription = new Subscription
  cartItems: ProductDataModel[] = []
  selectedCustomerData: any
  products = this._formBuilder.group({
    firstCtrl: ['', Validators.required],
  });

  thirdFormGroup = this._formBuilder.group({
    thirdCtrl: ['', Validators.required],
  });
  stepperOrientation: Observable<StepperOrientation>;

  constructor(private _formBuilder: FormBuilder, breakpointObserver: BreakpointObserver, private cartService: CartService) {
    this.stepperOrientation = breakpointObserver
      .observe('(min-width: 800px)')
      .pipe(map(({ matches }) => (matches ? 'horizontal' : 'vertical')));
  }
  ngOnDestroy(): void {
    console.log("Stepper destroyed");
    this.getProductsSubscription.unsubscribe()
  }

  ngOnInit(): void {
    this.getProductsSubscription = this.cartService.getProducts().subscribe((items) => {
      this.cartItems = [...items]
      console.log("Cart items::", this.cartItems)
    })
  }

  setCustomerData(customerData: any) {
    this.selectedCustomerData = customerData
  }

}

  1. create-invoice.component.html (Child Component):
 <div class="table-responsive">
                <table class="table table-striped table-bordered table-hover">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">Product Category</th>
                            <th scope="col">Sub Category</th>
                            <th scope="col">Master Category</th>
                            <th scope="col">Product Weight</th>
                            <th scope="col">Price</th>
                            <th scope="col">Labour</th>
                            <th scope="col">SGST (In %)</th>
                            <th scope="col">CGST (In %)</th>
                            <th scope="col">Discount</th>
                            <th scope="col">Final Amount</th>
                        </tr>
                    </thead>

                    <tbody>
                        <tr *ngFor="let item of _selectedProductsData;index as i">
                            <th scope="row">{{i+1}}</th>
                            <td>{{item.productCategory}}</td>
                            <td>{{item.subCategory}}</td>
                            <td>{{item.masterCategory}}</td>
                            <td>{{item.productWeight}} gms</td>
                            <td>
                                <input class="form-control priceInput" type="number" id="{{item.productGuid}}-price"
                                    min="0" (input)="item.price = getValue($event,item.id,'price')" #price
                                    placeholder="Enter Price">
                            </td>

                            <td>
                                <input class="form-control labourInput" type="number" id="{{item.productGuid}}-labour"
                                    min="0" (input)="item.labour = getValue($event,item.id,'labour')" #labor
                                    placeholder="Enter Labour">
                            </td>

                            <td>
                                <input class="form-control sgstInput" type="number" id="{{item.productGuid}}-SGST"
                                    min="0" (input)="item.SGST = getValue($event,item.id,'sgst')" #sgst
                                    placeholder="Enter SGST">
                            </td>
                            <td>
                                <input class="form-control cgstInput" type="number" id="{{item.productGuid}}-CGST"
                                    min="0" (input)="item.CGST = getValue($event,item.id,'cgst')" #cgst
                                    placeholder="Enter CGST">
                            </td>
                            <td>
                                <input class="form-control discountInput" type="number" min="0"
                                    (input)="item.discount = getValue($event,item.id,'discount')" #discount
                                    id="{{item.productGuid}}-discount" placeholder="Enter Discount">
                            </td>
                            <td>
                                {{ item.finalAmount ?? 0 }}
                                <input class="form-control" type="hidden" [value]="item.finalAmount ?? 0">
                            </td>
                        </tr>
                    </tbody>

                </table>
            </div>
  1. create-invoice.component.ts:
@Component({
  selector: 'app-create-invoice',
  templateUrl: './create-invoice.component.html',
  styleUrls: ['./create-invoice.component.scss']
})
export class CreateInvoiceComponent implements OnInit,OnDestroy {
  
  _selectedCustomersInfo:any
  _selectedProductsData:InvoiceProductDataModel[] = []
  totalWeight = 0
  totalDiscount = 0
  totalGST = 0
  totalAmountWithGST = 0
  currentDate:Date = new Date()

  @Input() set selectedProductsData(productsData: {lengthOfData:number,selectedProducts:ProductDataModel[]}) {
    this.totalWeight = 0
    this.totalDiscount = 0
    this.totalAmountWithGST = 0
    this.totalGST = 0
    var temp = Object.assign({},productsData)
    this._selectedProductsData = []
    this._selectedProductsData = [...temp.selectedProducts]
    // this._selectedProductsData = [...productsData.selectedProducts]
    this._selectedProductsData.forEach((product) => {
      this.totalWeight += product.productWeight
    })
    console.log(this._selectedProductsData)
  }

  @Input() set selectedCustomersInfo(customerInfo: any) {
    this._selectedCustomersInfo = customerInfo
  }
  constructor() { }
  ngOnDestroy(): void {
    console.log("Create Invoice Destroyed!!")
    this._selectedProductsData = []
  }

  ngOnInit(): void {
  }

  getValue(event: Event, productId:number, valueOf:string): number {
    let product = this._selectedProductsData.find(item => item.id === productId)
    if (product) {
      switch (valueOf) {
        case 'labour':
          product.labour = Number((event.target as HTMLInputElement).value)
          this.setFinalAmountOfEachProduct(product)
          this.setTotalAmountWithGST()
          break

        case 'price':
          product.price = Number((event.target as HTMLInputElement).value)
          this.setFinalAmountOfEachProduct(product)
          this.setTotalAmountWithGST()
          break

        case 'sgst':
          product.SGST = Number((event.target as HTMLInputElement).value)
          this.setFinalAmountOfEachProduct(product)
          this.setTotalAmountWithGST()
          this.setTotalGST()
          break

        case 'cgst':
          product.CGST = Number((event.target as HTMLInputElement).value)
          this.setFinalAmountOfEachProduct(product)
          this.setTotalAmountWithGST()
          this.setTotalGST()
          break

        case 'discount':
          product.discount = Number((event.target as HTMLInputElement).value)
          this.setFinalAmountOfEachProduct(product)
          this.setTotalDiscount()
          this.setTotalAmountWithGST()
          break

      }
    }
    console.log(this._selectedProductsData.find(i => i.id == productId))
    return Number((event.target as HTMLInputElement).value);
  }

  setFinalAmountOfEachProduct(product:InvoiceProductDataModel) {

      product.finalAmount = 0
      let partialSum = (product.labour ?? 0) + (product.price ?? 0) - (product.discount ?? 0)
      let cgst = product.CGST ? partialSum * ((product.CGST ?? 100) / 100) : 0
      let sgst = product.SGST ? partialSum * ((product.SGST ?? 100) / 100) : 0

      product.totalGST = cgst  + sgst 
      product.finalAmount = partialSum + cgst + sgst
  }

  setTotalDiscount() {
    this.totalDiscount = 0
    this._selectedProductsData.forEach((item)=> {
      this.totalDiscount += item.discount ?? 0
    })
  }

  setTotalAmountWithGST() {
    this.totalAmountWithGST = 0
    this._selectedProductsData.forEach((item)=> {
      this.totalAmountWithGST += item.finalAmount ?? 0
    })
  }

  setTotalGST() {
    this.totalGST = 0
    this._selectedProductsData.forEach((item)=> {
      this.totalGST += item.totalGST ?? 0
    })
  }

}

And its not limited to finalAmount property it happens with other properties too like discount,sgst etc..

Any help will be appreciated

Upvotes: 0

Views: 126

Answers (2)

Snake_Eyes
Snake_Eyes

Reputation: 55

We can use structuredClone() to deep copy nested objects here in this case as my object having an array to a key

Refer the following screenshot as well: enter image description here

Also JSON.parse(JSON.stringify(productsData)) has its own limitations

For more information refer these links:

  1. https://stackoverflow.com/a/5344074/18480147
  2. https://stackoverflow.com/a/10916838/18480147

Upvotes: 0

lemek
lemek

Reputation: 798

From what I've understood you've got a problem with some data that have been changed in parent but it's not updated in child component, that have received it thru @input(). If I am right, you should read about Angular lifecycle hooks: HERE. Basically what you need to do, is to implement change detection hooks

@Input() someInputData: any;
    
ngOnChanges(changes: SimpleChanges) {    
    this.doSomething(changes.someInputData.currentValue);
}

The alternative is to use getter-setter approach like that

private _someInputData: string;

@Input() set someInputData(value: any) {
   this._someInputData = value;
   this.doSomething(this._someInputData);
}

get someInputData(): any {
    return this._someInputData;
}

Which approach is better? The short answer is I don't know :) I haven't measure performance difference, but:

  • ngOnChanges() will allow you to compare current and prev value
  • ngOnChanges() will track all inputs in comparison to getter-setter approach

However, there are some particular scenarios, usually with nested objects that tend to resist change detection in Angular, and for more elaborate solution see: SOLUTION

EDIT! Based on your comments, now I understand your problem, however I do not feel competent enough to tell you, why exactly such thing is occurring, it's related to the fact how Objects are made. Basically while passing Object through @Input decorator, you're creating a copy of object itself with all of its references. TLDR: Object passed by @Input() still holds a reference to memory that holds some variable. In that case what you need to do, is to create a deep copy of object and all of Objects within this object, because otherwise the nested references are still there. I see, that you're trying to mitigate this behaviour by using Object.assign(), which to my knowledge should work. However instead of doing so, please try using another approach:

const tempClone = JSON.parse(JSON.stringify(productsData));

Upvotes: 1

Related Questions