Tables with dynamic data using Angular and Material

Jesús Chávez Salvatiera
12 min readDec 23, 2018

--

This article aims to explain one of the best components of Angular Material. This is the MatTableModule, which in principle is a component for generating tables with an object array. In the following link we can review the documentation of Angular Material for this component:
https://material.angular.io/components/table/overview

For a better explanation of this case, we will also use another Material component in the simplest way, This will be the “Expansion Panel” . Let’s design the case then…

Imagine a simple scenario of a world that we all know, the world of Pokemon. How many of us spent hours in our Game Boys looking for these friends and collecting badges to get to the tournament. Each of us as trainers had a list of pokemon that we were trapping and also a list of badges we got.

Let’s think about this scenario, and that basically we are asked for a list of trainers where we can see what pokemon and what medals each coach has selected. So we first need to see the list of trainers, then select them and check in other lists which pokemons and bages they have. A simple mokup will like this:

Then let’s define the Trainer class, with the basic attributes and a list of pokemon and badges:

import { Badge } from './badge';import { Pokemon } from './pokemon';export class PokemonTrainer {name: string;age: number;badges: Badge[];pokemons: Pokemon[];}

Also we want to import the MatTableModule module in our app.module.ts:

import { MatTableModule } from '@angular/material';

To see the mat-table component in action we need to define first an array of Pokemon Trainer. We’ll fill our table with this information and use the attributes that we want in the correct columns. So basically our HTML template will be like this:

<table mat-table [dataSource]="dsTrainers" class="mat-elevation-z8"><ng-container matColumnDef="name"><th mat-header-cell *matHeaderCellDef> Name </th><td mat-cell *matCellDef="let trainer"> {{trainer.name}} </td></ng-container>...<tr mat-header-row *matHeaderRowDef="dcTrainers"></tr><tr mat-row *matRowDef="let row; columns: dcTrainers;"></tr></table>

This may look tricky, but it is quite simple once we understand the main concepts. Let’s highlight the important words in the code above.

So you see that we define a table as usual in html with the th,td and tr . However you also could check in the tr’s definition that we have a mat-header-row wich is basically, as its name implies, the definition of the headers of the cell. This is equals to “dcTrainers”, which we need to define in our .ts file as a string[] with the names of the columns:

dcTrainers: string[] = ['name', 'age', 'numberPokemons', 'numberBadges', 'edit'];

In the th as usual, we enter our Title for each column, and in the td we will use an iteration of the for loop that will go over the entire array of our table using the “let trainer”. The trainer will be each object that we define in our array and we could access to any of its attributes.

So maybe, the most important part will be in the definition of the array for the table, this will be done with [dataSource]=”dsTrainers”. The dsTrainers will be a variable that we define in our .ts file, this variable could be an Array of objects or a MatTableDataSource class.

So, what is the MatTableDataSource class?

The definition in the Angular Material documentation is the next:

“ Data source that accepts a client-side data array and includes native support of filtering, sorting (using MatSort), and pagination (using MatPaginator).”

So, it is a class that support all our data as an array and also give us more functionalities once we create the table, we will check this as deep as we can in the next story.

Now, we need to create our array of PokmenoTrainer and also use an Object of MatTableDataSource to fill our table.

export class AppComponent implements OnInit {dcTrainers: string[] = ['name', 'age', 'numberPokemons', 'numberBadges', 'edit'];dsTrainers: MatTableDataSource<PokemonTrainer>;
trainers: PokemonTrainer[] = [
{ name: 'Josue Bernedo', age: 18, badges: [], pokemons: [] },{ name: 'Andy Vicente', age: 14, badges: [], pokemons: [] },{ name: 'Martin Naveda', age: 14, badges: [], pokemons: [] },];constructor() {this.dsTrainers = new MatTableDataSource<PokemonTrainer>();
}
ngOnInit() {
this.updateTableTrainers();}

}

To this point I like to say that you could find all the code for this example in the next link of StackBlitz:

https://stackblitz.com/edit/angular-kj6g7p

We will use also the “Expansion Panel” in our html template, so when a user edit one trainer the panel of trainer collapse and the panel for editing get expanded.

