3

I have an array of objects in a component. which I will iterate in the template.

app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'sample-app';
  classesData = [];

  constructor() {
  }

  ngOnInit() {
    this.classesData = [
      {title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
    ];
  }

  duplicate() {
    const newData = JSON.parse(JSON.stringify(this.classesData[1]));
    newData.title += 'Copy';
    this.classesData.splice(1, 0, newData);
  }
}

app.template.html

<form #testingFrom="ngForm">
  <p>{{classesData | json}}</p>
  <div *ngFor="let classData of classesData; let i=index">
    <input [(ngModel)]="classData.title" name="{{'title-' + i}}" type="text">
  </div>
  <button (click)="duplicate()">Duplicate</button>
</form>

My aim is when a user clicks on the duplicate button I simply add a new element at index 1 in an array. My initial state looks like (before user clicks)

enter image description here

And my state after a user clicks duplicate button

enter image description here

In the image above at 3rd input field, we are getting Hello1Copy instead of Hello1.

5
  • 1
    Either I'm missing the point, or you are not specifying your issue. Commented Feb 19, 2019 at 16:41
  • what is the question? Commented Feb 19, 2019 at 16:42
  • Why this.classesData[1]? I mean, why are you taking index 1? What is the duplicate button meant to duplicate? the first item? the last item? all items? Commented Feb 19, 2019 at 16:43
  • @SakutoI have added an issue details Commented Feb 19, 2019 at 16:43
  • @briosheje this is just a demo. Commented Feb 19, 2019 at 16:44

4 Answers 4

1

I completely suspect that this behavior is happening because of conflict in the name attribute value. For this case only, if you splice the newItem at first location, it only adds that's variable and other DOM's doesn't re-render. For cross verification you can try replacing input element with simple binding like {{classData.title}} and everything works fine.

This behavior can easily be solved by not conflicting name attribute value for all time. What that means is to assign a unique id variable with each collection item and use it.

this.classesData = [
  { id: 1, title: 'Hello0' }, 
  { id: 2, title: 'Hello1' }, 
  { id: 3, title: 'Hello2' }
];
duplicate() {
    const newData = JSON.parse(JSON.stringify(this.classesData[1]));
    newData.title += 'Copy';
    newData.id = Date.now()
    this.classesData.splice(1, 0, newData);
}

Template

<div *ngFor="let classData of classesData;let i=index">
   <input [(ngModel)]="classData.title" [name]="'title_'+classData.id" type="text">
</div>

Stackblitz


You can also verify the same by removing name attribute from each input field. But that would not suffice, it would throw

ERROR Error: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.

So add [ngModelOptions]="{standalone: true}" on each input field to make input working without name attribute. As suggested in another answer by @briosheje, you can also re-enforce rendering using trackBy.

PS: I'm investigating why this works differently when there is a combination of name and input, I suspect about form API wiring with input element. I'll update the answer as soon as I get something.

Sign up to request clarification or add additional context in comments.

Comments

1

The problem is that you are using a form. Because you're using a form, you need to specify how angular should track the changes for your form items, if you're planning to alter the existing source. You can do such using the trackBy pipe:

<form #testingFrom="ngForm">
  <p>{{classesData | json}}</p>
  <div *ngFor="let classData of classesData; let i=index; trackBy: trackByFn">
    <input [(ngModel)]="classData.title" [name]="'title-' + i" type="text">
  </div>
  <button (click)="duplicate()">Duplicate</button>
</form>

Typescript relevant part:

  trackByFn(index: any) {
    return index;
  }

Please note that adding elements to the collection will work in your original example.

Working stackblitz: https://stackblitz.com/edit/angular-uabuya

Comments

0

Make another variable and iterate that variable to crate inputs

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
  title = 'sample-app';
  originalData=[];
  classesData = [];

  constructor() {
  }

  ngOnInit() {
    this.classesData = [
      {title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
    ];
    this.originalData=[...this.classesData]; // changed here
  }

  duplicate() {
    const newData = JSON.parse(JSON.stringify(this.classesData[1]));
    newData.title += 'Copy';
    this.classesData.splice(1, 0, newData);
  }
}

Working Demo

Comments

0

You can solve your issue using "trackBy" feature. Please see below code sample.

app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'sample-app';
  classesData = [];

  constructor() {}

  ngOnInit() {
    this.classesData = [
      { title: 'Hello0' },
      { title: 'Hello1' },
      { title: 'Hello2' }
    ];
  }

  duplicate() {
    const newData = JSON.parse(JSON.stringify(this.classesData[1]));
    newData.title += 'Copy';
    this.classesData.splice(1, 0, newData);
  }

  trackByIndex(index: number, obj: any): any {
    return index;
  }
}

app.component.html

<form>
  <p>{{classesData | json}}</p>
  <div *ngFor="let classData of classesData; let i=index;trackBy:trackByIndex;">
    <input [(ngModel)]="classesData[i].title" name="{{'title-' + i}}" type="text" />
  </div>
  <button (click)="duplicate()">Duplicate</button>
</form>

Please let me know if this solution works for you!

Comments

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.