Skip to content

Commit

Permalink
feat(table): add generic table component
Browse files Browse the repository at this point in the history
  • Loading branch information
mbarbeau committed Aug 7, 2017
1 parent 7c678e3 commit 2bdafd8
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 8 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@angular/core": "4.1.3",
"@angular/forms": "4.1.3",
"@angular/http": "4.1.3",
"@angular/material": "^2.0.0-beta.7",
"@angular/material": "^2.0.0-beta.8",
"@angular/platform-browser": "4.1.3",
"@ngx-translate/core": "^6.0.1",
"@types/jspdf": "^1.1.31",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export * from './module';

export * from './collapsible';
export * from './confirm-dialog';
export * from './clickout';
export * from './clone';
export * from './keyvalue';
export * from './list';
export * from './panel';
export * from './sidenav';
export * from './spinner';
export * from './table';
20 changes: 16 additions & 4 deletions src/lib/shared/module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CdkTableModule } from '@angular/cdk';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
Expand All @@ -8,17 +9,20 @@ import { TranslateModule } from '@ngx-translate/core';

import { ClickoutDirective } from './clickout';
import { CollapsibleComponent, CollapseDirective } from './collapsible';
import { ConfirmDialogComponent, ConfirmDialogService } from './confirm-dialog';
import { ClonePipe } from './clone';
import { KeyvaluePipe } from './keyvalue';
import { ListComponent, ListItemDirective } from './list';
import { PanelComponent } from './panel';
import { SidenavShimDirective } from './sidenav';
import { SpinnerComponent, SpinnerBindingDirective } from './spinner';
import { TableComponent } from './table';


@NgModule({
imports: [
CommonModule,
CdkTableModule,
FormsModule,
ReactiveFormsModule,
HttpModule,
Expand All @@ -43,27 +47,35 @@ import { SpinnerComponent, SpinnerBindingDirective } from './spinner';
PanelComponent,
SidenavShimDirective,
SpinnerComponent,
SpinnerBindingDirective
SpinnerBindingDirective,
TableComponent
],
declarations: [
ClickoutDirective,
CollapsibleComponent,
CollapseDirective,
ConfirmDialogComponent,
ClonePipe,
KeyvaluePipe,
ListComponent,
ListItemDirective,
PanelComponent,
SidenavShimDirective,
SpinnerComponent,
SpinnerBindingDirective
SpinnerBindingDirective,
TableComponent
],
entryComponents: [
ConfirmDialogComponent
],
providers: [
ConfirmDialogService
]
})
export class IgoSharedModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: IgoSharedModule,
providers: []
ngModule: IgoSharedModule
};
}
}
5 changes: 5 additions & 0 deletions src/lib/shared/table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './table.component';
export * from './table-database';
export * from './table-datasource';
export * from './table-model.interface';
export * from './table-action-color.enum';
5 changes: 5 additions & 0 deletions src/lib/shared/table/table-action-color.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum TableActionColor {
primary,
accent,
warn
}
30 changes: 30 additions & 0 deletions src/lib/shared/table/table-database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {BehaviorSubject} from 'rxjs/BehaviorSubject';

export class TableDatabase {
/** Stream that emits whenever the data has been modified. */
dataChange: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]);
get data(): any[] { return this.dataChange.value; }

constructor(data?) {
if (data) {
this.dataChange.next(data);
}
}

set(data) {
this.dataChange.next(data);
}

add(item) {
const copiedData = this.data.slice();
copiedData.push(item);
this.set(copiedData);
}

remove(item) {
const copiedData = this.data.slice();
const index = copiedData.indexOf(item);
copiedData.splice(index, 1);
this.set(copiedData);
}
}
65 changes: 65 additions & 0 deletions src/lib/shared/table/table-datasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { DataSource } from '@angular/cdk';
import { MdSort } from '@angular/material';

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

import { TableDatabase, TableModel } from './index';

