Angular material table with angular 7 - Defining columns using ngFor

I am going to ramble slightly about this approach in general, below in bold will be the implementation that has best worked for me so far, so if that is why you are here then keep scrolling, scrolling, scrolling.

Another point you should know before going through the trouble of implementing this, you will have to make some adjustments to your sorting behaviour, this is explained towards the end of the post, short version: it is a relatively trivial adjustment.

Angular Material is something you would be hard pressed to have missed if you have been around Angular for any length of time, you'd also be equally as hard-pressed not to have used the Angular Material data table component.

If you haven't come across it then no worries, have a little read to see if Angular Material is useful to you and then come back.

So let's talk about the column definitions, to do that let's talk around a snippet of a template:

 <!-- Name Column -->
  <ng-container matColumnDef="name">
    <th mat-header-cell *matHeaderCellDef> Name </th>
    <td mat-cell *matCellDef="let element"> {{element.name}} </td>
  </ng-container>

  <!-- Weight Column -->
  <ng-container matColumnDef="weight">
    <th mat-header-cell *matHeaderCellDef> Weight </th>
    <td mat-cell *matCellDef="let element"> {{element.weight}} </td>
  </ng-container>

I have shamelessly copy and pasted directly from the angular material website, after all why not, if we are going to focus on an example let's get that example from the creators of what we are using!

Anyhow on to column definitions, in the example we see that we have a container for each column in our table, I have only copied two of the column definitions, but it should come as no surprise that for each column we will need another container like the above.

Now some projects are ok with declaring each and every column in this way, others not so much and prefer to use ngFor to generate these containers with a backing array of objects that define a column definition, a column header and where to get a value for each row.

I, personally do not mind either way although given a choice I would keep static columns static and not treat them as dynamic columns.
For one reason or other projects seem to prefer iterating over column definitions rather than defining each column statically in their template regardless of the nature of the column in general and that is perfectly fine as far as I can tell.

That being said the proof is in the pudding or in our case the implementation, and I have seen many different ways of implementing this, some have had some severe drawbacks; the first drawback that seems relatively common is not being able to define values within nested objects.

This leads to having to transform your data just to facilitate being able to show data in a table, I don't like this.
I  believe that in the case of a table we should not have to worry about an additional constraint on the form of our data merely to allow the looping of column definitions from data on the typescript side.

So, for me, this means I must be able to declare values for each column using nested data if I want to and any implementation that precludes this means I am not interested in it unless my data is intrinsically flat.

