3

StackBlitzExample

I am trying to create a simple component for a collection of addresses. The user can add, remove, and edit all of the addresses. It all mostly works except when there exists more than one address, the user deletes the first address only, and then adds a new address. The new address is added but the existing address is also cleared. In my application I also get the error - ExpressionChangedAfterItHasBeenCheckedError.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'hello',
  template: `<h1>Hello {{name}}!</h1><button type="button" (click)="add()">Add</button>
  <form>
  <div *ngFor="let addr of data; let i=index;">
    <input  name="{{'address1'+i}}" type="text" [(ngModel)]="addr.address1" />
    <input  name="{{'zip'+i}}" type="text" [(ngModel)]="addr.zip" />
    <button type="button" (click)="delete(i, addr)">Delete</button>
    <hr/>
  </div>
  </form>
  `,
  styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent  {
  @Input() name: string;
  data = [
    {
      address1:'500 Main St',
      address2:'',
      city:'Norman',
      state:'OK',
      zip:'73070'
    },
    {
      address1:'501 Main St',
      address2:'',
      city:'OKC',
      state:'OK',
      zip:'73105'
    }
  ];


  add() {
    this.data.push( { address1:'',
      address2:'',
      city:'',
      state:'',
      zip:''});
  }

  delete(index:number, row:any) {
   this.data.splice(index, 1);
  }
}

2
  • 1
    Can you be more specific about the steps needed to reproduce this error? I've tried adding, editing and deleting items, and it all seems to just work for me. Commented Jan 18, 2018 at 21:21
  • I have had it work and then stop working. I got some help on getter and was able to fix it by using the trackBy feature ngFor. Commented Jan 19, 2018 at 14:57

2 Answers 2

3

I was able to get things to work by using the trackBy feature of ngFor. Here is the modified code and the correction was made to StackBlitz also.

    @Component({
  selector: 'hello',
  template: `<h1>Hello {{name}}!</h1><button type="button" (click)="add()">Add</button>
  <form>
  <div *ngFor="let addr of data; let i=index; trackBy:trackByIndex">
    <input  name="{{'address1'+i}}" type="text" [(ngModel)]="addr.address1" />
      <input  name="{{'address2'+i}}" type="text" [(ngModel)]="addr.address2" />
        <input  name="{{'city'+i}}" type="text" [(ngModel)]="addr.city" />
          <input  name="{{'state'+i}}" type="text" [(ngModel)]="addr.state" />
            <input  name="{{'zip'+i}}" type="text" [(ngModel)]="addr.zip" />
            <button type="button" (click)="delete(i, addr)">Delete</button>
            <hr/>
  </div>
  </form>
  `,
  styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent  {
  @Input() name: string;
  data = [
    {
      address1:'500 Main St',
      address2:'',
      city:'Norman',
      state:'OK',
      zip:'73070'
    },
    {
      address1:'501 Main St',
      address2:'',
      city:'OKC',
      state:'OK',
      zip:'73105'
    }
  ];


  add() {
    this.data.push( { address1:'',
      address2:'',
      city:'',
      state:'',
      zip:''});
  }

  public trackByIndex(index: number, value: number) {
    return index;
  }

  delete(index:number, row:any) {
   this.data.splice(index, 1);
  }
}
Sign up to request clarification or add additional context in comments.

Comments

0

You want a FormArray. It is perfect for this kind of thing. I've gone ahead and recreated your application, using Reactive Forms so you can compare and contrast and see how it works.

Here's the link to the working example with all the code: https://stackblitz.com/edit/angular-eeamqq.

Note that I added the value of the reactive form to the template so you can see it is essentially an object being built dynamically.

Let me know if you would like me to expand further on some of the techniques I've used in the above app.

I'll also link the code here so you can look at it without loading the application:

The template:

  <button type="button" (click)="onAdd()">Add</button>
  <hr>

  <form [formGroup]="addressForm">
    <div formArrayName="addresses" 
         *ngFor="let address of addressForm.get('addresses').controls; let i = index;">
      <div [formGroupName]="i">
         <input formControlName="address1" placeholder="Address 1">
         <input formControlName="address2" placeholder="Address 2">
         <input formControlName="city" placeholder="City">
         <input formControlName="state" placeholder="State">
         <input formControlName="zip" placeholder="Zip">
         <button (click)="onDelete(i)">Delete</button>
      </div>
      <hr>
    </div>
  </form>

The important bits of the component class:

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    const addressForms = this.data.map(address => this.createAddressForm(address));

    this.addressForm = this.fb.group({
      addresses: this.fb.array(addressForms)
    });
  }

  createAddressGroup(address?: any) {
    return this.fb.group({
      address1: address ? address.address1 : '',
      address2: address ? address.address2 : '',
      city: address ? address.city : '',
      state: address ? address.state : '',
      zip: address ? address.zip : ''
    });
  }

  onAdd() {
    const addresses = this.addressForm.get('addresses') as FormArray;
    addresses.push(this.createAddressForm());
  }

  onDelete(index: number) {
    const addresses = this.addressForm.get('addresses') as FormArray;
    addresses.removeAt(index);
  }

1 Comment

Thanks for the example. We have been trying to do forms in just one style and we chose not to use reactive forms. I was able to find a solution by using the trackBy feature of ngFor.

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.