export class TableDataSource extends DataSource<any> {
_filterChange = new BehaviorSubject('');
get filter(): string { return this._filterChange.value; }
set filter(filter: string) { this._filterChange.next(filter); }

constructor(private _database: TableDatabase,
private _model: TableModel,
private _sort: MdSort) {
super();
}

/** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<any[]> {
if (!this._database) { return Observable.merge([]); }
const displayDataChanges = [
this._database.dataChange,
this._filterChange,
this._sort.mdSortChange
];

return Observable.merge(...displayDataChanges)
.map(() => {
return this.getFilteredData(this._database.data);
}).map((data) => {
return this.getSortedData(data);
});
}

disconnect() {}

getFilteredData(data): any[] {
if (!this.filter) { return data; }
return data.slice().filter((item: any) => {

let searchStr: string = this._model.columns
.filter((c) => c.filterable)
.map((c) => item[c.name])
.join(' ').toLowerCase();

return searchStr.indexOf(this.filter.toLowerCase()) != -1;
});
}

getSortedData(data): any[] {
if (!this._sort.active || this._sort.direction == '') { return data; }

return data.sort((a, b) => {
let propertyA: number|string = a[this._sort.active];
let propertyB: number|string = b[this._sort.active];

let valueA = isNaN(+propertyA) ? propertyA : +propertyA;
let valueB = isNaN(+propertyB) ? propertyB : +propertyB;

return (valueA < valueB ? -1 : 1) * (this._sort.direction == 'asc' ? 1 : -1);
});
}
}
24 changes: 24 additions & 0 deletions src/lib/shared/table/table-model.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { TableActionColor } from './table-action-color.enum';

export interface TableColumn {
name: string,
title: string,
sortable?: boolean,
filterable?: boolean,
displayed?: boolean
}

export interface ClickAction {
( item: any ): void;
}

export interface TableAction {
icon: string,
color?: TableActionColor
click: ClickAction
}

export interface TableModel {
columns: TableColumn[],
actions?: TableAction[]
}
40 changes: 40 additions & 0 deletions src/lib/shared/table/table.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<div class='table-container'>
<div class='table-header'>
<md-input-container floatPlaceholder='never'>
<input mdInput #filter [placeholder]="'Filter' | translate">
</md-input-container>
</div>

<md-table #table [dataSource]='dataSource' mdSort>

<ng-container [cdkColumnDef]='column.name' *ngFor='let column of model.columns'>
<ng-container *ngIf='column.sortable'>
<md-header-cell *cdkHeaderCellDef md-sort-header> {{column.title}} </md-header-cell>
</ng-container>

<ng-container *ngIf='!column.sortable'>
<md-header-cell *cdkHeaderCellDef> {{column.title}} </md-header-cell>
</ng-container>

<md-cell *cdkCellDef='let row' class="mat-cell-text"> {{getValue(row, column.name)}} </md-cell>
</ng-container>

<!-- Action Column -->
<ng-container cdkColumnDef='action'>
<md-header-cell *cdkHeaderCellDef></md-header-cell>
<md-cell *cdkCellDef='let row'>
<button *ngFor='let action of model.actions'
md-mini-fab
[color]='getActionColor(action.color)'
(click)='action.click(row)'>
<md-icon>{{action.icon}}</md-icon>
</button>
</md-cell>
</ng-container>

<md-header-row *cdkHeaderRowDef='displayedColumns'></md-header-row>
<md-row *cdkRowDef='let row; columns: displayedColumns;'></md-row>

</md-table>

</div>
36 changes: 36 additions & 0 deletions src/lib/shared/table/table.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { IgoTestModule } from '../../../test/module';
import { IgoSharedModule } from '../../shared';

import { TableComponent } from './table.component';


describe('TableComponent', () => {
let component: TableComponent;
let fixture: ComponentFixture<TableComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
IgoTestModule,
IgoSharedModule
],
declarations: [
TableComponent
],
providers: []
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
48 changes: 48 additions & 0 deletions src/lib/shared/table/table.component.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@require '../../../style/var.styl';
@require '../../../style/media.styl';

/*** Main ***/
:host {
width: 100%;
height: 100%;
display: block;
}

.table-container {
display: flex;
flex-direction: column;
max-height: 100%;
}

.table-header {
min-height: 64px;
max-width: 500px;
display: flex;
align-items: baseline;
padding: 8px 24px 0;
font-size: 20px;
justify-content: space-between;
}

.mat-input-container {
font-size: 14px;
flex-grow: 1;
margin-left: 32px;
}

.mat-table {
overflow: auto;
}

.mat-header-row, .mat-row {
height: 60px;
}

.mat-cell-text {
overflow: hidden;
word-wrap: break-word;
}

button {
margin-right: 10px;
}
Loading

0 comments on commit 2bdafd8

Please sign in to comment.