Now on to the implementation (let me preface by apologising, i have yet to find a decent way to structure code within my posts :( ).

Let's define our template first:

1  | <div class="mat-elevation-z8">
2  |     <table mat-table [dataSource]="dataSource">
3  |         <ng-container *ngFor="let columnDefinition of columnDefinitions" 
4  |                                  matColumnDef={{columnDefinition.matColumnDef}}>
5  |                 <th mat-header-cell *matHeaderCellDef mat-sort-header> 
6  |                     {{columnDefinition.columnHeaderName}} 
|                 </th>
|                 <td mat-cell *matCellDef="let row"> 
9  |                     {{columnDefinition.value(row)}} 
10|                 </td>
11|         </ng-container>
12|   
13|         <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
14|         <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
15|   </table>
16| </div>

Hopefully, your first observation is that the above is very simple.

Our first point of call will be line 3, this is where our *ngFor directive is placed, it iterates over an array of column definitions which will drive what comes next, we will see the form that takes later on.

Line 4 is where we provide a value for the matColumnDef which is gained from the current columnDefinition, so far so simple.

Line 6 is where we declare the value for our column header, which again we get from columnDefinition, yet again simple stuff.

Line 9 is where we get somewhat a bit of 'complexity', but it is more than enough to handle, we need to be able to get values within nested arrays and objects, to do this we specify a function which takes in the row value and gets us back a value to show.

Line 13 is where we specify what columns we want to actually show, this is another array that we will see in just a moment.

So now on to the typescript side of this, I thought I would start with this snippet:

1 | displayedColumns: string[] = ['species', 'example', 'noOfMembers'];
2 |  private columnDefinitions = [
3 |      {
4 |          matColumnDef : "species", 
5 |          columnHeaderName: "Species", 
6 |          value: (row) => `${row.type}`
7 |      },
8 |      {
9 |          matColumnDef : "example", 
10|          columnHeaderName: "Example", 
11|         value: (row) => `${row.members[0].species}`
12|      },
13|      {
14|          matColumnDef : "noOfMembers", 
15|          columnHeaderName: "Number Of Members", 
16|          value: (row) => `${row.facts.numberOfMembers}`
17|      }
18|  ];

Line 1 is the array that dictates which columns are shown and which are not, you can manipulate this over time to change the columns displayed, but we will keep it simple and keep this static.

Everything up to line 6 should be easy enough, line 6 is merely a function that is called to get back the value to be displayed for each row, as you will remember we pass the current row through to this function in our template and for each column definition here we provide the path to get the value.

That's it, doing this we end up with the following output :



Hopefully, you will find some more useful data to show nevertheless I hope this post has proved useful, as always I value any comments you may have that might improve upon this post, so get commenting!

I have precluded extra functionality such as sorting and pagination in this post if you want to add these functionalities, then please go ahead, pagination seems to be unaffected by this implementation.

Sorting is a different story, unfortunately sorting functionality makes the following assumptions:

`By default, the MatTableDataSource sorts with the assumption that the sorted column's name matches the data property name that the column displays.` - Documentation
Fortunately, though we have a way around this and while it is somewhat bloated it really doesn't complicate much of anything, just override your data sources sortingDataAccessor function like so:

this.dataSource.sortingDataAccessor = (row, property) => {
      switch(property) {
        case 'example': return row.members[0].species;
        case 'numberOfMembers': return row.facts.numberOfMembers;
        default: return row[property];
   }
};

Notice that I have not included our first column in here, the reason is we can preclude column definitions from this switch statement that are properties that are not nested by  ensuring that the sorted column name matches the data property name.

In the example above i would just change matColumnDef from 'species' to 'type' which save a line of code and probably makes for a more readable solution anyway.

This does add to the maintainability of this table but the alternative is to only have a flat data structure which is not always possible given we do not always have control of what is sent from a server.

We could map our data to a flat structure once we receive it but this incurs a multitude of costs - complexity (depending on how complicated it is to flatten your data), readability, maintainability, scaleability and potentially performance.

Finally, we could decide not to bother with dynamic columns instead opting to just declare a column definition one by one in our template but this is probably already been ruled out, and you probably have a legitimate need for it such as dynamic columns that can change over time.

Finally, the following is all the code you need across all files so you can have a play and get your data working for the implementation, the template, however, is already provided above.

app.component.ts

import {Component, OnInit, ViewChild} from '@angular/core';
import {MatTableDataSource} from '@angular/material';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  
  dataSource: MatTableDataSource<any>;
  
  private species = [
        {
          type: "Bird",
          facts: {
            numberOfMembers: "9000 - 10000"
          },
          members: [
            {species: "Hawk"},
            {species: "Eagle"},
            {species: "Crow"}
          ]
        }
  ];

  displayedColumns: string[] = ['species', 'example', 'noOfMembers'];
  private columnDefinitions = [
      {
          matColumnDef : "species", 
          columnHeaderName: "Species", 
          value: (row) => `${row.type}`
      },
      {
          matColumnDef : "example", 
          columnHeaderName: "Example", 
          value: (row) => `${row.members[0].species}`
      },
      {
          matColumnDef : "noOfMembers", 
          columnHeaderName: "Number Of Members", 
          value: (row) => `${row.facts.numberOfMembers}`
      }
  ];
  constructor() {
    this.dataSource = new MatTableDataSource(this.species);
  }
}


app.component.scss

.example-container {
  height: 400px;
  width: 550px;
  overflow: auto;
}

table {
  width: 800px;
}

td.mat-column-star {
  width: 20px;
  padding-right: 8px;
}

th.mat-column-position, td.mat-column-position {
  padding-left: 8px;
}

.mat-table-sticky:first-child {
  border-right: 1px solid #e0e0e0;
}

.mat-table-sticky:last-child {
  border-left: 1px solid #e0e0e0;

}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'
import { MatTableModule } from '@angular/material/table';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatPaginatorModule} from '@angular/material/paginator';
import {MatInputModule} from '@angular/material/input';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    BrowserAnimationsModule,
    MatTableModule,
    MatFormFieldModule,
    MatPaginatorModule,
    MatInputModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})

export class AppModule { }



Comments


  1. I like your post very much. It is very much useful for my research. I hope you to share more info about this. Keep posting angularjs online training india

    ReplyDelete
  2. table mat-table matSort [dataSource]="dynamicdataSource" class="mat-elevation-z8">


    th mat-header-cell *matHeaderCellDef mat-sort-header>
    {{headertext(columnname)}}
    /th>
    td mat-cell *matCellDef="let row">
    {{row[columnname]}}
    /td>

    tr mat-header-row *matHeaderRowDef="displayedColumns">tr
    tr mat-row *matRowDef="let row; columns: displayedColumns;">tr


    table>

    ReplyDelete

Post a Comment

Popular posts from this blog

Creating contextual drop downs using RxJs, Angular 7 and Angular Material

From nothing to an Angular 7 app running in 5 minutes with AWS