Common 'wrapper' component for ngx-datatable

5.2k views Asked by At

Some introduction:

We are currently developing an application based on Angular2 that is quite data-heavy. In order to show this data, we decided to give ngx-datatables a try. Plenty of components will be needed showing data in grids. We added a customized footer template as well as a kind of customized header showing a page size selector using a <select> element.

The number of markup lines grew quite a lot, therefore we would like to move the definition <ngx-datatable> with header and footer to a separate grid component. Now we would like to reuse that component by allowing the developer using the grid to simply define the columns in markup, to have full flexibility when it comes to the column content.

The idea is to have a commonly used grid component that only asks for data as input and renders it. The typical functionality (server-side sorting and paging) in the grid should only exist once in the grid component. The component which uses the grid component should just provide the data which the grid components subscribes to, that's it.

What we have at the moment:

Common grid component with selector 'grid' defined in .ts file

<div class="gridheader">
    ... page size selector and other elements ...
</div>
<ngx-datatable
  class="material"
  [columnMode]="'force'"
  [rows]="data"
  [headerHeight]="'auto'"
  [footerHeight]="'auto'"
  [rowHeight]="'auto'"
  [externalPaging]="true"
  [externalSorting]="true"
  [count]="totalElements"
  [offset]="currentPageNumber"
  [limit]="pageSize"
  [loadingIndicator]="isLoading"
  (page)='loadPage($event)'
  (sort)="onSort($event)">

  <ng-content>
  </ng-content>

  <ngx-datatable-footer>
    <ng-template 
      ngx-datatable-footer-template 
      let-rowCount="rowCount"
      let-pageSize="pageSize"
      let-selectedCount="selectedCount"
      let-curPage="curPage"
      let-offset="offset">
      <div style="padding: 5px 10px">
        <div>
          <strong>Summary</strong>: Gender: Female
        </div>
        <hr style="width:100%" />
        <div>
          Rows: {{rowCount}} |
          Size: {{pageSize}} |
          Current: {{curPage}} |
          Offset: {{offset}}
        </div>
      </div>
    </ng-template>
  </ngx-datatable-footer>

</ngx-datatable>

Specific grid

<grid (onFetchDataRequired)="fetchDataRequired($event)">

  <ngx-datatable-column prop="Id" name=" ">
    <ng-template let-value="value" ngx-datatable-cell-template>
      <a [routerLink]="['edit', value]" class="btn btn-sm btn-outline-primary">
        <i class="fa fa-pencil" aria-hidden="true"></i>
      </a>
    </ng-template>
  </ngx-datatable-column>
  <ngx-datatable-column name="CreatedBy" prop="CreatedBy">
    <ng-template let-value="value" ngx-datatable-cell-template>
      {{value}}
    </ng-template>
  </ngx-datatable-column>

  ... even more columns ...   

</grid>

We tried to use <ng-content></ng-content> for the columns but no luck, the grid is just not rendered, I guess becauseno no columns are defined.

Is there a way of not repeating the same code for the grid definition over and over again and to implement some kind of wrapper that takes care of the common markup?

Grateful for any input. Thanks in advance!

Update

We managed to do it via the .ts file and a ng-template in the markup, but we would prefer to define columns only in the markup. Any idea anyone?

1

There are 1 answers

7
krawuzikapuzi On BEST ANSWER

We decided to go with the solution having the column definitions in the .ts file.

Here is our solution:

Common grid component with selector 'grid' defined in .ts file

grid.component.html

<div class="ngx-datatable material">
    <div class="datatable-footer datatable-footer-inner">
        <div class="page-count">
            Show
            <select (change)="onLimitChange($event.target.value)" class="page-limit">
                <option
                    *ngFor="let option of pageLimitOptions"
                    [value]="option.value"
                    [selected]="option.value == currentPageLimit">
                    {{option.value}}
                </option>
            </select>
            per page
        </div>
    </div>
    <ngx-datatable
        class="material striped"
        [columns]="columns"
        [columnMode]="'force'"
        [rows]="gridModel.Data"
        [headerHeight]="'auto'"
        [footerHeight]="'auto'"
        [rowHeight]="'auto'"
        [externalPaging]="true"
        [externalSorting]="true"
        [count]="gridModel?.TotalElements"
        [offset]="gridModel?.CurrentPageNumber"
        [limit]="gridModel?.PageSize"
        [loadingIndicator]="isLoading"
        (page)='loadPage($event)'
        (sort)="onSort($event)">
    </ngx-datatable>
</div>
<app-spinner [isRunning]="isLoading"></app-spinner>
<ng-template #emptyTemplate let-row="row" let-value="value"></ng-template>
<ng-template #idAnchorEditTemplate let-row="row" let-value="value">
    <a [routerLink]="['edit', value]" class="btn btn-sm btn-outline-primary">
        <i class="fa fa-pencil" aria-hidden="true"></i>
    </a>
</ng-template>
<ng-template #dateTemplate let-row="row" let-value="value">
    {{value | date:'dd.MM.yyyy' }}
</ng-template>
<ng-template #dateTimeTemplate let-row="row" let-value="value">
    {{value | date:'dd.MM.yyyy HH:mm:ss' }}
</ng-template>

grid.component.ts

