Home > Blockchain >  Angular material table lost value after adding a row
Angular material table lost value after adding a row

Time:08-29

I am about to learn Angular and I ran into an issue. I do have a material table with an add button underneath. When I press the add button a new row is added. However, I do observe a strange behavior. When I add a new row the values of the other rows are removed. I have no explanation for that behavior and would like to ask you if you have any advice.

This is the table with the add button:

<table id="options-table" mat-table [dataSource]="options" >
  <ng-container matColumnDef="option-name">
    <th mat-header-cell *matHeaderCellDef>Option name</th>
    <td mat-cell *matCellDef="let option; let rowIndex = index">
      <mat-form-field>
        <input matInput [(ngModel)]="options[rowIndex].optionName" placeholder="Enter a name for the option" [formControl]="optionNameFormControl">
        <mat-error *ngIf="optionNameFormControl.hasError('required')">
          Option name is <strong>required</strong>
        </mat-error>
      </mat-form-field>
    </td>
  </ng-container>

  <!-- ... more columns ... --
        
  <tr mat-header-row *matHeaderRowDef="optionTableColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: optionTableColumns;"></tr>
</table>

<button mat-raised-button color="primary" (click)="onAddOption()">Add an Option</button>

This is the component code:

import { Component, OnInit } from '@angular/core';
import { OptionGroup } from '../option-group';
import { Option} from '../option';
import { Location } from '@angular/common';
import { FormControl, ValidatorFn, Validators } from '@angular/forms';
import {ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-start-new-decision',
  templateUrl: './start-new-decision.component.html',
  styleUrls: ['./start-new-decision.component.scss']
})
export class StartNewDecisionComponent implements OnInit {
  options: Option[] = [{"optionName" : '', 'description': ''}];

  optionNameFormControl = new FormControl('', [Validators.required]);

  optionTableColumns: string[] = ['option-name'/*, more properties */];


  constructor(private location: Location, private cdref: ChangeDetectorRef) { }

  ngOnInit(): void {
  }


  onAddOption(): void {
    let optionToAdd: Option = {
      optionName: '',
      description: ''
    };
    console.log("Option to add: "   JSON.stringify(optionToAdd));
    console.log("Options before add: "   JSON.stringify(this.options));
    this.options.push(optionToAdd);
    console.log("Options after add: "   JSON.stringify(this.options));
    // Unfortunately material table requires a full copy to recognize the change. TODO: see if there is another way of doing it.
    this.options = [...this.options];
    console.log("Options after copy: "   JSON.stringify(this.options));
    this.cdref.detectChanges();
  }
}

This is how the table looks initially: initial table

When I press "Add an Option" it looks like: After add

When I change the option name in the second row to "test" it looks like: After change option name

So far everything works as expected. But when I now click on "Add an Option" again, the value "test" of the second row is gone.: enter image description here

And this is the log output, showing that the model is correct.



start-new-decision.component.ts:54 Option to add: {"optionName":"","description":""}
start-new-decision.component.ts:55 Options before add: [{"optionName":"","description":""}]
start-new-decision.component.ts:57 Options after add: [{"optionName":"","description":""},{"optionName":"","description":""}]
start-new-decision.component.ts:60 Options after copy: [{"optionName":"","description":""},{"optionName":"","description":""}]
start-new-decision.component.ts:54 Option to add: {"optionName":"","description":""}
start-new-decision.component.ts:55 Options before add: [{"optionName":"","description":""},{"optionName":"test","description":""}]
start-new-decision.component.ts:57 Options after add: [{"optionName":"","description":""},{"optionName":"test","description":""},{"optionName":"","description":""}]
start-new-decision.component.ts:60 Options after copy: [{"optionName":"","description":""},{"optionName":"test","description":""},{"optionName":"","description":""}]

CodePudding user response:

You bind the input to ngmodel and to formcontrol. And you are using the same form control for all lines. Not sure how the framework should handle this. You need to use formarrays and you should not use ngModel. On adding a new row you need to pass new "line" to the form group as well.

CodePudding user response:

