IT'S PRETTY EASY TO MASTER ANGULAR MATERIAL TABLES. UNTIL THE UX ISSUES COME

How to master angular material tables

Oct 2nd 2019
It's pretty easy to master angular material tables. Until the UX issues come

 

Introduction

When your shinny new table deploys to Production, you are happy. The pagination, filtering and all your cute buttons seem to be in perfect shape, and even the actual table loads so fast that the user will be amazed!

Well.. at least in your local machine.. then, comes the first bug report from the user...apparently he tried to click all over the place, as soon as something showed up on the screen.

But.."he is not supposed to use it like that...". Well, you just got your first head-to-head with one of UI/UX's major rules: "You are not the user".

At this point you realize that you need somehow to block the table (or not even show it), while you are still loading your components, or even, fetching data from some API (of course you could do this using Guards...more to come in a future article).

One possible solution would be to present a spinner, instead of the actual table results. And what if you have an empty table, shouldn't you show some kind of message to the user? Let's find out...

 

Application Architecture

For the purpose of presenting something more meaningful, we are going to use The Cat API to feed our table (since everyone loves cats, right?). The information fetched from the API will be stored in our NgRx/store, and then it will be presented by our App Component, using a Material Table.

The final folder structure is as simple as it looks:

 

Implementation

api.service.ts

After generating a new Angular Project , you need to create a service under src/app/services/api.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { delay } from 'rxjs/operators';
​
@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private APIGetBreed = 'https://api.thecatapi.com/v1/breeds';
  constructor(private http: HttpClient) {}
​
  getBreeds() {
    let headers = new HttpHeaders();
    headers = headers.append(
      'x-api-key',
      'YOUR_OWN_API_KEY_SHOULD_GO_HERE'
    );
    return (
      this.http
        .get(this.APIGetBreed, {
          headers: headers
        })
        /**
         * this delay is just to simulate a "slow-connection" scenario. Should be removed
         */
        .pipe(delay(4000))
    );
  }
}

 

 

Here you need to kindly enter your own x-api-key. You can get it on the Cat API website.

As you can see, we are doing a delay of 4s to simulate a possible slow connection. This will let us see the spinner in action.

NgRx/store

For NgRx we will be using version 8, that has a lot less boilerplate than the previous versions. I will not get into much detail about this, since there are already great articles out there that explain all the differences between this and the older versions.

As you could see above, NgRx/store lives inside a store folder under src/. Additionally, in this folder we have our "cat structure" defined in src/app/store/cat.model.ts.

 

cat.action.ts

Here we define four actions:

  • Get Cats - can be used to get the current state;
  • Begin Get Cats - this will "trigger" the side effect, so we can get the list of cats from the API;
  • Success Get Cats - will be used to take the API's payload and generate a new state;
  • Error - in case something goes unexpected;

 

import { createAction, props } from '@ngrx/store';
import Cat from './cat.model';
​
export const GetCatsAction = createAction('[Cat] - Get Cats');
​
export const BeginGetCatsAction = createAction('[Cat] - Begin Get Cats');
​
export const SuccessGetCatsAction = createAction(
  '[Cat] - Success Get Cats',
  props<{ payload: Cat[] }>()
);
​
export const ErrorCatAction = createAction('[Cat] - Error', props<Error>());

 

 

cat.reducers.ts

It is pretty self-explanatory, and it looks like this:

import { Action, createReducer, on, createFeatureSelector, createSelector, State } from '@ngrx/store';
import * as CatActions from './cat.action';
import CatState, { initializeState } from './cat.state';
​
export const intialState = initializeState();
​
const reducer = createReducer(
  intialState,
  on(CatActions.GetCatsAction, state => state),
  on(CatActions.SuccessGetCatsAction, (state: CatState, { payload }) => {
    return { ...state, cats: payload, isLoaded: true };
  }),
  on(CatActions.ErrorCatAction, (state: CatState, error: Error) => {
    console.log(error);
    return { ...state, CatError: error };
  })
);
​
export function CatReducer(state: CatState | undefined, action: Action) {
  return reducer(state, action);
}
 

 

cat.effects.ts

As you can see, we are using the Begin Get Cats action as a trigger to get the list of "breeds" from our API.

Additionally you can see that we have a commented line, which can be handy to test a use case where the API does not return any data.

