45

I'm trying to build an inline editable table using the latest material+cdk for angular.

Question

How can I make mat-table use [formGroupName] so that the form fields can be referenced by its correct form path?

This is what I got so far: Complete StackBlitz example

Template

<form [formGroup]="form">
  <h1>Works</h1>
  <div formArrayName="dates" *ngFor="let date of rows.controls; let i = index;">
    <div [formGroupName]="i">
      <input type="date" formControlName="from" placeholder="From date">
      <input type="date" formControlName="to" placeholder="To date">
    </div>
  </div>


  <h1>Wont work</h1>
  <table mat-table [dataSource]="dataSource" formArrayName="dates">
    <!-- Row definitions -->
    <tr mat-header-row *matHeaderRowDef="displayColumns"></tr>
    <tr mat-row *matRowDef="let row; let i = index; columns: displayColumns;" [formGroupName]="i"></tr>

    <!-- Column definitions -->
    <ng-container matColumnDef="from">
      <th mat-header-cell *matHeaderCellDef> From </th>
      <td mat-cell *matCellDef="let row"> 
        <input type="date" formControlName="from" placeholder="From date">
      </td>
    </ng-container>

    <ng-container matColumnDef="to">
      <th mat-header-cell *matHeaderCellDef> To </th>
      <td mat-cell *matCellDef="let row">
        <input type="date" formControlName="to" placeholder="To date">
      </td>
    </ng-container>
  </table>
  <button type="button" (click)="addRow()">Add row</button>
</form>

Component

export class AppComponent implements  OnInit  {
  data: TableData[] = [ { from: new Date(), to: new Date() } ];
  dataSource = new BehaviorSubject<AbstractControl[]>([]);
  displayColumns = ['from', 'to'];
  rows: FormArray = this.fb.array([]);
  form: FormGroup = this.fb.group({ 'dates': this.rows });

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.data.forEach((d: TableData) => this.addRow(d, false));
    this.updateView();
  }

  emptyTable() {
    while (this.rows.length !== 0) {
      this.rows.removeAt(0);
    }
  }

  addRow(d?: TableData, noUpdate?: boolean) {
    const row = this.fb.group({
      'from'   : [d && d.from ? d.from : null, []],
      'to'     : [d && d.to   ? d.to   : null, []]
    });
    this.rows.push(row);
    if (!noUpdate) { this.updateView(); }
  }

  updateView() {
    this.dataSource.next(this.rows.controls);
  }
}

Problem

This wont work. Console yields

ERROR Error: Cannot find control with path: 'dates -> from'

It seems as if the [formGroupName]="i" has no effect, cause the path should be dates -> 0 -> from when using a formArray.

My current workaround: For this problem, I've bypassed the internal path lookup (formControlName="from") and use the form control directly: [formControl]="row.get('from')", but I would like to know how I can (or at least why I cannot) use the Reactive Form preferred way.

Any tips are welcome. Thank you.


Since I think this is a bug, I've registered an issue with the angular/material2 github repo.

Øystein Amundsen
  • 3,993
  • 8
  • 44
  • 63

5 Answers5

32

I would use the index which we can get within matCellDef binding:

*matCellDef="let row; let index = index" [formGroupName]="index"

Forked Stackblitz

For solving problems with sorting and filtering take a look at this answer Angular Material Table Sorting with reactive formarray

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Wont that produce the index from the columns and not the rows? – Øystein Amundsen Jul 15 '18 at 12:49
  • 1
    You can check it by printing index https://stackblitz.com/edit/angular-material-editable-table-7gczzz?file=src/app/app.component.html – yurzui Jul 15 '18 at 12:51
  • 2
    Seemingly works fine. Strange, if this is intentional, it is not intuitive. But thank you. – Øystein Amundsen Jul 15 '18 at 12:53
  • 3
    this generates problems when using pagination and filtering, I'm struggling with that right now... Pagination is easy to solve, but not filtering – Pizzicato Apr 12 '19 at 11:20
  • @Pizzicato how did you solve pagination when using index as formGroupName. I'm facing the same problem. – Deepa Panicker Dec 02 '19 at 07:02
  • This works fine if you dont have sort or pagination... Pagination you can solve because you know how many items you are showing at the moment and which page index too and like that you can recalculate the right index but with sort... not really – Celso Soares Jan 23 '20 at 18:00
  • @Pizzicato , how have you solved the filtering problem? – Georgi Aug 17 '20 at 17:45
  • @Pizzicato I added info about filtering – yurzui Nov 22 '20 at 05:42
  • 3
    For newer versions of Material, you will need to change `let index = index` to `let index = dataIndex` – mb-ca Jun 24 '21 at 16:47