Thanks. It is working now:

  <form [formGroup]="optionsTableFormGroup">
    <ng-container formArrayName="optionsTableFormArray"> 
      <table mat-table #optionsTable  [dataSource]="optionsTableDataSource">
          <ng-container matColumnDef="option-name">
            <th mat-header-cell *matHeaderCellDef>Option name</th>
            <td mat-cell *matCellDef="let option; let rowIndex = index" [formGroup]="option">
              <mat-form-field>
                <input matInput placeholder="Enter a name for the option" formControlName="optionName">
                <mat-error *ngIf="option.get('optionName').hasError('maxlength')">
                  Option name exceeds the maximum length of 40 characters
                </mat-error>
                <mat-error *ngIf="option.get('optionName').hasError('required')">
                  Option name is <strong>required</strong>
                </mat-error>           
              </mat-form-field>
            </td>
          </ng-container>

          <ng-container matColumnDef="description">
            <th mat-header-cell *matHeaderCellDef>Description</th>
            <td mat-cell *matCellDef="let option" [formGroup]="option">
              <mat-form-field>
                <textarea matInput placeholder="Enter a description for the option" formControlName="description"></textarea>
                <mat-error *ngIf="option.required">
                  Description exceeds the maximum length of 200 characters
                </mat-error>  
              </mat-form-field>
            </td>
          </ng-container>

          <ng-container matColumnDef="option-actions">
              <th mat-header-cell *matHeaderCellDef></th>
              <td mat-cell *matCellDef="let option; let i = index">
                <button mat-raised-button color="warn" (click)="onRemoveOption(i)">Remove</button>
              </td>
          </ng-container>
            
          <tr mat-header-row *matHeaderRowDef="optionTableColumns"></tr>
          <tr mat-row *matRowDef="let row; columns: optionTableColumns;"></tr>
      </table>
    </ng-container>
  </form>
  <button mat-raised-button color="primary" (click)="onAddOption()">Add an Option</button>

And the ts code:

import { Component, OnInit, ViewChild } from '@angular/core';
import { OptionGroup } from '../option-group';
import { Option} from '../option';
import { Location } from '@angular/common';
import { FormGroup, FormBuilder, FormControl, ValidatorFn, Validators, AbstractControl, FormArray } from '@angular/forms';
import { MatTable, MatTableDataSource } from '@angular/material/table';

const emailValidators : ValidatorFn[] = [Validators.required, Validators.email]; 
const optionNameValidators : ValidatorFn[] = [Validators.required]; 

@Component({
  selector: 'app-start-new-decision',
  templateUrl: './start-new-decision.component.html',
  styleUrls: ['./start-new-decision.component.scss']
})
export class StartNewDecisionComponent implements OnInit {
  @ViewChild('optionsTable') optionsTable!: MatTable<any>;
  optionsTableDataSource!: MatTableDataSource<AbstractControl>;

  optionsTableFormGroup!: FormGroup;
  
  optionTableColumns: string[] = ['option-name', 'description', 'option-actions'];

  constructor(private location: Location, private fb: FormBuilder) { 
    this.createOptionsTableFormGroup();
    this.optionsTableDataSource = new MatTableDataSource(this.optionsTableFormArray.controls);
  }

  createOptionsTableFormGroup(): void {
    this.optionsTableFormGroup = this.fb.group({
      optionsTableFormArray: this.fb.array([
        this.createOptionFormGroup()
      ])
    });
  }

  get optionsTableFormArray(): FormArray {
    return this.optionsTableFormGroup.get('optionsTableFormArray') as FormArray;
  }

  ngOnInit(): void {

  }

  onRemoveOption(i: number): void {
    this.removeOptionControl(i);
    this.optionsTable.renderRows();
  }

  onAddOption(): void {
    let option: Option = {'optionName': '', 'description': ''};
    this.addOptionControl(option);
    this.optionsTable.renderRows();
  }

  createOptionFormGroup() : FormGroup {
    return this.fb.group({
      optionName: ['', [Validators.required, Validators.maxLength(40)]],
      description: ['', [Validators.maxLength(200)]]
    })
  }

  addOptionControl(option: Option) {
    this.optionsTableFormArray.push(this.createOptionFormGroup());
  }

  removeOptionControl(i: number) {
    this.optionsTableFormArray.removeAt(i);
  }
}
  • Related