Angualr2- ngModel within ngFor not working. Model gets updated correctly but some error in the view

Issue

I am working on a small task of creating a Trip Planner using Angular2. The work flow is as follows: The user will input start point and destination along with number of intermediate stop points. Once the user submit “Go” button, the Planner table appears, where user can input the intermediate stop points as well as add/remove the stop points. Somehow I am not getting the desired output as shown in the screen shot.First Screen

2nd Screen

On adding new stop point, this is what I am getting…
My output on adding a new stop point

Desired output….
Desired output

This is how the Tripdetails object looks like. See the difference between the object and the view.

console image

The code are as follows:
Trip.ts

export class Trip {
    startPoint:string;
    destination:string;
    stops:number;
}

TripDetails.ts

export class TripDetail {
    startPoint:string;
    destination:string;
}

app.component.ts

import { Component } from '@angular/core';
import { Trip } from './Trip';
import { TripDetail } from './TripDetails';

@Component({
  selector: 'my-app',
  moduleId: module.id,
  templateUrl: 'app.component.html'
})

export class AppComponent  { 
    trip: Trip = {
        startPoint : "",
        destination : "",
        stops : 0
    };
    tripDetails: TripDetail[];

    clicked(startPoint:string, destination:string, stops:string):void {
        let length = parseInt(stops);
            this.trip.startPoint = startPoint;
            this.trip.destination = destination;
            this.trip.stops = length;
            this.tripDetails = [];
        for (let i=0 ; i< length; i++) {
            let tripDetail :TripDetail={
                startPoint: "",
                destination: ""
            }
            if( i==0) {
                tripDetail.startPoint = this.trip.startPoint;
            }
            if( i== length-1) {
                tripDetail.destination = this.trip.destination;
            }
            this.tripDetails.push(tripDetail);
        }
    }

    syncData(index:number, locationType:string) {
        if(locationType == 'destinationLocation') {
            this.tripDetails[index + 1].startPoint = this.tripDetails[index].destination; 

        }
        else if (locationType == 'sourceLocation') {
            this.tripDetails[index - 1].destination = this.tripDetails[index].startPoint; 
        }
    }

    addStop(index:number):void {
        let stop:TripDetail={
            startPoint:"newStop",
            destination:"newStop"
        }
        this.tripDetails.splice(index,0,stop);
    }   

    removeStop(index:number):void{
        let destination = this.tripDetails[index].destination,
            arrlength = this.tripDetails.length;
        if(index==0){
            return;
        }
        else if( index== arrlength-1) {
            this.tripDetails[index-1].destination = this.tripDetails[index].destination;
            this.tripDetails.splice(index,1);
        }
        else{
            this.tripDetails.splice(index,1);
            this.tripDetails[index].destination= destination;   
        }
    }


}

app.component.html

<div class="container content">
    <div>
        <form>
            <label for="startPoint">From:</label>
            <input class="form-control" type="text" placeholder="Enter start point" name="startPoint" [(ngModel)] = "trip.startPoint" >

            <label for="destination">To:</label>
            <input class="form-control" type="text" name="destination" placeholder="Enter destiantion" id="destination" [(ngModel)]="trip.destination" >

            <label for="Stops">Stops:</label>
            <input class="form-control" type="text" placeholder="Enter number of stops"  name="stops" id="stops" [(ngModel)]="trip.stops"  >

            <div class="go-btn">
                <button class="btn btn-primary" (click)="clicked(trip.startPoint,trip.destination,trip.stops)">Go</button>
            </div>
            <div *ngIf = "trip.stops && trip.startPoint && trip.destination" class="trip-table  table-bordered">
                <table class="table table-stripped">
                    <thead class="thead">
                        <tr scope="row">
                        <th>FROM</th><th>TO</th>
                    </tr>
                    </thead>
                    <tr *ngFor= " let stops of tripDetails; let i=index; let first = first; let last= last; trackBy: index ">
                        <td>

                            <input type="text" class="form-control"  name="locationFrom-{{i}}" [(ngModel)]= "stops.startPoint"  (input)="syncData(i, 'sourceLocation')" >
                        </td>
                        <td>

                            <input type="text" class="form-control"  name="locationTo-{{i}}" [(ngModel)]= "stops.destination"  (input)="syncData(i, 'destinationLocation')" >
                        </td>
                        <td>
                            <button class="btn btn-primary" (click)="addStop(i)">+</button>
                        </td>
                        <td>
                            <button class="btn btn-primary" (click)="removeStop(i)" >-</button>
                        </td>
                    </tr>
                </table>                
            </div>
        </form>
    </div>
</div>  

app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';

import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule , FormsModule],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Solution

You are absolutely correct with using trackBy to track the index, you just had some problems with that. trackby is needed when there is not an unique ngModel index to track (variable with property, e.g [(ngModel)]="myItem.myProperty") and is therefore needed when dealing with primitive arrays, for example trackBy would be needed in below example:

<div *ngFor="let item of items">
  <input [(ngModel)]="item" />
</div>

If you would try and modify the input fields above, the input field would instantly loose focus when typing something.

But you might say…

My array is not of primitive type and my array tripDetails contains objects which can be tracked with the unique ngModel index?

True, in case you were not using a form, you would be fine without trackBy.

But since we are dealing with a form, Angular doesn’t really care about the unique ngModel, but looks at the name attribute, which basically means that field suddenly doesn’t have an unique ngModel with index to track.

So use trackBy

<tr *ngFor= "let stops of tripDetails; let i=index; trackBy:trackByFn">

and the corresponding function in the component:

trackByFn(index: number, stops: string) {
  return index;
}

Here’s a

Demo

Answered By – AT82

Answer Checked By – Marie Seifert (AngularFixing Admin)

Leave a Reply

Your email address will not be published.