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.
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?
We decided to go with the solution having the column definitions in the .ts file.
Here is our solution:
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();
}
}
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With