25

here is the sample code

In Html:

    <form [formGroup]="tableForm">

    <mat-table formArrayName="users" [dataSource]="dataSource">

      <ng-container cdkColumnDef="position">
        <mat-header-cell *cdkHeaderCellDef> No. </mat-header-cell>
        <mat-cell *cdkCellDef="let row let rowIndex = index"  [formGroupName]="rowIndex"> 
          <input type="text" size="2" formControlName="position"> </mat-cell>
      </ng-container>


      <ng-container cdkColumnDef="name">
        <mat-header-cell *cdkHeaderCellDef> Name </mat-header-cell>
        <mat-cell *cdkCellDef="let row let rowIndex = index"  [formGroupName]="rowIndex"> 
          <input type="text" size="7" formControlName="name">
        </mat-cell>
      </ng-container>

        <ng-container cdkColumnDef="weight">
        <mat-header-cell *cdkHeaderCellDef> Weight </mat-header-cell>
        <mat-cell *cdkCellDef="let row let rowIndex = index"  [formGroupName]="rowIndex"> 
          <input type="text" size="3" formControlName="weight">
        </mat-cell>
      </ng-container>

        <ng-container cdkColumnDef="symbol">
        <mat-header-cell *cdkHeaderCellDef> Symbol </mat-header-cell>
        <mat-cell *cdkCellDef="let row let rowIndex = index"  [formGroupName]="rowIndex"> 
          <input type="text" size="2" formControlName="symbol">
        </mat-cell>
      </ng-container>

      <!-- Header and Row Declarations -->
      <mat-header-row *cdkHeaderRowDef="displayedColumns"></mat-header-row>
      <mat-row *cdkRowDef="let row; columns: displayedColumns;"></mat-row>
    </mat-table>
    </form>