import { Component, Injectable, Input, Output, OnInit, OnDestroy, EventEmitter, ViewChild, TemplateRef } from '@angular/core';
import { NgxDatatableModule, DatatableComponent } from '@swimlane/ngx-datatable';
import { TableColumn } from '@swimlane/ngx-datatable/release/types';

import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/Rx';

import { GridModel } from '../grid/grid-model.model'

@Injectable()
@Component({
    selector: 'grid',
    templateUrl: './grid.component.html'
})
export class GridComponent<T> implements OnInit, OnDestroy {
    @Input()
    columns: TableColumn[];

    private _gridModelInput = new BehaviorSubject<GridModel<T>>(undefined);

    @ViewChild('emptyTemplate') 
    public emptyTemplate: TemplateRef<any>;

    @ViewChild('idAnchorEditTemplate') 
    public idAnchorEditTemplate: TemplateRef<any>;

    @ViewChild('dateTemplate') 
    public dateTemplate: TemplateRef<any>;

    @ViewChild('dateTimeTemplate') 
    public dateTimeTemplate: TemplateRef<any>;

    // change data to use getter and setter
    @Input()
    set gridModelInput(value) {
        // set the latest value for _data BehaviorSubject
        if (value !== undefined) {
            this._gridModelInput.next(value);
        }
    };

    get gridModelInput() {
        // get the latest value from _data BehaviorSubject
        return this._gridModelInput.getValue();
    }

    @Output()
    onFetchDataRequired = new EventEmitter<GridModel<T>>();

    private gridModel: GridModel<T>;
    private isLoading: boolean = false;
    private currentPageLimit: number = 0;
    private pageLimitOptions = [
        {value: 10},
        {value: 25},
        {value: 50},
        {value: 100},
    ];

    constructor() {
    }

    ngOnInit(): void {
        this.gridModel = new GridModel<T>();

        this._gridModelInput.subscribe(gridModel => {
            this.gridModel = gridModel;
            this.isLoading = false;
        }, err => console.log(err));

        this.loadPage();
    }

    protected loadPage(pageEvent = {offset: 0}){
        this.gridModel.CurrentPageNumber = pageEvent.offset;
        this.onFetchDataRequired.emit(this.gridModel);
        this.isLoading = true;
    }

    protected onSort(event) {
        if (this.gridModel.SortBy != event.sorts[0].prop) {
            //this means we are sorting on a new column
            //so we need to return the paging to the first page
            this.gridModel.CurrentPageNumber = 0;            
        }

        this.gridModel.SortBy = event.sorts[0].prop;
        this.gridModel.SortDir = event.sorts[0].dir;

        this.loadPage();
    }

    public onLimitChange(limit: any): void {
        this.gridModel.PageSize = this.currentPageLimit = parseInt(limit, 10);
        this.gridModel.CurrentPageNumber = 0;
        this.loadPage();
    }

    ngOnDestroy(): void {
        this._gridModelInput.unsubscribe();
    }
}

Usage of this grid component wrapper

data-grid.component.html

<grid
    (onFetchDataRequired)="fetchDataRequired($event)"
    [gridModelInput]="gridModel">
</grid>

data-grid.component.ts

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

import { GridComponent } from '../shared/grid/grid.component';
import { GridModel } from '../shared/grid/grid-model.model';
import { DataGridRowModel } from './data-gridrow.model';
import { DataFetchService } from './data-fetch.service';

@Component({
  templateUrl: 'data-grid.component.html'
})
export class DataGridComponent implements OnInit {
  @ViewChild(GridComponent) grid: GridComponent<DataGridRowModel>;
  gridModel: GridModel<DataGridRowModel> = new GridModel<DataGridRowModel>('DateCreated', 'desc');

  ngOnInit(): void {
    this.grid.columns = [
      { prop: 'Id', cellTemplate: this.grid.idAnchorEditTemplate, headerTemplate: this.grid.emptyTemplate }
      , { prop: 'CreatedBy' }
      , { prop: 'DateCreated', cellTemplate: this.grid.dateTimeTemplate, name: 'Created Date' }
    ];
  }

  constructor(private dataFetchService: DataFetchService) {
  }

  fetchDataRequired(gridModel: GridModel<DataGridRowModel>) {
    this.dataFetchService
      .getSortedPagedResults(gridModel)
      .subscribe(gridModelResponse => {
        this.gridModel = gridModelResponse;
    });
  }
}

The cool thing about it is, that it has commonly used templates pre-defined, e.g. for the id column (idAnchorEditTemplate), date columns (dateTemplate) or date/time columns (dateTimeTemplate). This allows maintenance of column templates that are used throughout the application in a single file.

One additional type that will be needed is GridModel:

export class GridModel<T> {
    PageSize: number;
    TotalElements: number;
    TotalPages: number;
    CurrentPageNumber: number;
    SortBy: string;
    SortDir: string;
    Data: Array<T>;

    constructor(defaultSortBy: string = 'Id', defaultSortDir: string = 'asc') {
        this.PageSize = 10;
        this.TotalElements = 0;
        this.TotalPages = 0;
        this.CurrentPageNumber = 0;
        this.Data = new Array<T>();

        this.SortBy = defaultSortBy;
        this.SortDir = defaultSortDir;
    }
}

Maybe someone benefit from it someday :)