<mat-accordion><mat-expansion-panel [expanded]='f_firstPanel' [disabled]='!f_firstPanel'><mat-expansion-panel-header><mat-panel-title>Pokemon Trainers</mat-panel-title></mat-expansion-panel-header><table mat-table [dataSource]="dsTrainers" class="mat-elevation-z8"><ng-container matColumnDef="name"><th mat-header-cell *matHeaderCellDef> Name </th><td mat-cell *matCellDef="let trainer"> {{trainer.name}} </td></ng-container><ng-container matColumnDef="age"><th mat-header-cell *matHeaderCellDef> Age </th><td mat-cell *matCellDef="let trainer"> {{trainer.age}} </td></ng-container><ng-container matColumnDef="numberPokemons"><th mat-header-cell *matHeaderCellDef> # Pokemons </th><td mat-cell *matCellDef="let trainer"> {{trainer.pokemons.length}} </td></ng-container><ng-container matColumnDef="numberBadges"><th mat-header-cell *matHeaderCellDef> # Badges </th><td mat-cell *matCellDef="let trainer"> {{trainer.badges.length}} </td></ng-container><ng-container matColumnDef="edit"><th mat-header-cell *matHeaderCellDef> Edit </th><td mat-cell *matCellDef="let trainer"><button mat-raised-button color="primary" (click)='editTrainer(trainer)'> Edit</button></td></ng-container><tr mat-header-row *matHeaderRowDef="dcTrainers"></tr><tr mat-row *matRowDef="let row; columns: dcTrainers;"></tr></table></mat-expansion-panel><mat-expansion-panel [expanded]='f_secondPanel' [disabled]='!f_secondPanel'><mat-expansion-panel-header><mat-panel-title>Editing the Pokemon Trainer</mat-panel-title></mat-expansion-panel-header></mat-expansion-panel></mat-accordion>

To use the accordion, we need to import this module in our app.module.ts:

import {MatExpansionModule} from '@angular/material/expansion';

As you can see in the html template, we use two flags that controls our panels: f_firstPanel and f_secondPanel. Both should be declared as boolean in out .ts file, and we’ll init with the value for f_firstPanel in true.

So, our app should look like this until this point:

Let’s check the table of trainers, in the column for edit we define a button. When the event of (click) happen, it will trigger a funtion that we define in our .ts file.

In the html file:

<button mat-raised-button color="primary" (click)='editTrainer(trainer)'> Edit</button>

In the .ts file:

editTrainer(trainer: PokemonTrainer) {this.selectedTrainer = trainer;this.f_firstPanel = false;this.f_secondPanel = true;this.dsPokemons.data = this.selectedTrainer.pokemons;this.dsBadges.data = this.selectedTrainer.badges;}

We use a variable that is defined as a type PokemonTrainer class, we will use this variable to save the temporary changes that we will made for each element in our array of trainers. We control the first panel to be collapsed and the second to be expanded, also there are two more Data Sources variables: the dsPokemons and the dsBadges. Each of them will be used for two tables that will show the list of pokemons and badges per each trainer. That’s why we equals the .data attribute to the .pokemons and .badges of the trainer.

Don’t forget to define this new variables:

dcPokemons: string[] = ['kdex', 'name', 'type'];dsPokemons: MatTableDataSource<Pokemon>;dcBadges: string[] = ['name', 'giver', 'description'];dsBadges: MatTableDataSource<Badge>;selectedTrainer: PokemonTrainer;
constructor() {
this.dsTrainers = new MatTableDataSource<PokemonTrainer>();this.dsPokemons = new MatTableDataSource<Pokemon>();this.dsBadges = new MatTableDataSource<Badge>();}

So now, when a user click in the edit, the first panel will collapse and some variables will fill with data of the selected trainer. Our draft mockup will look like this now:

We add a select option, so we could add some pokemons to the trainer, the same will happen with the badges. And also, we add two buttons at the bottom to cancel the edit and don’t save changes and to finish the edit and save it.

Let’s create the templates for the second panel:

<mat-expansion-panel [expanded]='f_secondPanel' [disabled]='!f_secondPanel'><mat-expansion-panel-header><mat-panel-title>Editing the Pokemon Trainer</mat-panel-title></mat-expansion-panel-header><mat-tab-group><mat-tab label="Pokemon"><h4>Pokemon Choose</h4><mat-form-field><mat-select [(ngModel)]="pokemonToAdd" placeholder="Pokemon list"><mat-option *ngFor="let pokemon of pokemons"  [value]="pokemon">{{pokemon.name}}</mat-option></mat-select></mat-form-field><button mat-raised-button (click)="addPokemon()" style="margin: 10px">Add Pokemon</button><br><table mat-table [dataSource]="dsPokemons" class="mat-elevation-z8"><ng-container matColumnDef="kdex"><th mat-header-cell *matHeaderCellDef> Kdex </th><td mat-cell *matCellDef="let pokemon"> {{pokemon.kdex}} </td></ng-container><ng-container matColumnDef="name"><th mat-header-cell *matHeaderCellDef> Name </th><td mat-cell *matCellDef="let pokemon"> {{pokemon.name}} </td></ng-container><ng-container matColumnDef="type"><th mat-header-cell *matHeaderCellDef> Type </th><td mat-cell *matCellDef="let pokemon"> {{pokemon.type}}  </td></ng-container><tr mat-header-row *matHeaderRowDef="dcPokemons"></tr><tr mat-row *matRowDef="let row; columns: dcPokemons;"></tr></table></mat-tab><mat-tab label="Badges"><h4>Badge Choose</h4><mat-form-field><mat-select [(ngModel)]="badgeToAdd" placeholder="Badge list"><mat-option *ngFor="let badge of badges"  [value]="badge">{{badge.name}}</mat-option></mat-select></mat-form-field><button mat-raised-button (click)="addBadge()" style="margin: 10px">Add Badge</button><br><table mat-table [dataSource]="dsBadges" class="mat-elevation-z8"><ng-container matColumnDef="name"><th mat-header-cell *matHeaderCellDef> Name </th><td mat-cell *matCellDef="let badge"> {{badge.name}} </td></ng-container><ng-container matColumnDef="giver"><th mat-header-cell *matHeaderCellDef> Giver </th><td mat-cell *matCellDef="let badge"> {{badge.giver}} </td></ng-container><ng-container matColumnDef="description"><th mat-header-cell *matHeaderCellDef> Description </th><td mat-cell *matCellDef="let badge"> {{badge.description}} </td></ng-container><tr mat-header-row *matHeaderRowDef="dcBadges"></tr><tr mat-row *matRowDef="let row; columns: dcBadges;"></tr></table></mat-tab></mat-tab-group><br><div style="margin: auto"><button mat-raised-button color="primary" style="margin: 30px" (click)="finishEdit()">Finish</button><button mat-raised-button color="primary" style="margin: 30px" (click)="cancelEdit()">Cancel</button></div></mat-expansion-panel>

We add two more tables for the badges and for the pokemons, also we are using the component ‘Tabs’ for material to separate both tables. We need to import also this module in the app.module.ts:

import {MatTabsModule} from '@angular/material/tabs';

So, let’s check some of the new variables and new functions that we add for this purpose:

pokemons: Array of Pokemon that we use in our select option.

pokemonToAdd: Variable that we use to push new pokemons of the select option to our array ok pokemons.

addPokemon(): Function that push a new pokemon and also updates the table of pokemons.

badges: Array of Bage that we use in our select option.

badgeToAdd: Variable that we use to push new badges of the select option to our array ok badges.

addBadge(): Function that push a new badge and also updates the table of badges.

finishEdit(): Function that finish and save the changes in the element Tariner o the array of trainers.

cancelEdit():Function that cancel all the edit and don’t save the changes in the element Tariner o the array of trainers.

Our .ts file will look like this until now:

