File

src/components/sortable_table/sortable-table.component.ts

Description

Main component of the module.

Implements

OnChanges

Metadata

encapsulation ViewEncapsulation.None
selector ngfb-sortable-table
styleUrls sortable-table.component.scss
templateUrl sortable-table.component.html

Inputs

addNew

Type: Component

databaseDataPath

Type: string

filterByInputValue

Type: SearchString

filterBySelect

Type: TableFilter

itemComponent

Type: TableItem

onChange

Type: Function

pagination

Type: Pagination

setHeaders

Type: SetHeadersFunction

title

Type: string

Constructor

constructor(DB: SortableTableService)

Methods

Public ngOnChanges
ngOnChanges(changes: SimpleChanges)

Listen to input changes

Parameters :
  • changes
Returns: void
Public reset
reset(nextEvent: any)

Resets view after other than previous event happened

Parameters :
  • nextEvent
Returns: void
Public filterBySelectChanged
filterBySelectChanged($event: any)

Fetch data when user choose some value in filterBySelect

Parameters :
  • $event
Returns: void
Public paginationChanged
paginationChanged(value: any)

Change pagination number

Parameters :
  • value
Returns: void
Public fetchData
fetchData(event: number, fieldToQueryBy: FieldToQueryBy, cleanUp: boolean)

Get data from database and parse it accrdingly.

Parameters :
  • event
  • fieldToQueryBy
  • cleanUp
Returns: void
Public onInfinite
onInfinite()

InfiniteScroll event litener

Returns: void
Public trackByFn
trackByFn(index: any, item: any)

Function for trackBy in *ngFor directive

Parameters :
  • index
  • item
Returns: string
Public sortBy
sortBy(field: any)

Sort table by some field

Parameters :
  • field
Returns: void
Public onItemChange
onItemChange(result: any)

Emits when addNew component close
or when itemChange event happen in TableItemComponent

Parameters :
  • result
Returns: void

Properties

Public data
data: any[]
DB
DB: SortableTableService
Public fieldToSortBy
fieldToSortBy: string
Public headers
headers: HeaderItem[]
Public isFirstTime
isFirstTime: boolean
Default value: true
Public isLoading
isLoading: boolean
Default value: false
Public onSearchInputChanged
onSearchInputChanged: any

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 searchString
searchString: any
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>
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""