Home > front end >  Use *ngFor and [(ngModel)] to change an array of objects with a Map
Use *ngFor and [(ngModel)] to change an array of objects with a Map

Time:11-24

I have an class with an array of objects:

export class MyObject {
  sections: Section[]
}

The Section class contains a Map:

export class Section {
  dataPoints: Map<string, string>
}

I want to present an object of class MyObject to the user of the frontend and let the user change its content. I have a problem in the HTML code for the component for that:

<ul>
  <li *ngFor="let section of this.myObject; index as sectionsIndex">
    <ul>
      <li *ngFor="let dataPoint of section.dataPoints | keyvalue">
        <mat-form-field appearance="fill">
          <mat-label>Label:</mat-label>
          <input matInput placeholder="Input field placeholder"
              [(ngModel)]="this.myObject.sections[sectionsIndex].dataPoints[dataPoint.key]">
        </mat-form-field>
      </li>
    </ul>
  </li>
</ul>

I followed How to iterate using ngFor loop Map containing key as string and values as map iteration which brought me to the keyvalue pipe, but accessing the original object via [(ngModel)]="this.myObject.sections[sectionsIndex].dataPoints[dataPoint.key]" results in a compiler error "Element implicitly has an 'any' type because type 'Map<string, string>' has no index signature. Did you mean to call 'get'?" as well as the same error message with "Did you mean to call 'set'?" at the end.

The user should be able to change the content of the dataPoints Map in Sections as well as other class variables of MyObject which I didn't mention in the code example, as well as add and remove Sections from the array and add and remove elements from the Map dataPoints by using add / remove buttons that I left out in the code as well to concentrate on the problem.

What is best practice to deal with the problem of changing an array of objects with a Map? What leads to the error in my code?

CodePudding user response:

hard for me to tell exactly what you are trying to do, but a couple of things to think about:

  1. It doesn't make sense to write this.myObject.sections[sectionsIndex] because you already have access to that object by simply using: section

  2. did you possibly simply mean this: [(ngModel)]="dataPoint"

CodePudding user response:

It's a bit tricky because the values are primitive values (strings), so they are not passed to you by reference through the key value pipe. That means that updating the value will have no effect on the actual map. However, you can simply use (ngModelChange) to customize what happens when the value changes. Remove the round brackets from ngModel so that values are only passed in to the input from the key value pipe.

If the values were objects you could use two way binding [(ngModel)] with one of their properties, since you would have a reference to said object.

Note you also need a custom track by function here, or else Angular will rerender the input on each value change, causing the user to lose focus on the input.

Stackblitz: https://stackblitz.com/edit/angular-ivy-ejgn4p?file=src/app/app.component.html

<ul>
  <li *ngFor="let section of this.myObject.sections">
    <ul>
      <li *ngFor="let dataPoint of section.dataPoints | keyvalue; trackBy: trackByKey">
        <label>{{dataPoint.key}}
        <input
          [ngModel]="dataPoint.value"
          (ngModelChange)="section.dataPoints.set(dataPoint.key, $event)"
        />
        </label>
      </li>
    </ul>
  </li>
</ul>
export class AppComponent {
  myObject: MyObject = {
    sections: [
      {
        dataPoints: new Map([
          ['k11', 'v11'],
          ['k12', 'v12'],
          ['k13', 'v13'],
        ]),
      },
      {
        dataPoints: new Map([
          ['k21', 'v21'],
          ['k22', 'v22'],
          ['k23', 'v23'],
        ]),
      },
    ],
  };

  trackByKey(i: number, d: { key: string; value: string }) {
    return d.key;
  }
}
  • Related