Then we dispatch a Success Get Cats action with the actual payload:

import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { ApiService } from '../services/api.service';
import * as CatActions from './cat.action';
import { Observable, of } from 'rxjs';
import Cat from './cat.model';
​
@Injectable()
export class CatEffects {
  constructor(private action$: Actions, private apiService: ApiService) {}
​
  GetCats$: Observable<Action> = createEffect(() =>
    this.action$.pipe(
      ofType(CatActions.BeginGetCatsAction),
      mergeMap(action =>
        this.apiService.getBreeds().pipe(
          map((data: Cat[]) => {
            // data = []; // little hack to test an empty response from the API
            return CatActions.SuccessGetCatsAction({ payload: data });
          }),
          catchError((error: Error) => {
            return of(CatActions.ErrorCatAction(error));
          })
        )
      )
    )
  );
}
 

 

cat.state.ts

Besides the common array of Cats, and a possible error, we have an "isLoaded" variable. At first sight this might not seem so useful, but it will let us distinguish between an empty table that still has not loaded, and an empty table that has correctly load, and that has really no data to show.

import Cat from './cat.model';
​
export default class CatState {
  cats: Array<Cat>;
  catError: Error;
  isLoaded: false;
}
​
export const initializeState = () => {
  return { cats: Array<Cat>() };
};
 

 

app.component.html

So here we have three possible "states":

  • A populated Material Table;
  • A spinner, while the data is still loading;
  • A message saying that there is no data to load.

We get each of these by looking at the MatTableDataSource length, and at our "isLoaded" variable, regarding our current store state.

<h1>Material Table Loader with NgRx 8</h1>
<h4>Cat List Example</h4>
​
<div *ngIf="!isDataSourceEmpty(); else loadingOrEmpty">
  <table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <th class="medium-th" mat-header-cell *matHeaderCellDef> Name </th>
      <td mat-cell *matCellDef="let element"> {{element.name}} </td>
    </ng-container>
​
    <!-- Origin Column -->
    <ng-container matColumnDef="origin">
      <th class="medium-th" mat-header-cell *matHeaderCellDef> Origin </th>
      <td mat-cell *matCellDef="let element"> {{element.origin}} </td>
    </ng-container>
​
    <!-- Temperament Column -->
    <ng-container matColumnDef="temperament">
      <th class="large-th" mat-header-cell *matHeaderCellDef> Temperament </th>
      <td mat-cell *matCellDef="let element"> {{element.temperament}} </td>
    </ng-container>
​
    <!-- Life Span Column -->
    <ng-container matColumnDef="life_span">
      <th class="small-th" mat-header-cell *matHeaderCellDef> Life Span </th>
      <td mat-cell *matCellDef="let element"> {{element.life_span}} </td>
    </ng-container>
​
    <!-- Alt Names Column -->
    <ng-container matColumnDef="alt_names">
      <th class="small-th" mat-header-cell *matHeaderCellDef> Alt Names </th>
      <td mat-cell *matCellDef="let element"> {{element.alt_names}} </td>
    </ng-container>
​
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</div>
​
<div [style.visibility]="isCatListLoaded && !isDataSourceEmpty() ? 'visible' : 'hidden'">
  <mat-paginator [pageSize]="5" [pageSizeOptions]="[5, 10, 20]" data-cy="tablePaginator"> </mat-paginator>
</div>
<div *ngIf="isCatListLoaded && isDataSourceEmpty()" class="table-loading-empty text-center">
  <img class="center" [src]="emptyListimg" height=200 alt="empty list">
  <h3><i>Oops</i> it seems there are no cats in the box...</h3>
</div>
​
<!-- if the cat list is still loading-->
<ng-template #loadingOrEmpty>
  <div *ngIf="!isCatListLoaded">
    <mat-progress-spinner color="primary" mode="indeterminate" style="margin:0 auto; margin-top:150px">
    </mat-progress-spinner>
  </div>
</ng-template>
 

 

Conclusion

Of course we could improve our Component even more, since until this point we are not dealing with possible errors originated by the API.

You can clone and play with the actual project here: https://github.com/helio-freitas/material-loader-with-ngrx

Thanks!

Written by Hélio Freitas /  Software Developer at Cleverti

background

Cleverti is now exclusive distributor of ez publish

Back to News.. Next Article