49

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.

0

5 Answers 5

34

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

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

9 Comments

Wont that produce the index from the columns and not the rows?
You can check it by printing index stackblitz.com/edit/…
Seemingly works fine. Strange, if this is intentional, it is not intuitive. But thank you.
this generates problems when using pagination and filtering, I'm struggling with that right now... Pagination is easy to solve, but not filtering
For newer versions of Material, you will need to change let index = index to let index = dataIndex
|
26

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]
        });
      }

5 Comments

Hi, nice answer, can you please edit your answer to add mat-error implementation?
How to implement paginator in this example?
For pagination :Pagination To paginate the table's data, add a <mat-paginator> 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 material.angular.io/components/table/overview#datasource
I am getting: Cannot find control with name: '0'
I was missing formArrayName="users". Now I am getting: Error: Cannot find control with path: 'users -> 0'
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>

4 Comments

Good answer, but matSort does not work i tried all ways and i did a lot search to find solution but i failed can you please help me to apply sorting on the columns
Hello Snæbjørn: can you please help me, the sorting does not work
@MohamadChami the filter works for id and userId. Not sure why it doesn't work for the others. Probably some internal ngForm stuff.
Hi @Snæbjørn the filter you have applied doesn't work. do you have any working filters over formControls?
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)"

Comments

-2

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.