src/components/sortable_table/sortable-table.component.ts
Main component of the module.
encapsulation | ViewEncapsulation.None |
selector | ngfb-sortable-table |
styleUrls | sortable-table.component.scss |
templateUrl | sortable-table.component.html |
addNew
|
Type: |
databaseDataPath
|
Type: |
filterByInputValue
|
Type: |
filterBySelect
|
Type: |
itemComponent
|
Type: |
onChange
|
Type: |
pagination
|
Type: |
setHeaders
|
Type: |
title
|
Type: |
constructor(DB: SortableTableService)
|
Public ngOnChanges |
ngOnChanges(changes: SimpleChanges)
|
Listen to input changes
Parameters :
Returns:
void
|
Public reset |
reset(nextEvent: any)
|
Resets view after other than previous event happened
Parameters :
Returns:
void
|
Public filterBySelectChanged |
filterBySelectChanged($event: any)
|
Fetch data when user choose some value in filterBySelect
Parameters :
Returns:
void
|
Public paginationChanged |
paginationChanged(value: any)
|
Change pagination number
Parameters :
Returns:
void
|
Public fetchData |
fetchData(event: number, fieldToQueryBy: FieldToQueryBy, cleanUp: boolean)
|
Get data from database and parse it accrdingly.
Parameters :
Returns:
void
|
Public onInfinite |
onInfinite()
|
InfiniteScroll event litener
Returns:
void
|
Public trackByFn |
trackByFn(index: any, item: any)
|
Function for trackBy in *ngFor directive
Parameters :
Returns:
string
|
Public sortBy |
sortBy(field: any)
|
Sort table by some field
Parameters :
Returns:
void
|
Public onItemChange |
onItemChange(result: any)
|
Emits when addNew component close
Parameters :
Returns:
void
|
Public data |
data: |
DB |
DB: |
Public fieldToSortBy |
fieldToSortBy: |
Public headers |
headers: |
Public isFirstTime |
isFirstTime: |
Default value: true
|
Public isLoading |
isLoading: |
Default value: false
|
Public onSearchInputChanged |
onSearchInputChanged: |
If there is an @Input filterByInputValue provided |
Public searchString |
searchString: |
import {
Component, OnChanges, Input, ViewChild, ViewEncapsulation, SimpleChange, SimpleChanges
} from '@angular/core';
import { SortableTableService, SortableEvents } from "../../services/sortable-table.service";
import { HeaderItem, TableFilter, Pagination, FieldToQueryBy, SetHeadersFunction, SearchString, TableItem } from "../../models/";
import { Observable } from "rxjs";
declare const require: any;
const debounce = require('lodash.debounce');
/**
* Utility function for handling headers if there is no @Input setHeaders
* @param data
*/
const mapper = (data: Observable<{key: string, value : Array<any>}>) : Observable<Array<HeaderItem>> =>
data.map(({value}) => {
const keys = Object.keys(value);
if (keys.length) {
const data = value[Object.keys(value)[0]];
return Object.keys(data).map(key => ({name : key, sortable: false}))
}
});
/**
* Main component of the module.
*/
@Component({
selector: 'ngfb-sortable-table',
templateUrl: './sortable-table.component.html',
styleUrls: ['./sortable-table.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class SortableTableComponent implements OnChanges {
@Input() public title: string;
@Input() public databaseDataPath: string;
@Input() public setHeaders: SetHeadersFunction;
@Input() public pagination: Pagination;
@Input() public filterByInputValue: SearchString;
@Input() public filterBySelect: TableFilter;
@Input() public itemComponent: TableItem;
@Input() public onChange: Function;
@Input() public addNew: Component;
@ViewChild('searchString') public searchString;
public isLoading: boolean = false;
public data: Array<any> = [];
public headers: Array<HeaderItem>;
public fieldToSortBy: string;
public isFirstTime: boolean = true;
constructor(public DB: SortableTableService) { }
/**
* Listen to input changes
* @param changes
*/
public ngOnChanges(changes: SimpleChanges): void {
const pathToFetchChangedTo = changes['databaseDataPath'] as SimpleChange;
if (pathToFetchChangedTo) {
this.DB.lastEventHappened = undefined;
if (!pathToFetchChangedTo.isFirstChange()) {
this.data = [];
this.fieldToSortBy = '';
}
if (this.pagination) {
this.DB.setPagination(this.pagination.defaultOption || 20);
}
this.isFirstTime = true;
if (this.filterBySelect) {
this.fetchData(SortableEvents.FilterBySelect, {
value: this.filterBySelect.defaultOption,
field: this.filterBySelect.field
}, true);
} else {
this.fetchData();
}
}
}
/**
* If there is an @Input filterByInputValue provided
* this method will be triggered on keyup event.
* debounce function is much more useful here in comparation with
* Observable.debounceTime(timer) because input could be created or deleted dinamically.
*/
public onSearchInputChanged = debounce(function ($event) {
Observable
.of($event['target']['value'])
.map(inputVal => inputVal.replace(/\?!.#$[]/g, ''))
.first()
.subscribe(inputVal => {
this.fetchData(SortableEvents.FilterBySearch, {
field: this.filterByInputValue.defaultField,
value: inputVal
}, true);
})
}, 500);
/**
* Resets view after other than previous event happened
* @param nextEvent
*/
public reset(nextEvent): void {
switch (nextEvent) {
case SortableEvents.FilterBySearch : {
this.fieldToSortBy = '';
if (this.filterBySelect) {
this.filterBySelect.defaultOption = this.filterBySelect.resetTo;
}
break;
}
case SortableEvents.FilterBySelect: {
this.fieldToSortBy = '';
if (this.filterByInputValue && this.searchString) {
this.searchString['nativeElement'].value = '';
}
break;
}
case SortableEvents.SortByField : {
if (this.filterBySelect) {
this.filterBySelect.defaultOption = this.filterBySelect.resetTo;
}
if (this.filterByInputValue && this.searchString) {
this.searchString['nativeElement'].value = '';
}
break;
}
}
}
/**
* Fetch data when user choose some value in filterBySelect
* @param $event
*/
public filterBySelectChanged($event) {
if (this.pagination) {
this.paginationChanged(this.pagination.defaultOption);
}
this.fetchData(SortableEvents.FilterBySelect, {
value: $event.value,
field: this.filterBySelect.field
}, true)
}
/**
* Change pagination number
* @param value
*/
public paginationChanged(value) {
this.pagination.defaultOption = value;
this.DB.setPagination(value);
}
/**
* Get data from database and parse it accrdingly.
* @param event
* @param fieldToQueryBy
* @param cleanUp
*/
public fetchData(event? : number, fieldToQueryBy?: FieldToQueryBy, cleanUp?: boolean): void {
this.isLoading = true;
this.reset(event);
/**
* This stream whaits for data from db.
* @type {"../..Observable".Observable<T>|Observable<R|T>}
*/
const requestStream = this.DB
.get(this.databaseDataPath, event, fieldToQueryBy)
.share()
.first();
/**
* This stream puts data to view
*/
requestStream
.map(({key, value}) => Object.keys(value || {}).map(key => value[key]))
.do(() => this.isLoading = false)
.first() //never lose the link to obj in order to save rendering process
.subscribe(arr => cleanUp ? this.data.splice(0, this.data.length, ...arr) : this.data.push(...arr));
/**
* Handle headers only once. When path to data in db changes.
*/
if (this.isFirstTime) {
const headers = this.setHeaders ? this.setHeaders(requestStream) : mapper(requestStream);
headers
.first()
.subscribe((data: Array<HeaderItem>) => this.headers = data);
this.isFirstTime = false;
}
}
/**
* InfiniteScroll event litener
*/
public onInfinite(): void {
if (this.pagination) {
this.fetchData(
SortableEvents.InfiniteScroll,
null,
this.DB.lastEventHappened !== undefined
);
}
}
/**
* Function for trackBy in *ngFor directive
* @param index
* @param item
*/
public trackByFn(index, item): string {
return item.$key;
}
/**
* Sort table by some field
* @param field
*/
public sortBy(field): void {
this.fieldToSortBy = this.fieldToSortBy === field ? `-${field}` : field;
if (this.pagination) {
this.paginationChanged(this.pagination.defaultOption);
}
this.fetchData(SortableEvents.SortByField, {
field: field,
order: this.fieldToSortBy.startsWith('-') ? 'desc' : 'asc'
}, true);
}
/**
* Emits when addNew component close
* or when itemChange event happen in TableItemComponent
* @param result
*/
public onItemChange(result): void {
if (this.onChange) {
this.onChange(result);
}
}
}
<header class="main-toolbar md-elevation-z2" [style.display]="!filterByInputValue && !filterBySelect && !title ? 'none' : 'flex'">
<h1 *ngIf="title" class="title">{{title}}</h1>
<md-input-container *ngIf="filterByInputValue">
<input #searchString
mdInput
[placeholder]="filterByInputValue?.inputPlaceholder"
(keyup)="onSearchInputChanged($event)">
</md-input-container>
<md-select *ngIf="filterByInputValue && filterByInputValue.fields.length > 1" [placeholder]="filterByInputValue?.selectPlaceholder"
[(ngModel)]="filterByInputValue.defaultField">
<md-option
*ngFor="let option of filterByInputValue.fields"
[value]="option.value">
{{ option.text }}
</md-option>
</md-select>
<span class="flex"></span>
<md-select class="filter-select" *ngIf="filterBySelect"
[placeholder]="filterBySelect?.placeholder"
(change)="filterBySelectChanged($event)"
[(ngModel)]="filterBySelect.defaultOption">
<md-option
*ngFor="let option of filterBySelect.options"
[value]="option.value">
{{ option.text }}
</md-option>
</md-select>
</header>
<main class="main-container" ngfbInfiniteScroll (scrolled)="onInfinite()">
<div class="table-content md-elevation-z2 column-filter">
<table class="table table-header">
<thead *ngIf="headers && headers.length">
<th *ngFor="let header of headers">
<div>
<p>{{header.name}}</p>
<button md-mini-fab *ngIf="header.sortable" (click)="sortBy(header.name)">
<md-icon>{{ fieldToSortBy === '-' + header.name || fieldToSortBy !== header.name ? 'arrow_upward' : 'arrow_downward'}}</md-icon>
</button>
</div>
</th>
</thead>
<tbody *ngIf="data && data.length">
<ng-template *ngFor="let item of data; let i = index; trackBy: trackByFn"
ngfbSortableItem
[item]="item"
[index]="i"
(itemChange)="onItemChange($event)"
[component]="itemComponent">
</ng-template>
</tbody>
</table>
</div>
<ngfb-loading [isLoading]="isLoading"></ngfb-loading>
</main>
<footer *ngIf="pagination || addNew">
<md-select *ngIf="pagination" [placeholder]="pagination?.placeholder"
(change)="paginationChanged($event.value)"
[ngModel]="pagination.defaultOption">
<md-option *ngFor="let option of pagination.options" [value]="option">{{ option }}</md-option>
</md-select>
<span class="flex"></span>
<button color="primary"
*ngIf="addNew"
md-fab
ngfbDialog
(onDialogResult)="onItemChange($event)"
[component]="addNew">
<i class="material-icons">exposure_plus_1</i>
</button>
</footer>