import { Component, OnInit } from '@angular/core';import { MatTableDataSource, MatIconRegistry } from '@angular/material';import { DomSanitizer } from '@angular/platform-browser';import { PokemonTrainer } from './pokemonTrainer';import { Badge } from './badge';import { Pokemon } from './pokemon';@Component({selector: 'my-app',templateUrl: './app.component.html',styleUrls: ['./app.component.css']})export class AppComponent implements OnInit {name = 'Angular';// DataSource and Column names of tablesdcTrainers: string[] = ['name', 'age', 'numberPokemons', 'numberBadges', 'edit'];dsTrainers: MatTableDataSource<PokemonTrainer>;dcPokemons: string[] = ['kdex', 'name', 'type'];dsPokemons: MatTableDataSource<Pokemon>;dcBadges: string[] = ['name', 'giver', 'description'];dsBadges: MatTableDataSource<Badge>;// VariablesselectedTrainer: PokemonTrainer;pokemonToAdd: Pokemon;badgeToAdd: Badge;trainers: PokemonTrainer[] = [{ name: 'Josue Bernedo', age: 18, badges: [], pokemons: [] },{ name: 'Andy Vicente', age: 14, badges: [], pokemons: [] },{ name: 'Martin Naveda', age: 14, badges: [], pokemons: [] },];// Flags that control the expansion panelf_firstPanel = false;f_secondPanel = false;pokemons: Pokemon[] = [{ kdex: 1, name: 'Bulbasaur', type: 'Grass' },{ kdex: 4, name: 'Charmander', type: 'Fire' },{ kdex: 33, name: 'Nidorino', type: 'Poison' },{ kdex: 37, name: 'Vulpix', type: 'Fire' },{ kdex: 79, name: 'Slowpoke', type: 'Water' },];badges: Badge[] = [{ name: 'Boulder Badge', giverName: 'Brock', description: 'It is a simple gray octagon' },{ name: 'Cascade Badge', giverName: 'Misty', description: 'It is in the shape of a light blue raindrop' },{ name: 'Thunder Badge', giverName: 'Lt. Surge', description: 'It is in the shape of an eight-pointed gold star with an orange octagon in the center' },{ name: 'Rainbow Badge', giverName: 'Koga', description: 'It is shaped like a flower, showing grass, with rainbow colored petals' },];constructor() {this.dsTrainers = new MatTableDataSource<PokemonTrainer>();this.dsPokemons = new MatTableDataSource<Pokemon>();this.dsBadges = new MatTableDataSource<Badge>();}ngOnInit() {this.f_firstPanel = true;this.updateTableTrainers();}// Button InteractioneditTrainer(trainer: PokemonTrainer) {this.selectedTrainer = trainer;this.f_firstPanel = false;this.f_secondPanel = true;this.dsPokemons.data = this.selectedTrainer.pokemons;this.dsBadges.data = this.selectedTrainer.badges;}addPokemon() {this.selectedTrainer.pokemons.push(this.pokemonToAdd);this.dsPokemons.data = this.selectedTrainer.pokemons;}addBadge() {this.selectedTrainer.badges.push(this.badgeToAdd);this.dsBadges.data = this.selectedTrainer.badges;}cancelEdit() {this.f_firstPanel = true;this.f_secondPanel = false;this.selectedTrainer = null;}finishEdit() {this.f_firstPanel = true;this.f_secondPanel = false;const index = this.findIndexofTrainer();this.trainers[index] = this.selectedTrainer;this.updateTableTrainers();this.selectedTrainer = null;}findIndexofTrainer(): number {const index = this.trainers.findIndex(t => t.name === this.selectedTrainer.name);return index;}updateTableTrainers() {this.dsTrainers.data = this.trainers;}}

It’s quite simple, each time we add a pokemon or badge to our trainer, we push the element to the the respective array of the selectedTrainer that will save temporarily the data. To update any of these tables we need to update the .data attribute of the DataSources:

this.dsBadges.data = this.selectedTrainer.badges;

Then if we want to finish the edition, we click in the finish or cancel buttons. The finish button will first find the index of the array were is the selectedTariner, aftter this it will update the information of this trainer and the table. And will also collapse the second panel and expand the first.

finishEdit() {this.f_firstPanel = true;this.f_secondPanel = false;const index = this.findIndexofTrainer();this.trainers[index] = this.selectedTrainer;this.updateTableTrainers();this.selectedTrainer = null;}

The cancel button will only control the panels and set the selected Trainer in null, so no changes will be reflected in our trainers table:

cancelEdit() {this.f_firstPanel = true;this.f_secondPanel = false;this.selectedTrainer = null;}

Some of you may realized that we have an error in our code:

When we click on cancel, all the changes that we made editing remains and also update the trainers table. Why this happens?

Well the answer will be in the assignment of the data. Check this part of our code:

editTrainer(trainer: PokemonTrainer) {this.selectedTrainer = trainer;this.f_firstPanel = false;this.f_secondPanel = true;this.dsPokemons.data = this.selectedTrainer.pokemons;this.dsBadges.data = this.selectedTrainer.badges;}

We basically equal the selectedTrainer to the trainer that comes from our dsTrainers.data that is also equals to the trainers array.

So what?, that’s what we want rigth? …

Well yes, but here comes one of the concepts that is such a headache when start to learn programming: the variable assignment. I don’t want to go deep in this concept because I’m sure there are other articles that will explain it better, I let you one here and please give it a check if you don’t know this concept yet:

http://www.syntaxsuccess.com/viewarticle/javascript-variable-assignment-explained

So going back to our code, when we create the Object of the trainers array and we equal to the dsTrainers.data we made that they point to same location in memmory. Same happens when we equals the trainer that came for the Data Source to the selectedTrainer varible, so each time we change one of them the data in the same location of memory change and get reflected in all the variables.

To avoid this we need to create a new Object for our selected Trainer and assign the attributes one by one. There is a simplest way to do this by clonning the Object, this article give some ways to do that:

I will use the one that uses JSON:

function jsonCopy(src) {
return JSON.parse(JSON.stringify(src));
}

So in our code we change the line:

this.selectedTrainer = trainer;

to

this.selectedTrainer = JSON.parse(JSON.stringify(trainer));

And will solve it. Let’s check now:

Finish:

Cancel:

We are going to see more of the Angular Material table in another story, thanks for reading this :) !

--

--