Controller code:

    displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];


     dataSource ;
      tableForm: FormGroup;



     constructor(private formBuilder: FormBuilder){
     this.dataSource = [
      {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
      {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
      {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
      {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
      {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
      {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
      {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
      {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
      {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
      {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
    ];
      }

      ngOnInit(){
        this.tableForm= this.formBuilder.group({
            users: this.formBuilder.array([])
        })
        this.setUsersForm();
        this.tableForm.get('users').valueChanges.subscribe(users => {console.log('users', users)});
      }
      private setUsersForm(){
        const userCtrl = this.tableForm.get('users') as FormArray;
        this.dataSource.forEach((user)=>{
          userCtrl.push(this.setUsersFormArray(user))
        })
      };
      private setUsersFormArray(user){


        return this.formBuilder.group({
            position:[user.position],
            name:[user.name],
            weight:[user.weight], 
            symbol:[user.symbol]
        });
      }
Øystein Amundsen
  • 3,993
  • 8
  • 44
  • 63
Rejayi CS
  • 1,034
  • 1
  • 10
  • 22
  • Hi, nice answer, can you please edit your answer to add mat-error implementation? – Furqan S. Mahmoud Jun 29 '19 at 12:37
  • How to implement paginator in this example? – Deepa Panicker Nov 28 '19 at 11:49
  • For pagination :Pagination To paginate the table's data, add a after the table. If you are using the MatTableDataSource for your table's data source, simply provide the MatPaginator to your data source. It will automatically listen for page changes made by the user and send the right paged data to the table. check this link for more https://material.angular.io/components/table/overview#datasource – Rejayi CS Jun 16 '20 at 04:32
  • I am getting: Cannot find control with name: '0' – Mark Nov 01 '21 at 09:58
  • I was missing formArrayName="users". Now I am getting: Error: Cannot find control with path: 'users -> 0' – Mark Nov 01 '21 at 10:08
11

A little late to the party but I managed to get it working without relying on the index. This solution also supports filtering etc from the MatTableDataSource.

https://stackblitz.com/edit/angular-material-table-with-form-59imvq

Component

import {
  Component, ElementRef, OnInit
} from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'
import { AlbumService } from './album.service';
import { UserService } from './user.service';
import { Album } from './album.model';
import { User } from './user.model';
import { FormArray, FormGroup, FormBuilder } from '@angular/forms';
import { MatTableDataSource } from '@angular/material';

@Component({
  selector: 'table-form-app',
  templateUrl: 'app.component.html'
})
export class AppComponent implements OnInit {
  form: FormGroup;
  users: User[] = [];
  dataSource: MatTableDataSource<any>;
  displayedColumns = ['id', 'userId', 'title']
  constructor(
    private _albumService: AlbumService,
    private _userService: UserService,
    private _formBuilder: FormBuilder
    ) {}

  ngOnInit() {
    this.form = this._formBuilder.group({
      albums: this._formBuilder.array([])
    });
    this._albumService.getAllAsFormArray().subscribe(albums => {
      this.form.setControl('albums', albums);
      this.dataSource = new MatTableDataSource((this.form.get('albums') as FormArray).controls);
      this.dataSource.filterPredicate = (data: FormGroup, filter: string) => { 
          return Object.values(data.controls).some(x => x.value == filter); 
        };
    });
    this._userService.getAll().subscribe(users => {
      this.users = users;
    })
  }

  get albums(): FormArray {
    return this.form.get('albums') as FormArray;
  }

  // On user change I clear the title of that album 
  onUserChange(event, album: FormGroup) {
    const title = album.get('title');

    title.setValue(null);
    title.markAsUntouched();
    // Notice the ngIf at the title cell definition. The user with id 3 can't set the title of the albums
  }

  applyFilter(filterValue: string) {
    this.dataSource.filter = filterValue.trim().toLowerCase();
  }
}

HTML

<mat-form-field>
  <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>

<form [formGroup]="form" autocomplete="off">
    <mat-table [dataSource]="dataSource">

      <!--- Note that these columns can be defined in any order.
            The actual rendered columns are set as a property on the row definition" -->

      <!-- Id Column -->
      <ng-container matColumnDef="id">
        <mat-header-cell *matHeaderCellDef> Id </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.get('id').value}}. </mat-cell>
      </ng-container>

      <!-- User Column -->
      <ng-container matColumnDef="userId">
        <mat-header-cell *matHeaderCellDef> User </mat-header-cell>
        <mat-cell *matCellDef="let element" [formGroup]="element">
          <mat-form-field floatLabel="never">
            <mat-select formControlName="userId" (selectionChange)="onUserChange($event, element)" required>
              <mat-option *ngFor="let user of users" [value]="user.id">
                {{ user.username }}
              </mat-option>
            </mat-select>
          </mat-form-field>
        </mat-cell>
      </ng-container>

      <!-- Title Column -->
      <ng-container matColumnDef="title">
        <mat-header-cell *matHeaderCellDef> Title </mat-header-cell>
        <mat-cell *matCellDef="let element;" [formGroup]="element">
          <mat-form-field floatLabel="never" *ngIf="element.get('userId').value !== 3">
            <input matInput placeholder="Title" formControlName="title" required>
          </mat-form-field>
        </mat-cell>
      </ng-container>

      <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
      <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
    </mat-table>
</form>
<mat-accordion>
  <mat-expansion-panel>
    <mat-expansion-panel-header>
      <mat-panel-title>
        Form value
      </mat-panel-title>
    </mat-expansion-panel-header>
    <code>
      {{form.value | json}}
    </code>
  </mat-expansion-panel>
</mat-accordion>
Snæbjørn
  • 10,322
  • 14
  • 65
  • 124
2

Create a function that calculates the actual index.

getActualIndex(index : number)    {
    return index + pageSize * pageIndex;
}

You can get the pageSize and pageIndex from the paginator. Then, in the template use this function:

formControlName="getActualIndex(index)"
double-beep
  • 5,031
  • 17
  • 33
  • 41
-1

For matSort to work the type definition is important, at least that's what I found. So with type as any in the code :

dataSource: MatTableDataSource<any>; 

Will not work. There has to be a type defined here to make it work, try to define a interface and pass it in the generics of MatTableDataSource .

Also matColumnDef has to match the property name of the defined type.

George Stocker
  • 57,289
  • 29
  • 176
  • 237