event.target).parentElement.textContent.trim();
38 | const interpolateParams = {
39 | 'column_name': columnName,
40 | 'title': this.dataTable.title
41 | };
42 |
43 | this.dataTable.columnSelectorNotification = (isChecked ? this.dataTable.labels.headerColumnSelectorAdded :
44 | this.dataTable.labels.headerColumnSelectorRemoved)
45 | .replace('{column_name}', interpolateParams.column_name)
46 | .replace('{title}', interpolateParams.title);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 | Angular5 DataTable Demo App
2 |
3 |
4 | Home
5 |
7 |
8 |
9 |
10 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/libs/datatable/src/datatable.module.ts:
--------------------------------------------------------------------------------
1 | // modules
2 | import { ModuleWithProviders, NgModule } from '@angular/core';
3 | import { CommonModule } from '@angular/common';
4 | import { BrowserModule } from '@angular/platform-browser';
5 | import { FormsModule } from '@angular/forms';
6 |
7 | // utils
8 | import { HideDirective } from './utils/hide';
9 | import { MinPipe } from './utils/min';
10 | import { PixelConverter } from './utils/px';
11 | // types & tools
12 | import { DataTableTranslations } from './types/data-table-translations.type';
13 | import { CellCallback } from './types/cell-callback.type';
14 | import { RowCallback } from './types/row-callback.type';
15 | import { DataTableResource } from './tools/data-table-resource';
16 | import { DataTableParams } from './types/data-table-params.type';
17 | // components & directives
18 | import { DataTableComponent } from './components/table/table.component';
19 | import { DataTableRowComponent } from './components/row/row.component';
20 | import { DataTableColumnDirective } from './directives/column/column.directive';
21 | import { DataTableHeaderComponent } from './components/header/header.component';
22 | import { DataTablePaginationComponent } from './components/pagination/pagination.component';
23 |
24 | export {
25 | DataTableComponent, DataTableColumnDirective, DataTableRowComponent, DataTablePaginationComponent, DataTableHeaderComponent,
26 | DataTableResource,
27 | DataTableParams, DataTableTranslations,
28 | CellCallback, RowCallback
29 | };
30 |
31 | @NgModule({
32 | declarations: [
33 | DataTableComponent, DataTableColumnDirective,
34 | DataTableRowComponent, DataTablePaginationComponent, DataTableHeaderComponent,
35 | PixelConverter, HideDirective, MinPipe
36 | ],
37 | imports: [
38 | CommonModule,
39 | FormsModule
40 | ],
41 | exports: [DataTableComponent, DataTableColumnDirective]
42 | })
43 |
44 | export class DataTableModule {
45 | public static forRoot(): ModuleWithProviders {
46 | return {
47 | ngModule: DataTableModule,
48 | providers: []
49 | };
50 | }
51 | }
52 |
53 |
54 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/row/row.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ElementRef,
4 | EventEmitter,
5 | forwardRef,
6 | Inject,
7 | Input,
8 | OnDestroy,
9 | OnInit,
10 | Output,
11 | Renderer2
12 | } from '@angular/core';
13 | import { DataTableComponent } from '../table/table.component';
14 |
15 | @Component({
16 | selector: '[dataTableRow]',
17 | templateUrl: './row.component.html',
18 | styleUrls: ['./row.component.css']
19 | })
20 | export class DataTableRowComponent implements OnInit, OnDestroy {
21 |
22 | public _this = this;
23 |
24 | @Input() item: any;
25 | @Input() index: number;
26 |
27 | expanded: boolean;
28 |
29 | private _listeners = [];
30 |
31 | // row selection:
32 | private _selected: boolean;
33 |
34 | @Output() selectedChange = new EventEmitter();
35 |
36 | get selected() {
37 | return this._selected;
38 | }
39 |
40 | set selected(selected) {
41 | this._selected = selected;
42 | this.selectedChange.emit(selected);
43 | }
44 |
45 | // other:
46 | get displayIndex() {
47 | if (this.dataTable.pagination) {
48 | return this.dataTable.displayParams.offset + this.index + 1;
49 | } else {
50 | return this.index + 1;
51 | }
52 | }
53 |
54 | getTooltip() {
55 | if (this.dataTable.rowTooltip) {
56 | return this.dataTable.rowTooltip(this.item, this, this.index);
57 | }
58 | return '';
59 | }
60 |
61 | constructor(@Inject(forwardRef(() => DataTableComponent)) public dataTable: DataTableComponent,
62 | private renderer: Renderer2, private elementRef: ElementRef) {}
63 |
64 | ngOnInit() {
65 | if (this.dataTable.rowClick.observers.length > 0) {
66 | this._listeners.push(
67 | this.renderer.listen(this.elementRef.nativeElement, 'click',
68 | (event) => this.dataTable.rowClicked(this, event))
69 | );
70 | }
71 | if (this.dataTable.rowDoubleClick.observers.length > 0) {
72 | this._listeners.push(
73 | this.renderer.listen(this.elementRef.nativeElement, 'dblclick',
74 | (event) => this.dataTable.rowDoubleClicked(this, event))
75 | );
76 | }
77 | }
78 |
79 | ngOnDestroy() {
80 | this.selected = false;
81 | this._listeners.forEach(fn => fn());
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/pagination/pagination.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | ElementRef,
4 | forwardRef,
5 | Inject,
6 | Input,
7 | ViewChild
8 | } from '@angular/core';
9 | import { DataTableComponent} from '../table/table.component';
10 |
11 | let nextId = 0;
12 |
13 | @Component({
14 | selector: 'data-table-pagination',
15 | templateUrl: './pagination.component.html',
16 | styleUrls: ['./pagination.component.css']
17 | })
18 | export class DataTablePaginationComponent {
19 |
20 | id = `pagination-${nextId++}`;
21 |
22 | @ViewChild('pageInput') pageInput: ElementRef;
23 |
24 | Math: any;
25 |
26 | @Input() limits: number[];
27 |
28 | constructor(@Inject(forwardRef(() => DataTableComponent)) public dataTable: DataTableComponent) {
29 | this.Math = Math;
30 | }
31 |
32 | pageBack() {
33 | this.dataTable.offset -= Math.min(this.dataTable.limit, this.dataTable.offset);
34 | if (this.dataTable.offset <= 0) {
35 | this.pageInput.nativeElement.focus();
36 | }
37 | }
38 | pageForward() {
39 | this.dataTable.offset += this.dataTable.limit;
40 | if ((this.dataTable.offset + this.dataTable.limit) >= this.dataTable.itemCount) {
41 | this.pageInput.nativeElement.focus();
42 | }
43 | }
44 |
45 | pageFirst() {
46 | this.dataTable.offset = 0;
47 | this.pageInput.nativeElement.focus();
48 | }
49 |
50 | pageLast() {
51 | this.dataTable.offset = (this.maxPage - 1) * this.dataTable.limit;
52 | if ((this.dataTable.offset + this.dataTable.limit) >= this.dataTable.itemCount) {
53 | this.pageInput.nativeElement.focus();
54 | }
55 | }
56 |
57 | get maxPage() {
58 | return Math.ceil(this.dataTable.itemCount / this.dataTable.limit);
59 | }
60 |
61 | get limit() {
62 | return this.dataTable.limit;
63 | }
64 |
65 | set limit(value) {
66 | this.dataTable.limit = +value;
67 | // returning back to the first page.
68 | this.page = 1;
69 | }
70 |
71 | get page() {
72 | return this.dataTable.page;
73 | }
74 |
75 | set page(value) {
76 | this.dataTable.page = +value;
77 | }
78 |
79 | validate(event) {
80 | const newValue = +event.target.value;
81 | if (newValue !== this.page) {
82 | this.page = (event.target.value > this.maxPage) ? this.maxPage : (newValue < 1 ) ? 1 : newValue;
83 | event.target.value = this.page;
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular5-datable-demo",
3 | "version": "0.1.1",
4 | "license": "MIT",
5 | "private": false,
6 | "scripts": {
7 | "ng": "ng",
8 | "start": "ng serve",
9 | "build": "ng build --prod",
10 | "test": "ng test -sm=false",
11 | "lint": "ng lint",
12 | "e2e": "ng e2e",
13 | "build:docs": "compodoc ./libs -p src/tsconfig.packages.json -s",
14 | "build:app": "cpr dist/angular5-data-table src/angualar5-datatable --overwrite && ng build --app packages && rimraf src/angular5-data-table",
15 | "build:app:prod": "cpr dist/angular5-data-table src/angular5-data-table --overwrite && ng build --prod --named-chunks=true --app packages && rimraf src/angular5-data-table",
16 | "build:libs:datatable": "ng-packagr -p libs/datatable/package.json",
17 | "publish:libs:datatable": "npm publish dist/angular5-data-table"
18 | },
19 | "dependencies": {
20 | "@angular/animations": "^5.2.0",
21 | "@angular/common": "^5.2.0",
22 | "@angular/compiler": "^5.2.0",
23 | "@angular/core": "^5.2.0",
24 | "@angular/forms": "^5.2.0",
25 | "@angular/http": "^5.2.0",
26 | "@angular/platform-browser": "^5.2.0",
27 | "@angular/platform-browser-dynamic": "^5.2.0",
28 | "@angular/router": "^5.2.0",
29 | "bootstrap": "^4.0.0",
30 | "core-js": "^2.4.1",
31 | "font-awesome": "^4.7.0",
32 | "highlight.js": "^9.12.0",
33 | "lodash-es": "^4.17.4",
34 | "ngx-md": "^3.1.1",
35 | "popper.js": "^1.14.3",
36 | "rxjs": "^5.5.6",
37 | "tslint-angular": "^1.1.1",
38 | "zone.js": "^0.8.19"
39 | },
40 | "devDependencies": {
41 | "@angular-devkit/core": "0.3.2",
42 | "@angular-devkit/schematics": "0.3.2",
43 | "@angular/cli": "1.7.1",
44 | "@angular/compiler-cli": "^5.2.0",
45 | "@angular/language-service": "^5.2.0",
46 | "@compodoc/compodoc": "^1.0.5",
47 | "@types/jasmine": "~2.8.3",
48 | "@types/jasminewd2": "~2.0.2",
49 | "@types/node": "^8.0.47",
50 | "codelyzer": "^4.2.0",
51 | "cpr": "^3.0.0",
52 | "jasmine-core": "~2.8.0",
53 | "jasmine-spec-reporter": "~4.2.1",
54 | "karma": "~2.0.0",
55 | "karma-chrome-launcher": "~2.2.0",
56 | "karma-coverage-istanbul-reporter": "^1.2.1",
57 | "karma-jasmine": "~1.1.0",
58 | "karma-jasmine-html-reporter": "^0.2.2",
59 | "ng-packagr": "^2.1.0",
60 | "node-sass": "4.7.2",
61 | "protractor": "~5.1.2",
62 | "rimraf": "^2.6.1",
63 | "ts-node": "~4.1.0",
64 | "tsickle": "^0.26.0",
65 | "tslint": "~5.9.1",
66 | "typescript": "2.6.1"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | import 'core-js/es6/symbol';
23 | import 'core-js/es6/object';
24 | import 'core-js/es6/function';
25 | import 'core-js/es6/parse-int';
26 | import 'core-js/es6/parse-float';
27 | import 'core-js/es6/number';
28 | import 'core-js/es6/math';
29 | import 'core-js/es6/string';
30 | import 'core-js/es6/date';
31 | import 'core-js/es6/array';
32 | import 'core-js/es6/regexp';
33 | import 'core-js/es6/map';
34 | import 'core-js/es6/weak-map';
35 | import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Required to support Web Animations `@angular/platform-browser/animations`.
51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
52 | **/
53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
54 |
55 |
56 |
57 | /***************************************************************************************************
58 | * Zone JS is required by default for Angular itself.
59 | */
60 | import 'zone.js/dist/zone'; // Included with Angular CLI.
61 |
62 |
63 |
64 | /***************************************************************************************************
65 | * APPLICATION IMPORTS
66 | */
67 |
--------------------------------------------------------------------------------
/src/app/components/code-viewer/code-viewer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, ViewChild } from '@angular/core';
2 | import { HttpClient } from '@angular/common/http';
3 | import { Observable } from 'rxjs/Observable';
4 | import { forkJoin } from 'rxjs/observable/forkJoin';
5 | import 'rxjs/add/observable/of';
6 |
7 | import * as Highlight from 'highlight.js';
8 | import { catchError } from 'rxjs/operators';
9 |
10 | @Component({
11 | selector: 'code-viewer',
12 | templateUrl: './code-viewer.component.html',
13 | styleUrls: ['./code-viewer.component.css']
14 | })
15 | export class CodeViewerComponent {
16 |
17 | @ViewChild('tsViewerNode') tsNode: ElementRef;
18 | @ViewChild('htmlViewerNode') htmlNode: ElementRef;
19 | @ViewChild('cssViewerNode') cssNode: ElementRef;
20 |
21 | showTsNode = true;
22 | showHtmlNode = true;
23 | showCssNode = true;
24 |
25 | tsContent = '';
26 | htmlContent = '';
27 | cssContent = '';
28 |
29 | visibility = false;
30 | baseUrl = 'assets/src';
31 | path = '';
32 |
33 | constructor(private http: HttpClient) {
34 | Highlight.configure({ useBR: false, languages: ['css', 'html', 'ts'] });
35 | Highlight.initHighlighting();
36 | }
37 |
38 | public toggle(): void {
39 | if (!this.visibility && this.path.length > 0) {
40 | const ts = this.http.get(`${this.baseUrl}/${this.path}.ts`, {responseType: 'text'})
41 | .pipe(catchError(() => Observable.of('')));
42 | const html = this.http.get(`${this.baseUrl}/${this.path}.html`, {responseType: 'text'})
43 | .pipe(catchError(() => Observable.of('')));
44 | const css = this.http.get(`${this.baseUrl}/${this.path}.css`, {responseType: 'text'})
45 | .pipe(catchError(() => Observable.of('')));
46 | forkJoin([ts, html, css]).subscribe((results: string[]) => {
47 | this.tsContent = results[0];
48 | this.htmlContent = results[1];
49 | this.cssContent = results[2];
50 | this.showCodeBlock();
51 | setTimeout(() => this.highlight(), 250);
52 | }, error => {
53 | console.debug(error); /* tslint:disable-line:no-console */
54 | });
55 | } else {
56 | this.hideCodeBlock();
57 | }
58 | }
59 |
60 | public setPath(path: string): void {
61 | this.path = path;
62 | }
63 |
64 | hideCodeBlock(): void {
65 | this.visibility = false;
66 | }
67 |
68 | showCodeBlock(): void {
69 | this.visibility = true;
70 | }
71 |
72 | highlight() {
73 | if (this.showTsNode) { Highlight.highlightBlock(this.tsNode.nativeElement); }
74 | if (this.showHtmlNode) { Highlight.highlightBlock(this.htmlNode.nativeElement); }
75 | if (this.showCssNode) { Highlight.highlightBlock(this.cssNode.nativeElement); }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/table/table.component.css:
--------------------------------------------------------------------------------
1 | /* bootstrap override: */
2 |
3 | :host /deep/ .data-table.table > tbody + tbody {
4 | border-top: none;
5 | }
6 |
7 | :host /deep/ .data-table.table td {
8 | vertical-align: middle;
9 | }
10 |
11 | :host /deep/ .data-table > thead > tr > th,
12 | :host /deep/ .data-table > tbody > tr > td {
13 | overflow: hidden;
14 | }
15 |
16 | :host /deep/ .data-table > thead > tr > td {
17 | border-bottom: 2px solid #dee2e6;
18 | }
19 |
20 | /* I can't use the bootstrap striped table, because of the expandable rows */
21 | :host /deep/ .row-odd {
22 | background-color: #F6F6F6;
23 | }
24 |
25 | :host /deep/ .row-even {
26 | }
27 |
28 | .data-table .substitute-rows > tr:hover,
29 | :host /deep/ .data-table .data-table-row:hover {
30 | background-color: #ECECEC;
31 | }
32 |
33 | /* table itself: */
34 |
35 | .data-table {
36 | box-shadow: 0 0 15px rgb(236, 236, 236);
37 | table-layout: fixed;
38 | }
39 |
40 | /* header cells: */
41 |
42 | .column-header {
43 | position: relative;
44 | }
45 |
46 | .expand-column-header {
47 | width: 50px;
48 | }
49 |
50 | .select-column-header {
51 | width: 50px;
52 | text-align: center;
53 | }
54 |
55 | .index-column-header {
56 | width: 40px;
57 | }
58 |
59 | .column-header.sortable button {
60 | -webkit-box-sizing: content-box;
61 | -moz-box-sizing: content-box;
62 | box-sizing: content-box;
63 | background: none;
64 | border: 0;
65 | color: inherit;
66 | cursor: pointer;
67 | font: inherit;
68 | line-height: normal;
69 | overflow: visible;
70 | padding: 0;
71 | -webkit-appearance: button; /* for input */
72 | -webkit-user-select: none; /* for button */
73 | -moz-user-select: none;
74 | -ms-user-select: none;
75 | text-align: left;
76 | }
77 |
78 | .column-header .column-sort-icon {
79 | margin-left: 8px;
80 | }
81 |
82 | .column-header.resizable .column-sort-icon {
83 | margin-right: 8px;
84 | }
85 |
86 | .column-header .column-sort-icon .column-sortable-icon {
87 | color: lightgray;
88 | }
89 |
90 | .column-header .column-resize-handle {
91 | position: absolute;
92 | top: 0;
93 | right: 0;
94 | margin: 0;
95 | padding: 0;
96 | width: 8px;
97 | height: 100%;
98 | cursor: col-resize;
99 | }
100 |
101 | /* cover: */
102 | .data-table-box {
103 | position: relative;
104 | }
105 |
106 | .busy {
107 | position: absolute;
108 | z-index: 1;
109 | top: 0;
110 | bottom: 0;
111 | left: 0;
112 | right: 0;
113 | background-color: rgba(0, 0, 0, 0.25);
114 | }
115 |
116 | .busy > i {
117 | position: absolute;
118 | left: 50%;
119 | top: 50%;
120 | transform: translate(-50%, -50%);
121 | }
122 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/row/row.component.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
26 |
27 |
29 |
30 |
33 |
34 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/app/components/datatable/demo3/data-table-demo3.html:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
21 |
25 |
26 |
30 |
31 |
36 |
37 |
40 |
41 |
42 |
43 | Selected:
44 | No item selected
45 |
46 |
47 |
48 |
49 |
50 | Notes for testers and developers
51 |
52 |
53 | The table demo is initialized with selectOnRowClick=true. This means that a row selection can be triggered clicking on the row. However, this functionality is not accessible, unless the user provides enough information regarding the triggering action consequences and implements an <input> (like a <button> or <a>) as part of the row that will allow the selection via keyboard too.
54 |
55 |
56 | The Rating column defines the column background color based on the rating value, via the cellColors function. The resulting color could be not compliant with high contrast requirements and it must be taken care by the developer for the appropriate fix.
57 |
58 |
59 | Selected region is not part of the datatable component. Any accessibility issue related to it must be fixed by developer.
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "angular5-data-table"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "name": "app",
10 | "outDir": "dist/app",
11 | "assets": [
12 | "assets",
13 | "favicon.ico",
14 | {"glob": "**/*","input":"./app/components/datatable/","output":"./assets/src/datatable"},
15 | {"glob": "**/*.md","input":"../libs/datatable/","output":"./assets/datatable"}
16 | ],
17 | "index": "index.html",
18 | "main": "main.ts",
19 | "polyfills": "polyfills.ts",
20 | "test": "test.ts",
21 | "tsconfig": "tsconfig.app.json",
22 | "testTsconfig": "tsconfig.spec.json",
23 | "prefix": "app",
24 | "styles": [
25 | "../node_modules/bootstrap/dist/css/bootstrap.min.css",
26 | "../node_modules/font-awesome/css/font-awesome.css",
27 | "styles.css"
28 | ],
29 | "scripts": [],
30 | "environmentSource": "environments/environment.ts",
31 | "environments": {
32 | "dev": "environments/environment.ts",
33 | "prod": "environments/environment.prod.ts"
34 | }
35 | },
36 | {
37 | "root": "src",
38 | "name": "packages",
39 | "outDir": "dist/app",
40 | "assets": [
41 | "assets",
42 | "favicon.ico"
43 | ],
44 | "index": "index.html",
45 | "main": "main.ts",
46 | "polyfills": "polyfills.ts",
47 | "test": "test.ts",
48 | "tsconfig": "tsconfig.packages.json",
49 | "testTsconfig": "tsconfig.spec.json",
50 | "prefix": "app",
51 | "styles": [
52 | "../node_modules/bootstrap/dist/css/bootstrap.min.css",
53 | "../node_modules/font-awesome/css/font-awesome.css",
54 | "styles.css"
55 | ],
56 | "scripts": [],
57 | "environmentSource": "environments/environment.ts",
58 | "environments": {
59 | "dev": "environments/environment.ts",
60 | "prod": "environments/environment.prod.ts"
61 | }
62 | }
63 | ],
64 | "e2e": {
65 | "protractor": {
66 | "config": "./protractor.conf.js"
67 | }
68 | },
69 | "lint": [
70 | {
71 | "project": "src/tsconfig.app.json",
72 | "exclude": "**/node_modules/**"
73 | },
74 | {
75 | "project": "src/tsconfig.packages.json",
76 | "exclude": "**/node_modules/**"
77 | },
78 | {
79 | "project": "src/tsconfig.spec.json",
80 | "exclude": "**/node_modules/**"
81 | },
82 | {
83 | "project": "e2e/tsconfig.e2e.json",
84 | "exclude": "**/node_modules/**"
85 | }
86 | ],
87 | "test": {
88 | "karma": {
89 | "config": "./karma.conf.js"
90 | }
91 | },
92 | "defaults": {
93 | "styleExt": "css",
94 | "component": {}
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/header/header.component.html:
--------------------------------------------------------------------------------
1 |
51 |
--------------------------------------------------------------------------------
/src/app/components/datatable/demo2/data-table-demo2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Highlight after:
4 |
5 |
6 |
17 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
32 |
33 |
36 |
37 |
39 |
40 | Actions
41 |
42 |
43 | Buy
44 |
45 |
46 |
47 |
48 |
49 | Selected:
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Notes for testers and developers
60 |
61 |
62 | The first column (Year) in this demo makes use of custom content through dataTableCell feature. in order to display custom content. As stated in datatable documentation, this is out from the control of the datatable component. Hence if the developers use it, it's developer's responsibility to check and fix any accessibility issue. Specifically, in this case Year cell content is a <input> element with not text associated. The developer must provide an accessible name for it.
63 |
64 |
65 | The last column (Actions ) in this demo makes use of custom content through dataTableCell feature. in order to display custom content. As stated in datatable documentation, this is out from the control of the datatable component. Hence if the developers use it, it's developer's responsibility to check and fix any accessibility issue. Specifically, in this case "Year" cell content is a <input> element with not text associated. The developer must provide an accessible name for it.
66 | The table demo defines a rowClick($event) function. This is a function that is executed when the user clicks on the row. However, this functionality is not accessible, unless the user provides an <input> (like a <button> or <a>) as part of the row that will allow to trigger the click via keyboard too.
67 |
68 |
69 | Highlight after input and Selected region are not part of the datatable component. Any accessibility issue related to them must be fixed by developer.
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/app/components/datatable/demo1/data-table-demo1.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
16 |
17 |
21 |
22 | {{ item.date | date:'yyyy-MM-dd' }}
23 |
24 |
25 |
29 |
30 |
34 |
35 |
40 |
41 | Active
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Notes for testers and developers
55 |
70 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/pagination/pagination.component.html:
--------------------------------------------------------------------------------
1 |
2 |
8 |
75 |
76 |
--------------------------------------------------------------------------------
/src/app/components/datatable/demo3/data-table-demo3-data.ts:
--------------------------------------------------------------------------------
1 | export const films = [
2 | {
3 | title: 'The Shawshank Redemption',
4 | year: 1994,
5 | rating: 9.2,
6 | director: 'Frank Darabont',
7 | description: 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.'
8 | },
9 | {
10 | title: 'The Godfather',
11 | year: 1972,
12 | rating: 9.2,
13 | director: 'Francis Ford Coppola',
14 | description: 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.'
15 | },
16 | {
17 | title: 'The Godfather: Part II',
18 | year: 1974,
19 | rating: 9.0,
20 | director: 'Francis Ford Coppola',
21 | description: 'The early life and career of Vito Corleone in 1920s New York is portrayed while his son, Michael, expands and tightens ' +
22 | 'his grip on his crime syndicate stretching from Lake Tahoe, Nevada to pre-revolution 1958 Cuba.'
23 | },
24 | {
25 | title: 'The Dark Knight',
26 | year: 2008,
27 | rating: 8.9,
28 | director: 'Christopher Nolan',
29 | description: 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, the caped crusader must come to ' +
30 | 'terms with one of the greatest psychological tests of his ability to fight injustice.'
31 | },
32 | {
33 | title: 'Pulp Fiction',
34 | year: 1994,
35 | rating: 8.9,
36 | director: 'Quentin Tarantino',
37 | description: 'The lives of two mob hit men, a boxer, a gangster\'s wife, and a pair of diner bandits intertwine in four tales of ' +
38 | 'violence and redemption.'
39 | },
40 | {
41 | title: 'Schindler\'s List',
42 | year: 1993,
43 | rating: 8.9,
44 | director: 'Stephen Spielberg',
45 | description: 'In Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing ' +
46 | 'their persecution by the Nazis.'
47 | },
48 | {
49 | title: '12 Angry Men',
50 | year: 1957,
51 | rating: 8.9,
52 | director: 'Sidney Lumet',
53 | description: 'A dissenting juror in a murder trial slowly manages to convince the others that the case is not as obviously clear ' +
54 | 'as it seemed in court.'
55 | },
56 | {
57 | title: 'The Good, the Bad and the Ugly',
58 | year: 1966,
59 | rating: 8.9,
60 | director: 'Sergio Leone',
61 | description: 'A bounty hunting scam joins two men in an uneasy alliance against a third in a race to find a fortune in gold buried ' +
62 | 'in a remote cemetery.'
63 | },
64 | {
65 | title: 'The Lord of the Rings: The Return of the King',
66 | year: 2003,
67 | rating: 8.9,
68 | director: 'Peter Jackson',
69 | description: 'Gandalf and Aragorn lead the World of Men against Sauron\'s army to draw his gaze from Frodo and Sam as they approach' +
70 | ' Mount Doom with the One Ring.'
71 | },
72 | {
73 | title: 'Fight Club',
74 | year: 1999,
75 | rating: 8.8,
76 | director: 'David Fincher',
77 | description: 'An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker and ' +
78 | 'they form an underground fight club that evolves into something much, much more...'
79 | },
80 | {
81 | title: 'The Lord of the Rings: The Fellowship of the Ring',
82 | year: 2001,
83 | rating: 8.8,
84 | director: 'Peter Jackson',
85 | description: 'A meek hobbit of the Shire and eight companions set out on a journey to Mount Doom to destroy the One Ring and the' +
86 | ' dark lord Sauron.'
87 | },
88 | {
89 | title: 'Star Wars: Episode V - The Empire Strikes Back',
90 | year: 1980,
91 | rating: 8.7,
92 | director: 'Irvin Kershner',
93 | description: 'After the rebels have been brutally overpowered by the Empire on their newly established base, Luke Skywalker takes ' +
94 | 'advanced Jedi training with Master Yoda, while his friends are pursued by Darth Vader as part of his plan to capture Luke.'
95 | },
96 | {
97 | title: 'Forest Gump',
98 | year: 1994,
99 | rating: 8.7,
100 | director: 'Stephen Spielberg',
101 | description: 'Forrest Gump, while not intelligent, has accidentally been present at many historic moments, but his true love, ' +
102 | 'Jenny Curran, eludes him.'
103 | },
104 | {
105 | title: 'Inception',
106 | year: 2010,
107 | rating: 8.7,
108 | director: 'Christopher Nolan',
109 | description: 'A thief who steals corporate secrets through use of dream-sharing technology is given the inverse task of planting an ' +
110 | 'idea into the mind of a CEO.'
111 | }
112 | ];
113 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/table/table.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/app/components/datatable/demo4/data-table-demo4-data.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | 'name': 'Aaron 2Moore',
4 | 'phoneNumber': '611-898-6201'
5 | },
6 | {
7 | 'name': 'Yvonne Conroy Mrs.',
8 | 'phoneNumber': '115-850-0969'
9 | },
10 | {
11 | 'name': 'Laron Padberg',
12 | 'phoneNumber': '632-654-3034'
13 | },
14 | {
15 | 'name': 'Dr. Maryam Spinka',
16 | 'phoneNumber': '547-345-0067'
17 | },
18 | {
19 | 'name': 'Kiley Baumbach',
20 | 'phoneNumber': '958-524-5164'
21 | },
22 | {
23 | 'name': 'Hollis MacGyver',
24 | 'phoneNumber': '603-607-3241'
25 | },
26 | {
27 | 'name': 'Axel McLaughlin',
28 | 'phoneNumber': '983-639-0705'
29 | },
30 | {
31 | 'name': 'Ricardo Botsford',
32 | 'phoneNumber': '408-082-9480'
33 | },
34 | {
35 | 'name': 'Corbin Funk Mrs.',
36 | 'phoneNumber': '386-937-8683'
37 | },
38 | {
39 | 'name': 'Rosalind Paucek',
40 | 'phoneNumber': '977-661-7403'
41 | },
42 | {
43 | 'name': 'Henderson Moore',
44 | 'phoneNumber': '078-101-6377'
45 | },
46 | {
47 | 'name': 'Kelli Schoen',
48 | 'phoneNumber': '654-591-6561'
49 | },
50 | {
51 | 'name': 'Kenna Fritsch',
52 | 'phoneNumber': '790-480-2859'
53 | },
54 | {
55 | 'name': 'Judge Marquardt',
56 | 'phoneNumber': '100-494-1787'
57 | },
58 | {
59 | 'name': 'Kurtis Hane',
60 | 'phoneNumber': '008-800-2959'
61 | },
62 | {
63 | 'name': 'Nicolette Lind',
64 | 'phoneNumber': '007-908-2460'
65 | },
66 | {
67 | 'name': 'Idella Green',
68 | 'phoneNumber': '147-865-1578'
69 | },
70 | {
71 | 'name': 'Mackenzie Bartell',
72 | 'phoneNumber': '235-649-0980'
73 | },
74 | {
75 | 'name': 'Mose Kohler',
76 | 'phoneNumber': '614-886-4868'
77 | },
78 | {
79 | 'name': 'Cielo Kuphal',
80 | 'phoneNumber': '590-976-7492'
81 | },
82 | {
83 | 'name': 'Haleigh Stokes',
84 | 'phoneNumber': '418-255-9365'
85 | },
86 | {
87 | 'name': 'Tyrese Walter',
88 | 'phoneNumber': '041-555-9831'
89 | },
90 | {
91 | 'name': 'Barney Shields',
92 | 'phoneNumber': '379-438-0217'
93 | },
94 | {
95 | 'name': 'Favian Abbott Miss',
96 | 'phoneNumber': '642-808-5400'
97 | },
98 | {
99 | 'name': 'Carissa Kunze',
100 | 'phoneNumber': '949-983-0342'
101 | },
102 | {
103 | 'name': 'Aaron 2Moore',
104 | 'phoneNumber': '611-898-6201'
105 | },
106 | {
107 | 'name': 'Yvonne Conroy Mrs.',
108 | 'phoneNumber': '115-850-0969'
109 | },
110 | {
111 | 'name': 'Laron Padberg',
112 | 'phoneNumber': '632-654-3034'
113 | },
114 | {
115 | 'name': 'Dr. Maryam Spinka',
116 | 'phoneNumber': '547-345-0067'
117 | },
118 | {
119 | 'name': 'Kiley Baumbach',
120 | 'phoneNumber': '958-524-5164'
121 | },
122 | {
123 | 'name': 'Hollis MacGyver',
124 | 'phoneNumber': '603-607-3241'
125 | },
126 | {
127 | 'name': 'Axel McLaughlin',
128 | 'phoneNumber': '983-639-0705'
129 | },
130 | {
131 | 'name': 'Ricardo Botsford',
132 | 'phoneNumber': '408-082-9480'
133 | },
134 | {
135 | 'name': 'Corbin Funk Mrs.',
136 | 'phoneNumber': '386-937-8683'
137 | },
138 | {
139 | 'name': 'Rosalind Paucek',
140 | 'phoneNumber': '977-661-7403'
141 | },
142 | {
143 | 'name': 'Henderson Moore',
144 | 'phoneNumber': '078-101-6377'
145 | },
146 | {
147 | 'name': 'Kelli Schoen',
148 | 'phoneNumber': '654-591-6561'
149 | },
150 | {
151 | 'name': 'Kenna Fritsch',
152 | 'phoneNumber': '790-480-2859'
153 | },
154 | {
155 | 'name': 'Judge Marquardt',
156 | 'phoneNumber': '100-494-1787'
157 | },
158 | {
159 | 'name': 'Kurtis Hane',
160 | 'phoneNumber': '008-800-2959'
161 | },
162 | {
163 | 'name': 'Nicolette Lind',
164 | 'phoneNumber': '007-908-2460'
165 | },
166 | {
167 | 'name': 'Idella Green',
168 | 'phoneNumber': '147-865-1578'
169 | },
170 | {
171 | 'name': 'Mackenzie Bartell',
172 | 'phoneNumber': '235-649-0980'
173 | },
174 | {
175 | 'name': 'Mose Kohler',
176 | 'phoneNumber': '614-886-4868'
177 | },
178 | {
179 | 'name': 'Cielo Kuphal',
180 | 'phoneNumber': '590-976-7492'
181 | },
182 | {
183 | 'name': 'Haleigh Stokes',
184 | 'phoneNumber': '418-255-9365'
185 | },
186 | {
187 | 'name': 'Tyrese Walter',
188 | 'phoneNumber': '041-555-9831'
189 | },
190 | {
191 | 'name': 'Barney Shields',
192 | 'phoneNumber': '379-438-0217'
193 | },
194 | {
195 | 'name': 'Favian Abbott Miss',
196 | 'phoneNumber': '642-808-5400'
197 | },
198 | {
199 | 'name': 'Carissa Kunze',
200 | 'phoneNumber': '949-983-0342'
201 | },
202 | {
203 | 'name': 'Aaron 2Moore',
204 | 'phoneNumber': '611-898-6201'
205 | },
206 | {
207 | 'name': 'Yvonne Conroy Mrs.',
208 | 'phoneNumber': '115-850-0969'
209 | },
210 | {
211 | 'name': 'Laron Padberg',
212 | 'phoneNumber': '632-654-3034'
213 | },
214 | {
215 | 'name': 'Dr. Maryam Spinka',
216 | 'phoneNumber': '547-345-0067'
217 | },
218 | {
219 | 'name': 'Kiley Baumbach',
220 | 'phoneNumber': '958-524-5164'
221 | },
222 | {
223 | 'name': 'Hollis MacGyver',
224 | 'phoneNumber': '603-607-3241'
225 | },
226 | {
227 | 'name': 'Axel McLaughlin',
228 | 'phoneNumber': '983-639-0705'
229 | },
230 | {
231 | 'name': 'Ricardo Botsford',
232 | 'phoneNumber': '408-082-9480'
233 | },
234 | {
235 | 'name': 'Corbin Funk Mrs.',
236 | 'phoneNumber': '386-937-8683'
237 | },
238 | {
239 | 'name': 'Rosalind Paucek',
240 | 'phoneNumber': '977-661-7403'
241 | },
242 | {
243 | 'name': 'Henderson Moore',
244 | 'phoneNumber': '078-101-6377'
245 | },
246 | {
247 | 'name': 'Kelli Schoen',
248 | 'phoneNumber': '654-591-6561'
249 | },
250 | {
251 | 'name': 'Kenna Fritsch',
252 | 'phoneNumber': '790-480-2859'
253 | },
254 | {
255 | 'name': 'Judge Marquardt',
256 | 'phoneNumber': '100-494-1787'
257 | },
258 | {
259 | 'name': 'Kurtis Hane',
260 | 'phoneNumber': '008-800-2959'
261 | },
262 | {
263 | 'name': 'Nicolette Lind',
264 | 'phoneNumber': '007-908-2460'
265 | },
266 | {
267 | 'name': 'Idella Green',
268 | 'phoneNumber': '147-865-1578'
269 | },
270 | {
271 | 'name': 'Mackenzie Bartell',
272 | 'phoneNumber': '235-649-0980'
273 | },
274 | {
275 | 'name': 'Mose Kohler',
276 | 'phoneNumber': '614-886-4868'
277 | },
278 | {
279 | 'name': 'Cielo Kuphal',
280 | 'phoneNumber': '590-976-7492'
281 | },
282 | {
283 | 'name': 'Haleigh Stokes',
284 | 'phoneNumber': '418-255-9365'
285 | },
286 | {
287 | 'name': 'Tyrese Walter',
288 | 'phoneNumber': '041-555-9831'
289 | },
290 | {
291 | 'name': 'Barney Shields',
292 | 'phoneNumber': '379-438-0217'
293 | },
294 | {
295 | 'name': 'Favian Abbott Miss',
296 | 'phoneNumber': '642-808-5400'
297 | },
298 | {
299 | 'name': 'Carissa Kunze',
300 | 'phoneNumber': '949-983-0342'
301 | },
302 | {
303 | 'name': 'Favian Abbott Miss',
304 | 'phoneNumber': '642-808-5400'
305 | },
306 | {
307 | 'name': 'Carissa Kunze',
308 | 'phoneNumber': '949-983-0342'
309 | },
310 | {
311 | 'name': 'Favian Abbott Miss',
312 | 'phoneNumber': '642-808-5400'
313 | },
314 | {
315 | 'name': 'Carissa Kunze',
316 | 'phoneNumber': '949-983-0342'
317 | }
318 | ];
319 |
--------------------------------------------------------------------------------
/libs/datatable/README.md:
--------------------------------------------------------------------------------
1 | # @angular5/datatable
2 | This library contains a datatable component with built-in solutions for features including:
3 | - pagination
4 | - sorting
5 | - row selection (single/multi)
6 | - expandable rows
7 | - column resizing
8 | - selecting visible columns
9 | - accessibility support
10 | ---
11 | # Dependencies
12 | Furthermore the component is based on Bootstrap v4.0 (*CSS-only*) and Font-Awesome v4.7, hence be sure to include them into your project.
13 | Most likely you need to install them as dependencies...
14 |
15 | ```bash
16 | npm install bootstrap@4.0.0 font-awesome@4.7.0
17 | ```
18 |
19 | ... then you need to include the CSS bundles into .angular-cli.json file as show below.
20 | ```json
21 | {
22 | "apps": [
23 | {
24 | "root": "src",
25 | "styles": [
26 | "../node_modules/bootstrap/dist/css/bootstrap.min.css",
27 | "../node_modules/font-awesome/css/font-awesome.css",
28 | "styles.css"
29 | ]
30 | }
31 | ]
32 | }
33 | ```
34 |
35 | ---
36 | # Installation
37 | ```bash
38 | npm install angular5-data-table
39 | ```
40 | ---
41 | # Usage
42 | #### 1.Import Datatable module
43 | ```ts
44 | import { BrowserModule } from '@angular/platform-browser';
45 | import { NgModule } from '@angular/core';
46 | import { DataTableModule } from 'angular5-data-table';
47 |
48 | @NgModule({
49 | imports: [
50 | ...
51 | DataTableModule.forRoot()
52 | ...
53 | ],
54 | bootstrap: [AppComponent]
55 | })
56 | export class AppModule { }
57 | ```
58 |
59 |
60 | #### 2. Include `` and `` into your component's template.
61 | ```HTML
62 |
63 |
64 | ...
65 |
72 |
76 |
77 |
80 |
81 | ...
82 |
83 | ```
84 | ---
85 | # API
86 | The component is highly configurable and customizable through many *Input*s and *Event*s.
87 |
88 | ## data-table
89 | * `title` (`string` | default: `''`) table's name - it's highly recommend it's set for accessibility reasons as this will provide a better experience when interacting with the component, especially through a SR.
90 | * `showTitle` (`boolean` | default: `true`): if `false`, the title is not shown into the component. Useful when want the header component visible (with its Reload and Coulumn Selector buttons), but not the title.
91 | * `items` (`JsonObject[]` | default: `[]`) table data to show.
92 | * `itemCount` (`number` | default: `0`) items's count.
93 | * `header` (`boolean` | default: `true`) show/hide the table header sub-component - this holds the table name and two buttons (_reload table_ and _column selector_).
94 | * `pagination` (`boolean` | default: ) enable pagination. If `true`, pagination controls are shown at the bottom of the table.
95 | * `indexColumn` (`boolean` | default: `true`) when `true` the table shows a 0-indexed column.
96 | * `indexColumnHeader` (`string` | default: `''`) text shown as column header for the index column.
97 | * `selectColumn` (`boolean` | default: `false`) when `true` the table shows a checkbox column for selecting specific row.
98 | * `multiSelect` (`boolean` | default: `false`) allows multi-row selection, showing a checkbox at select's column header.
99 | * `labels` (`DataTableTranslations` | default: `defaultTranslations`) interface holding all needed labels. You can pass a subset of the labels. The missing labels will be defaulted.
100 | * `expandableRow` (`boolean` | default: `false`) when `true` each row will have a collapsible content.
101 | * `selectOnRowClick` (`boolean` | default: `false`) when `true` each row is selectable via a single-click.
102 | * `reload` (`function(): void` | default: `null`) function that is invoked when the table needs to re-render its data. Note: most of the times this is the place where the developer connects to a server in order to pull down the item set.
103 | * `autoReload` (`boolean` | default: `false`) when `true`, the `reload` function gets invoked and init time (`ngOnInit`).
104 | * `rowColors` (`function(): 'color` | default: `null`) custom function that must return a _CSS color_ that will be applied to the entire row.
105 | * `rowTooltip` (`function` | default: `null`) custom function to show a title tooltip when hovering the row.
106 | * `showReloading` (`boolean` | default: `true`) when `true` an overlay with a gear icon is shown on top of the table while it's reloading.
107 | * `noDataMessage` (`string` | default: `''`) message displayed when no item are displayed. If it's empty nothing is shown.
108 | * `pageLimits` (`number[]` | default: `[10, 25, 50, 100, 250]`) items per page selector options.
109 | * `primaryColumn` (`string` | default: first data column) it identifies which columns has be marked as primary. This is an important aspect from an accessibility and SR perspective. If not given, the first column will be the primary column.
110 | * `page` (`number` | default: `0`) page to load, valid only if pagination is enabled.
111 | * `limit` (`number` | default: `10`) number of items per page, valid only pagination is enabled. If `limit` value is not a valid (not contained into `pageLimits` array) it will be defaulted to `pageLimits`'s first value.
112 | * `sortBy` (`string` | default: `''`) column table is sorted by.
113 | * `sortAsc` (`boolean` | default: `true`) valid only if `sortBy` is not defaulted. Defines the sorting order. If `true` sort is ascending, descending otherwise.
114 |
115 | ## data-table-column
116 | * `property` (`string` | default: _no default_) item's `JSONObject` key used to retrieve the row cell content.
117 | * `header` (`string` | default: _no default_) column header text.
118 | * `sortable` (`boolean` | default: `false`) marks the columns as sortable.
119 | * `resizable` (`boolean` | default: `false`) marks the columns as resizable.
120 | * `visible` (`boolean` | default: `true`) marks the columns visible.
121 | * `width` (`number | string` | default: `''`) defines the column width. It can be a string like `2rem` or a number. If it's a number, it will be considered as pixels.
122 |
123 | ### Custom column templates
124 | data-column's content and header are not restricted to be text only. They can hold more complex components. In order to do that developers can use two references: `#dataTableHeader` and `#dataTableCell`.
125 |
126 | #### Usage and sample
127 | ```html
128 |
129 | ...
130 |
132 |
133 | Actions
134 |
135 |
136 | Buy
137 |
138 |
139 | ...
140 |
141 | ```
142 | As it can be seen from the above snippet, the `dataTableHeader` and `dataTableCell` are targeting two ``s nodes which will be used respectively as column header and cell content. In both cases `item` refers to the whole row item, so developers can use whatever they may need.
143 |
144 | ---
145 | # Accessibility
146 | The library is fully tested with NVDA and Mozilla Firefox. Other browsers or screen readers combination may be supported too.
147 | If developers use `dataTableHeader` and `dataTableCell` for getting displayed custom text, accessibility and SR support go out from the datatable component support range. That means that, although this component is *fully* accessible, in scenarios where column custom templates are used some accessibility checks may fail. It's hence developer's responsibility to fix any potential accessibility issue. See the demo's example for more info.
148 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular 5 DataTable
2 |
3 | *Note*: Originally this was fork of this [package](https://github.com/MIt9/angular-4-data-table).
4 |
5 | A simple Angular 5 data table, with built-in solutions for features including:
6 | * pagination
7 | * sorting
8 | * row selection (single/multi)
9 | * expandable rows
10 | * column resizing
11 | * selecting visible columns
12 | * accessibility
13 |
14 | The component can be used not just with local data, but remote resources too, ie. when sorting and/or pagination are implemented server side.
15 |
16 | The library is packaged with [`ng-packagr`](https://github.com/dherges/ng-packagr).
17 |
18 | ## Dependencies
19 | Furthermore the component is based on Bootstrap v4.0 (*CSS-only*) and Font-Awesome v4.7, hence be sure to include them into your project.
20 | Most likely you need to install them as dependencies...
21 |
22 | ```bash
23 | npm install bootstrap@4.0.0 font-awesome@4.7.0
24 | ```
25 |
26 | ... then you need to include the CSS bundles into .angular-cli.json file as show below.
27 | ```json
28 | {
29 | "apps": [
30 | {
31 | "root": "src",
32 | "styles": [
33 | "../node_modules/bootstrap/dist/css/bootstrap.min.css",
34 | "../node_modules/font-awesome/css/font-awesome.css",
35 | "styles.css"
36 | ]
37 | }
38 | ]
39 | }
40 | ```
41 |
42 | ## Installation
43 |
44 | ```bash
45 | npm install angular5-data-table
46 | ```
47 |
48 | ## Usage
49 |
50 | ##### 1.Import Datatable module
51 | ```ts
52 | import { BrowserModule } from '@angular/platform-browser';
53 | import { NgModule } from '@angular/core';
54 | import { DataTableModule } from 'angular5-data-table';
55 |
56 | @NgModule({
57 | imports: [
58 | ...
59 | DataTableModule.forRoot()
60 | ...
61 | ],
62 | bootstrap: [AppComponent]
63 | })
64 | export class AppModule { }
65 | ```
66 |
67 | ##### 2. Include `` and `` into your component's template.
68 | ```HTML
69 |
70 |
71 | ...
72 |
79 |
83 |
84 |
87 |
88 | ...
89 |
90 | ```
91 |
92 | ## API
93 |
94 | ### data-table
95 | * `title` (`string` | default: `''`) table's name - it's highly recommend it's set for accessibility reasons as this will provide a better experience when interacting with the component, especially through a SR.
96 | * `showTitle` (`boolean` | default: `true`): if `false`, the title is not shown into the component. Useful when want the header component visible (with its Reload and Coulumn Selector buttons), but not the title.
97 | * `items` (`JsonObject[]` | default: `[]`) table data to show.
98 | * `itemCount` (`number` | default: `0`) items's count.
99 | * `header` (`boolean` | default: `true`) show/hide the table header sub-component - this holds the table name and two buttons (_reload table_ and _column selector_).
100 | * `pagination` (`boolean` | default: ) enable pagination. If `true`, pagination controls are shown at the bottom of the table.
101 | * `indexColumn` (`boolean` | default: `true`) when `true` the table shows a 0-indexed column.
102 | * `indexColumnHeader` (`string` | default: `''`) text shown as column header for the index column.
103 | * `selectColumn` (`boolean` | default: `false`) when `true` the table shows a checkbox column for selecting specific row.
104 | * `multiSelect` (`boolean` | default: `false`) allows multi-row selection, showing a checkbox at select's column header.
105 | * `labels` (`DataTableTranslations` | default: `defaultTranslations`) interface holding all needed labels. You can pass a subset of the labels. The missing labels will be defaulted.
106 | * `expandableRow` (`boolean` | default: `false`) when `true` each row will have a collapsible content.
107 | * `selectOnRowClick` (`boolean` | default: `false`) when `true` each row is selectable via a single-click.
108 | * `reload` (`function(): void` | default: `null`) function that is invoked when the table needs to re-render its data. Note: most of the times this is the place where the developer connects to a server in order to pull down the item set.
109 | * `autoReload` (`boolean` | default: `false`) when `true`, the `reload` function gets invoked and init time (`ngOnInit`).
110 | * `rowColors` (`function(): 'color` | default: `null`) custom function that must return a _CSS color_ that will be applied to the entire row.
111 | * `rowTooltip` (`function` | default: `null`) custom function to show a title tooltip when hovering the row.
112 | * `showReloading` (`boolean` | default: `true`) when `true` an overlay with a gear icon is shown on top of the table while it's reloading.
113 | * `noDataMessage` (`string` | default: `''`) message displayed when no item are displayed. If it's empty nothing is shown.
114 | * `pageLimits` (`number[]` | default: `[10, 25, 50, 100, 250]`) items per page selector options.
115 | * `primaryColumn` (`string` | default: first data column) it identifies which columns has be marked as primary. This is an important aspect from an accessibility and SR perspective. If not given, the first column will be the primary column.
116 | * `page` (`number` | default: `0`) page to load, valid only if pagination is enabled.
117 | * `limit` (`number` | default: `10`) number of items per page, valid only pagination is enabled. If `limit` value is not a valid (not contained into `pageLimits` array) it will be defaulted to `pageLimits`'s first value.
118 | * `sortBy` (`string` | default: `''`) column table is sorted by.
119 | * `sortAsc` (`boolean` | default: `true`) valid only if `sortBy` is not defaulted. Defines the sorting order. If `true` sort is ascending, descending otherwise.
120 |
121 | ### data-table-column
122 | * `property` (`string` | default: _no default_) item's `JSONObject` key used to retrieve the row cell content.
123 | * `header` (`string` | default: _no default_) column header text.
124 | * `sortable` (`boolean` | default: `false`) marks the columns as sortable.
125 | * `resizable` (`boolean` | default: `false`) marks the columns as resizable.
126 | * `visible` (`boolean` | default: `true`) marks the columns visible.
127 | * `width` (`number | string` | default: `''`) defines the column width. It can be a string like `2rem` or a number. If it's a number, it will be considered as pixels.
128 |
129 | ### Custom column templates
130 | data-column's content and header are not restricted to be text only - they can hold complex content too. In order to do that developers can use two references: `#dataTableHeader` and `#dataTableCell`.
131 |
132 | # Usage and sample
133 | ```html
134 |
135 | ...
136 |
138 |
139 | Actions
140 |
141 |
142 | Buy
143 |
144 |
145 | ...
146 |
147 | ```
148 | As it can be seen from the above snippet, the `dataTableHeader` and `dataTableCell` are targeting two ``s nodes which will be used respectively as column header and cell content. In both cases `item` refers to the whole row item, so developers can use whatever they may need.
149 |
150 | ## Demo app
151 | Clone this repository, run `npm install` and `ng serve`, then navigate to `http:localhost:4200` where you can access to the demo application sporting a few demos with code viewer and docs.
152 |
153 | ##Examples
154 | 
155 | 
156 | 
157 | 
158 | 
159 |
160 | ## License
161 | MIT License
162 |
--------------------------------------------------------------------------------
/libs/datatable/src/components/table/table.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AfterContentInit,
3 | Component,
4 | ContentChild,
5 | ContentChildren,
6 | EventEmitter,
7 | Input,
8 | OnDestroy,
9 | OnInit,
10 | Output,
11 | QueryList,
12 | TemplateRef,
13 | ViewChildren
14 | } from '@angular/core';
15 | import { DataTableColumnDirective } from '../../directives/column/column.directive';
16 | import { DataTableRowComponent } from '../row/row.component';
17 | import { DataTableParams } from '../../types/data-table-params.type';
18 | import { RowCallback } from '../../types/row-callback.type';
19 | import { DataTableTranslations } from '../../types/data-table-translations.type';
20 | import { defaultTranslations } from '../../types/default-translations.type';
21 | import { drag } from '../../utils/drag';
22 |
23 | import { Subject } from 'rxjs/Subject';
24 | import { Subscription } from 'rxjs/Subscription';
25 | import 'rxjs/add/operator/debounceTime';
26 | import 'rxjs/add/operator/do';
27 |
28 | let nextId = 0;
29 |
30 | @Component({
31 | selector: 'data-table',
32 | templateUrl: './table.component.html',
33 | styleUrls: ['./table.component.css']
34 | })
35 | export class DataTableComponent implements DataTableParams, OnInit, AfterContentInit, OnDestroy {
36 |
37 | private _items: any[] = [];
38 | private _itemCount;
39 |
40 | @Input()
41 | get items(): any[] {
42 | return this._items;
43 | }
44 |
45 | set items(items: any[]) {
46 | this._items = items;
47 | // no need to call notifier.next() because _onReloadFinished()
48 | // will change reloaded value causing notifier.next() to be called implicitly.
49 | this._onReloadFinished();
50 | }
51 |
52 |
53 | @Input()
54 | get itemCount(): number {
55 | return this._itemCount;
56 | }
57 |
58 | set itemCount(count: number) {
59 | this._itemCount = count;
60 | this.notifier.next();
61 | }
62 |
63 | // UI components:
64 | @ContentChildren(DataTableColumnDirective) columns: QueryList;
65 | @ViewChildren(DataTableRowComponent) rows: QueryList;
66 | @ContentChild('dataTableExpand') expandTemplate: TemplateRef;
67 |
68 | // One-time optional bindings with default values:
69 | @Input() title = '';
70 | @Input() showTitle = true;
71 | @Input() header = true;
72 | @Input() pagination = true;
73 | @Input() indexColumn = true;
74 | @Input() indexColumnHeader = '';
75 | @Input() rowColors: RowCallback;
76 | @Input() rowTooltip: RowCallback;
77 | @Input() selectColumn = false;
78 | @Input() multiSelect = true;
79 | @Input() substituteRows = true;
80 | @Input() expandableRows = false;
81 | @Input() labels: DataTableTranslations;
82 | @Input() selectOnRowClick = false;
83 | @Input() autoReload = true;
84 | @Input() showReloading = false;
85 | @Input() noDataMessage: string;
86 | @Input() pageLimits: number[] = [10, 25, 50, 100, 250];
87 | @Input() primaryColumn = '';
88 |
89 | // reload emitter
90 | @Output() reload = new EventEmitter();
91 |
92 | // event handlers:
93 | @Output() rowClick = new EventEmitter();
94 | @Output() rowDoubleClick = new EventEmitter();
95 | @Output() headerClick = new EventEmitter();
96 | @Output() cellClick = new EventEmitter();
97 | // UI state without input:
98 | indexColumnVisible: boolean;
99 | selectColumnVisible: boolean;
100 | expandColumnVisible: boolean;
101 |
102 | // ada notifications.
103 | reloadNotification: string;
104 | paginationNotification: string;
105 | sortNotification: string;
106 | columnSelectorNotification: string;
107 |
108 | _displayParams = {}; // params of the last finished reload
109 |
110 | subject = new Subject();
111 | subject$: Subscription;
112 |
113 | notifier = new Subject();
114 | notifier$: Subscription;
115 |
116 | // selection:
117 | selectedRow: DataTableRowComponent;
118 | selectedRows: DataTableRowComponent[] = [];
119 |
120 | Math: any;
121 | id = `datatable-${nextId++}`;
122 |
123 | // select all checkbox flag
124 | private _selectAllCheckbox = false;
125 |
126 | // column resizing:
127 | private _resizeInProgress = false;
128 |
129 | resizeLimit = 30;
130 |
131 | // Reloading:
132 | _reloading = false;
133 |
134 | get reloading() {
135 | return this._reloading;
136 | }
137 |
138 | set reloading(val: boolean) {
139 | this._reloading = val;
140 | this.notifier.next();
141 | }
142 |
143 | // UI state: visible get/set for the outside with @Input for one-time initial values
144 | private _sortBy: string;
145 |
146 | @Input()
147 | get sortBy(): string {
148 | return this._sortBy;
149 | }
150 |
151 | set sortBy(value: string) {
152 | this._sortBy = value;
153 | this.subject.next();
154 | }
155 |
156 | private _sortAsc = true;
157 |
158 | @Input()
159 | get sortAsc(): boolean {
160 | return this._sortAsc;
161 | }
162 |
163 | set sortAsc(value: boolean) {
164 | this._sortAsc = value;
165 | this.subject.next();
166 | }
167 |
168 | private _offset = 0;
169 |
170 | @Input()
171 | get offset(): number {
172 | return this._offset;
173 | }
174 |
175 | set offset(value: number) {
176 | this._offset = value;
177 | this.subject.next();
178 | }
179 |
180 | private _limit = 10;
181 |
182 | @Input()
183 | get limit(): number {
184 | return this._limit;
185 | }
186 |
187 | set limit(value: number) {
188 | this._limit = value;
189 | this.subject.next();
190 | }
191 |
192 | // calculated property:
193 | @Input()
194 | get page() {
195 | return this.itemCount !== 0 ? Math.floor(this.offset / this.limit) + 1 : 0;
196 | }
197 |
198 | set page(value) {
199 | this.offset = (value - 1) * this.limit;
200 | }
201 |
202 | get lastPage() {
203 | return Math.ceil(this.itemCount / this.limit);
204 | }
205 |
206 | // setting multiple observable properties simultaneously
207 | sort(sortBy: string, asc: boolean) {
208 | this.sortBy = sortBy;
209 | this.sortAsc = asc;
210 | }
211 |
212 | // init
213 | ngOnInit() {
214 | this._initDefaultValues();
215 | this._initDefaultClickEvents();
216 | this._updateDisplayParams();
217 |
218 | if (this.pageLimits.indexOf(this.limit) < 0) {
219 | this.limit = this.pageLimits[0];
220 | }
221 |
222 | this.labels = {...defaultTranslations, ...this.labels};
223 |
224 | if (this.autoReload) {
225 | this.reloadItems();
226 | }
227 |
228 | this.notifier$ = this.notifier.subscribe(() => this._notify());
229 | this.subject$ = this.subject.debounceTime(100).subscribe(() => this.reloadItems());
230 |
231 | }
232 |
233 | private _initDefaultValues() {
234 | this.indexColumnVisible = this.indexColumn;
235 | this.selectColumnVisible = this.selectColumn;
236 | this.expandColumnVisible = this.expandableRows;
237 | }
238 |
239 | private _initDefaultClickEvents() {
240 | this.headerClick.subscribe(
241 | (tableEvent: { column: DataTableColumnDirective, event: Event }) => this.sortColumn(tableEvent.column));
242 | if (this.selectOnRowClick) {
243 | this.rowClick.subscribe(
244 | (tableEvent: { row: DataTableRowComponent, event: Event }) => tableEvent.row.selected = !tableEvent.row.selected);
245 | }
246 | }
247 |
248 | reloadItems() {
249 | this.reloading = true;
250 | this.reload.emit(this._getRemoteParameters());
251 | }
252 |
253 | private _onReloadFinished() {
254 | if (this.reloading) {
255 | this._updateDisplayParams();
256 | this._selectAllCheckbox = false;
257 | this.reloading = false;
258 | }
259 | }
260 | get displayParams() {
261 | return this._displayParams;
262 | }
263 |
264 | _updateDisplayParams() {
265 | this._displayParams = {
266 | sortBy: this.sortBy,
267 | sortAsc: this.sortAsc,
268 | offset: this.offset,
269 | limit: this.limit
270 | };
271 | }
272 |
273 | constructor() { }
274 |
275 | public rowClicked(row: DataTableRowComponent, event: Event) {
276 | this.rowClick.emit({row, event});
277 | }
278 |
279 | public rowDoubleClicked(row: DataTableRowComponent, event: Event) {
280 | this.rowDoubleClick.emit({row, event});
281 | }
282 |
283 | public headerClicked(column: DataTableColumnDirective, event: Event) {
284 | if (!this._resizeInProgress) {
285 | event.preventDefault();
286 | event.stopPropagation();
287 | this.headerClick.emit({column, event});
288 | } else {
289 | this._resizeInProgress = false; // this is because I can't prevent click from mousup of the drag end
290 | }
291 | }
292 |
293 | private cellClicked(column: DataTableColumnDirective, row: DataTableRowComponent, event: MouseEvent) {
294 | this.cellClick.emit({row, column, event});
295 | }
296 |
297 | // functions:
298 | private _getRemoteParameters(): DataTableParams {
299 | const params = {};
300 |
301 | if (this.sortBy) {
302 | params.sortBy = this.sortBy;
303 | params.sortAsc = this.sortAsc;
304 | }
305 | if (this.pagination) {
306 | params.offset = this.offset;
307 | params.limit = this.limit;
308 | }
309 | return params;
310 | }
311 |
312 | private sortColumn(column: DataTableColumnDirective) {
313 | if (column.sortable) {
314 | const ascending = this.sortBy === column.property ? !this.sortAsc : true;
315 | this.sort(column.property, ascending);
316 | }
317 | }
318 |
319 | get columnCount() {
320 | let count = 0;
321 | count += this.indexColumnVisible ? 1 : 0;
322 | count += this.selectColumnVisible ? 1 : 0;
323 | count += this.expandColumnVisible ? 1 : 0;
324 | this.columns.toArray().forEach(column => {
325 | count += column.visible ? 1 : 0;
326 | });
327 | return count;
328 | }
329 |
330 | public getRowColor(item: any, index: number, row: DataTableRowComponent) {
331 | if (this.rowColors !== undefined) {
332 | return (this.rowColors)(item, row, index);
333 | }
334 | }
335 |
336 | get selectAllCheckbox() {
337 | return this._selectAllCheckbox;
338 | }
339 |
340 | set selectAllCheckbox(value) {
341 | this._selectAllCheckbox = value;
342 | this._onSelectAllChanged(value);
343 | }
344 |
345 | private _onSelectAllChanged(value: boolean) {
346 | this.rows.toArray().forEach(row => row.selected = value);
347 | }
348 |
349 | onRowSelectChanged(row: DataTableRowComponent) {
350 |
351 | // maintain the selectedRow(s) view
352 | if (this.multiSelect) {
353 | const index = this.selectedRows.indexOf(row);
354 | if (row.selected && index < 0) {
355 | this.selectedRows.push(row);
356 | } else if (!row.selected && index >= 0) {
357 | this.selectedRows.splice(index, 1);
358 | }
359 | } else {
360 | if (row.selected) {
361 | this.selectedRow = row;
362 | } else if (this.selectedRow === row) {
363 | delete this.selectedRow;
364 | }
365 | }
366 |
367 | // unselect all other rows:
368 | if (row.selected && !this.multiSelect) {
369 | this.rows.toArray().filter(row_ => row_.selected).forEach(row_ => {
370 | if (row_ !== row) { // avoid endless loop
371 | row_.selected = false;
372 | }
373 | });
374 | }
375 | }
376 |
377 | // other:
378 |
379 | get substituteItems() {
380 | return Array.from({length: this.displayParams.limit - this.items.length});
381 | }
382 |
383 | private resizeColumnStart(event: MouseEvent, column: DataTableColumnDirective, columnElement: HTMLElement) {
384 | this._resizeInProgress = true;
385 | let startOffset = columnElement.offsetWidth - event.pageX;
386 | drag(event, {
387 | move: (moveEvent: MouseEvent, dx: number) => {
388 | if (this._isResizeInLimit(columnElement, dx)) {
389 | column.width = startOffset + moveEvent.pageX + dx;
390 | }
391 | },
392 | });
393 | }
394 |
395 | private _isResizeInLimit(columnElement: HTMLElement, dx: number) {
396 | /* This is needed because CSS min-width didn't work on table-layout: fixed.
397 | Without the limits, resizing can make the next column disappear completely,
398 | and even increase the table width. The current implementation suffers from the fact,
399 | that offsetWidth sometimes contains out-of-date values. */
400 | if ((dx < 0 && (columnElement.offsetWidth + dx) <= this.resizeLimit) ||
401 | !columnElement.nextElementSibling || // resizing doesn't make sense for the last visible column
402 | (dx >= 0 && (( columnElement.nextElementSibling).offsetWidth + dx) <= this.resizeLimit)) {
403 | return false;
404 | }
405 | return true;
406 | }
407 |
408 | ngAfterContentInit(): void {
409 | if (this.primaryColumn === '') {
410 | this.primaryColumn = (this.columns.first as DataTableColumnDirective).property;
411 | }
412 | }
413 |
414 | _notify(): void {
415 | const loading = this.reloading;
416 |
417 | this.reloadNotification = loading ?
418 | this.labels.loadingText.replace('{title}', this.title) :
419 | this.labels.loadedText.replace('{title}', this.title);
420 |
421 | if (!loading) {
422 | if (this.pagination) {
423 | this.paginationNotification = this.labels.paginationText
424 | .replace('{from}', '' + (Math.ceil(this.itemCount / this.limit) !== 0 ? this.offset + 1 : '0'))
425 | .replace('{to}', '' + (Math.min(this.offset + this.limit, this.itemCount)))
426 | .replace('{total}', '' + this.itemCount);
427 | } else {
428 | this.paginationNotification = '';
429 | }
430 | if (this.columns !== undefined && this.sortBy !== undefined) {
431 | const col = this.columns.toArray().find(column => column.property === this.sortBy) as DataTableColumnDirective;
432 | this.sortNotification = (this.sortAsc ? this.labels.sortedAscending : this.labels.sortedDescending)
433 | .replace('{title}', this.title)
434 | .replace('{header}', col.header);
435 | } else {
436 | this.sortNotification = '';
437 | }
438 | }
439 | }
440 |
441 | ngOnDestroy() {
442 | this.subject$.unsubscribe();
443 | this.notifier$.unsubscribe();
444 | }
445 | }
446 |
--------------------------------------------------------------------------------
/src/app/components/datatable/demo1/data-table-demo1-data.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | 'name': 'Aaron 2Moore', 'email': 'Heath44@hotmail.com', 'jobTitle': 'Regional Configuration Producer',
4 | 'active': true, 'phoneNumber': '611-898-6201', 'date': '2015-11-06T07:21:25.510Z'
5 | },
6 | {
7 | 'name': 'Yvonne Conroy Mrs.', 'email': 'Gideon9@yahoo.com', 'jobTitle': 'Global Mobility Orchestrator',
8 | 'active': false, 'phoneNumber': '115-850-0969', 'date': '2014-12-20T00:48:40.276Z'
9 | },
10 | {
11 | 'name': 'Laron Padberg', 'email': 'Laney_Huels@hotmail.com', 'jobTitle': 'Senior Directives Supervisor',
12 | 'active': false, 'phoneNumber': '632-654-3034', 'date': '2015-09-29T04:33:38.544Z'
13 | },
14 | {
15 | 'name': 'Dr. Maryam Spinka', 'email': 'Aletha.Labadie@hotmail.com', 'jobTitle': 'Dynamic Mobility Associate',
16 | 'active': true, 'phoneNumber': '547-345-0067', 'date': '2015-09-23T01:13:39.320Z'
17 | },
18 | {
19 | 'name': 'Kiley Baumbach', 'email': 'Rogelio24@hotmail.com', 'jobTitle': 'Principal Metrics Orchestrator',
20 | 'active': true, 'phoneNumber': '958-524-5164', 'date': '2014-12-05T23:39:27.340Z'
21 | },
22 | {
23 | 'name': 'Hollis MacGyver', 'email': 'Yazmin.Heidenreich97@gmail.com', 'jobTitle': 'Direct Markets Assistant',
24 | 'active': true, 'phoneNumber': '603-607-3241', 'date': '2015-02-12T10:40:52.977Z'
25 | },
26 | {
27 | 'name': 'Axel McLaughlin', 'email': 'Deon_Heaney@gmail.com', 'jobTitle': 'Forward Mobility Architect',
28 | 'active': false, 'phoneNumber': '983-639-0705', 'date': '2015-03-01T02:28:26.030Z'
29 | },
30 | {
31 | 'name': 'Ricardo Botsford', 'email': 'Melisa73@yahoo.com', 'jobTitle': 'Direct Quality Consultant',
32 | 'active': true, 'phoneNumber': '408-082-9480', 'date': '2015-01-31T03:41:54.611Z'
33 | },
34 | {
35 | 'name': 'Corbin Funk Mrs.', 'email': 'Marjory.Morissette51@gmail.com', 'jobTitle': 'Human Configuration Manager',
36 | 'active': true, 'phoneNumber': '386-937-8683', 'date': '2014-12-05T15:07:36.843Z'
37 | },
38 | {
39 | 'name': 'Rosalind Paucek', 'email': 'Ivy_Stanton@gmail.com', 'jobTitle': 'Future Creative Supervisor',
40 | 'active': true, 'phoneNumber': '977-661-7403', 'date': '2015-06-10T17:42:38.644Z'
41 | },
42 | {
43 | 'name': 'Henderson Moore', 'email': 'Randi_Corkery@hotmail.com', 'jobTitle': 'Internal Accountability Director',
44 | 'active': true, 'phoneNumber': '078-101-6377', 'date': '2015-09-26T05:14:34.913Z'
45 | },
46 | {
47 | 'name': 'Kelli Schoen', 'email': 'Reva.Kiehn54@yahoo.com', 'jobTitle': 'National Accountability Architect',
48 | 'active': false, 'phoneNumber': '654-591-6561', 'date': '2015-05-04T06:50:37.482Z'
49 | },
50 | {
51 | 'name': 'Kenna Fritsch', 'email': 'Wilburn2@gmail.com', 'jobTitle': 'Legacy Response Administrator',
52 | 'active': true, 'phoneNumber': '790-480-2859', 'date': '2015-10-10T23:37:05.867Z'
53 | },
54 | {
55 | 'name': 'Judge Marquardt', 'email': 'Letha_Champlin69@hotmail.com', 'jobTitle': 'Human Program Specialist',
56 | 'active': true, 'phoneNumber': '100-494-1787', 'date': '2015-04-04T23:29:48.588Z'
57 | },
58 | {
59 | 'name': 'Kurtis Hane', 'email': 'Mona.Gaylord47@yahoo.com', 'jobTitle': 'International Optimization Director',
60 | 'active': false, 'phoneNumber': '008-800-2959', 'date': '2014-12-04T21:09:50.722Z'
61 | },
62 | {
63 | 'name': 'Nicolette Lind', 'email': 'Thurman30@yahoo.com', 'jobTitle': 'Legacy Marketing Facilitator',
64 | 'active': true, 'phoneNumber': '007-908-2460', 'date': '2015-06-22T08:11:57.381Z'
65 | },
66 | {
67 | 'name': 'Idella Green', 'email': 'Fernando_Ward@yahoo.com', 'jobTitle': 'Dynamic Division Orchestrator',
68 | 'active': false, 'phoneNumber': '147-865-1578', 'date': '2015-02-12T23:00:31.283Z'
69 | },
70 | {
71 | 'name': 'Mackenzie Bartell', 'email': 'Price25@yahoo.com', 'jobTitle': 'National Directives Associate',
72 | 'active': false, 'phoneNumber': '235-649-0980', 'date': '2015-06-24T20:21:51.356Z'
73 | },
74 | {
75 | 'name': 'Mose Kohler', 'email': 'Malika56@hotmail.com', 'jobTitle': 'Lead Implementation Executive',
76 | 'active': true, 'phoneNumber': '614-886-4868', 'date': '2015-03-04T13:05:23.698Z'
77 | },
78 | {
79 | 'name': 'Cielo Kuphal', 'email': 'Jude_Terry24@gmail.com', 'jobTitle': 'Dynamic Division Analyst',
80 | 'active': false, 'phoneNumber': '590-976-7492', 'date': '2015-06-02T20:52:32.664Z'
81 | },
82 | {
83 | 'name': 'Haleigh Stokes', 'email': 'Belle_Herman64@yahoo.com', 'jobTitle': 'Global Intranet Executive',
84 | 'active': false, 'phoneNumber': '418-255-9365', 'date': '2015-04-10T00:32:10.283Z'
85 | },
86 | {
87 | 'name': 'Tyrese Walter', 'email': 'Garland.Veum52@hotmail.com', 'jobTitle': 'Senior Web Liason',
88 | 'active': false, 'phoneNumber': '041-555-9831', 'date': '2015-08-18T20:05:08.839Z'
89 | },
90 | {
91 | 'name': 'Barney Shields', 'email': 'Anika27@gmail.com', 'jobTitle': 'District Web Administrator',
92 | 'active': true, 'phoneNumber': '379-438-0217', 'date': '2015-06-01T09:28:46.778Z'
93 | },
94 | {
95 | 'name': 'Favian Abbott Miss', 'email': 'Palma_Little@hotmail.com', 'jobTitle': 'Lead Implementation Facilitator',
96 | 'active': false, 'phoneNumber': '642-808-5400', 'date': '2015-08-09T07:38:06.588Z'
97 | },
98 | {
99 | 'name': 'Carissa Kunze', 'email': 'Merl_Frami@yahoo.com', 'jobTitle': 'Regional Division Technician',
100 | 'active': true, 'phoneNumber': '949-983-0342', 'date': '2015-11-05T08:09:09.463Z'
101 | },
102 | {
103 | 'name': 'Aaron 2Moore', 'email': 'Heath44@hotmail.com', 'jobTitle': 'Regional Configuration Producer',
104 | 'active': true, 'phoneNumber': '611-898-6201', 'date': '2015-11-06T07:21:25.510Z'
105 | },
106 | {
107 | 'name': 'Yvonne Conroy Mrs.', 'email': 'Gideon9@yahoo.com', 'jobTitle': 'Global Mobility Orchestrator',
108 | 'active': false, 'phoneNumber': '115-850-0969', 'date': '2014-12-20T00:48:40.276Z'
109 | },
110 | {
111 | 'name': 'Laron Padberg', 'email': 'Laney_Huels@hotmail.com', 'jobTitle': 'Senior Directives Supervisor',
112 | 'active': false, 'phoneNumber': '632-654-3034', 'date': '2015-09-29T04:33:38.544Z'
113 | },
114 | {
115 | 'name': 'Dr. Maryam Spinka', 'email': 'Aletha.Labadie@hotmail.com', 'jobTitle': 'Dynamic Mobility Associate',
116 | 'active': true, 'phoneNumber': '547-345-0067', 'date': '2015-09-23T01:13:39.320Z'
117 | },
118 | {
119 | 'name': 'Kiley Baumbach', 'email': 'Rogelio24@hotmail.com', 'jobTitle': 'Principal Metrics Orchestrator',
120 | 'active': true, 'phoneNumber': '958-524-5164', 'date': '2014-12-05T23:39:27.340Z'
121 | },
122 | {
123 | 'name': 'Hollis MacGyver', 'email': 'Yazmin.Heidenreich97@gmail.com', 'jobTitle': 'Direct Markets Assistant',
124 | 'active': true, 'phoneNumber': '603-607-3241', 'date': '2015-02-12T10:40:52.977Z'
125 | },
126 | {
127 | 'name': 'Axel McLaughlin', 'email': 'Deon_Heaney@gmail.com', 'jobTitle': 'Forward Mobility Architect',
128 | 'active': false, 'phoneNumber': '983-639-0705', 'date': '2015-03-01T02:28:26.030Z'
129 | },
130 | {
131 | 'name': 'Ricardo Botsford', 'email': 'Melisa73@yahoo.com', 'jobTitle': 'Direct Quality Consultant',
132 | 'active': true, 'phoneNumber': '408-082-9480', 'date': '2015-01-31T03:41:54.611Z'
133 | },
134 | {
135 | 'name': 'Corbin Funk Mrs.', 'email': 'Marjory.Morissette51@gmail.com', 'jobTitle': 'Human Configuration Manager',
136 | 'active': true, 'phoneNumber': '386-937-8683', 'date': '2014-12-05T15:07:36.843Z'
137 | },
138 | {
139 | 'name': 'Rosalind Paucek', 'email': 'Ivy_Stanton@gmail.com', 'jobTitle': 'Future Creative Supervisor',
140 | 'active': true, 'phoneNumber': '977-661-7403', 'date': '2015-06-10T17:42:38.644Z'
141 | },
142 | {
143 | 'name': 'Henderson Moore', 'email': 'Randi_Corkery@hotmail.com', 'jobTitle': 'Internal Accountability Director',
144 | 'active': true, 'phoneNumber': '078-101-6377', 'date': '2015-09-26T05:14:34.913Z'
145 | },
146 | {
147 | 'name': 'Kelli Schoen', 'email': 'Reva.Kiehn54@yahoo.com', 'jobTitle': 'National Accountability Architect',
148 | 'active': false, 'phoneNumber': '654-591-6561', 'date': '2015-05-04T06:50:37.482Z'
149 | },
150 | {
151 | 'name': 'Kenna Fritsch', 'email': 'Wilburn2@gmail.com', 'jobTitle': 'Legacy Response Administrator',
152 | 'active': true, 'phoneNumber': '790-480-2859', 'date': '2015-10-10T23:37:05.867Z'
153 | },
154 | {
155 | 'name': 'Judge Marquardt', 'email': 'Letha_Champlin69@hotmail.com', 'jobTitle': 'Human Program Specialist',
156 | 'active': true, 'phoneNumber': '100-494-1787', 'date': '2015-04-04T23:29:48.588Z'
157 | },
158 | {
159 | 'name': 'Kurtis Hane', 'email': 'Mona.Gaylord47@yahoo.com', 'jobTitle': 'International Optimization Director',
160 | 'active': false, 'phoneNumber': '008-800-2959', 'date': '2014-12-04T21:09:50.722Z'
161 | },
162 | {
163 | 'name': 'Nicolette Lind', 'email': 'Thurman30@yahoo.com', 'jobTitle': 'Legacy Marketing Facilitator',
164 | 'active': true, 'phoneNumber': '007-908-2460', 'date': '2015-06-22T08:11:57.381Z'
165 | },
166 | {
167 | 'name': 'Idella Green', 'email': 'Fernando_Ward@yahoo.com', 'jobTitle': 'Dynamic Division Orchestrator',
168 | 'active': false, 'phoneNumber': '147-865-1578', 'date': '2015-02-12T23:00:31.283Z'
169 | },
170 | {
171 | 'name': 'Mackenzie Bartell', 'email': 'Price25@yahoo.com', 'jobTitle': 'National Directives Associate',
172 | 'active': false, 'phoneNumber': '235-649-0980', 'date': '2015-06-24T20:21:51.356Z'
173 | },
174 | {
175 | 'name': 'Mose Kohler', 'email': 'Malika56@hotmail.com', 'jobTitle': 'Lead Implementation Executive',
176 | 'active': true, 'phoneNumber': '614-886-4868', 'date': '2015-03-04T13:05:23.698Z'
177 | },
178 | {
179 | 'name': 'Cielo Kuphal', 'email': 'Jude_Terry24@gmail.com', 'jobTitle': 'Dynamic Division Analyst',
180 | 'active': false, 'phoneNumber': '590-976-7492', 'date': '2015-06-02T20:52:32.664Z'
181 | },
182 | {
183 | 'name': 'Haleigh Stokes', 'email': 'Belle_Herman64@yahoo.com', 'jobTitle': 'Global Intranet Executive',
184 | 'active': false, 'phoneNumber': '418-255-9365', 'date': '2015-04-10T00:32:10.283Z'
185 | },
186 | {
187 | 'name': 'Tyrese Walter', 'email': 'Garland.Veum52@hotmail.com', 'jobTitle': 'Senior Web Liason',
188 | 'active': false, 'phoneNumber': '041-555-9831', 'date': '2015-08-18T20:05:08.839Z'
189 | },
190 | {
191 | 'name': 'Barney Shields', 'email': 'Anika27@gmail.com', 'jobTitle': 'District Web Administrator',
192 | 'active': true, 'phoneNumber': '379-438-0217', 'date': '2015-06-01T09:28:46.778Z'
193 | },
194 | {
195 | 'name': 'Favian Abbott Miss', 'email': 'Palma_Little@hotmail.com', 'jobTitle': 'Lead Implementation Facilitator',
196 | 'active': false, 'phoneNumber': '642-808-5400', 'date': '2015-08-09T07:38:06.588Z'
197 | },
198 | {
199 | 'name': 'Carissa Kunze', 'email': 'Merl_Frami@yahoo.com', 'jobTitle': 'Regional Division Technician',
200 | 'active': true, 'phoneNumber': '949-983-0342', 'date': '2015-11-05T08:09:09.463Z'
201 | },
202 | {
203 | 'name': 'Aaron 2Moore', 'email': 'Heath44@hotmail.com', 'jobTitle': 'Regional Configuration Producer',
204 | 'active': true, 'phoneNumber': '611-898-6201', 'date': '2015-11-06T07:21:25.510Z'
205 | },
206 | {
207 | 'name': 'Yvonne Conroy Mrs.', 'email': 'Gideon9@yahoo.com', 'jobTitle': 'Global Mobility Orchestrator',
208 | 'active': false, 'phoneNumber': '115-850-0969', 'date': '2014-12-20T00:48:40.276Z'
209 | },
210 | {
211 | 'name': 'Laron Padberg', 'email': 'Laney_Huels@hotmail.com', 'jobTitle': 'Senior Directives Supervisor',
212 | 'active': false, 'phoneNumber': '632-654-3034', 'date': '2015-09-29T04:33:38.544Z'
213 | },
214 | {
215 | 'name': 'Dr. Maryam Spinka', 'email': 'Aletha.Labadie@hotmail.com', 'jobTitle': 'Dynamic Mobility Associate',
216 | 'active': true, 'phoneNumber': '547-345-0067', 'date': '2015-09-23T01:13:39.320Z'
217 | },
218 | {
219 | 'name': 'Kiley Baumbach', 'email': 'Rogelio24@hotmail.com', 'jobTitle': 'Principal Metrics Orchestrator',
220 | 'active': true, 'phoneNumber': '958-524-5164', 'date': '2014-12-05T23:39:27.340Z'
221 | },
222 | {
223 | 'name': 'Hollis MacGyver', 'email': 'Yazmin.Heidenreich97@gmail.com', 'jobTitle': 'Direct Markets Assistant',
224 | 'active': true, 'phoneNumber': '603-607-3241', 'date': '2015-02-12T10:40:52.977Z'
225 | },
226 | {
227 | 'name': 'Axel McLaughlin', 'email': 'Deon_Heaney@gmail.com', 'jobTitle': 'Forward Mobility Architect',
228 | 'active': false, 'phoneNumber': '983-639-0705', 'date': '2015-03-01T02:28:26.030Z'
229 | },
230 | {
231 | 'name': 'Ricardo Botsford', 'email': 'Melisa73@yahoo.com', 'jobTitle': 'Direct Quality Consultant',
232 | 'active': true, 'phoneNumber': '408-082-9480', 'date': '2015-01-31T03:41:54.611Z'
233 | },
234 | {
235 | 'name': 'Corbin Funk Mrs.', 'email': 'Marjory.Morissette51@gmail.com', 'jobTitle': 'Human Configuration Manager',
236 | 'active': true, 'phoneNumber': '386-937-8683', 'date': '2014-12-05T15:07:36.843Z'
237 | },
238 | {
239 | 'name': 'Rosalind Paucek', 'email': 'Ivy_Stanton@gmail.com', 'jobTitle': 'Future Creative Supervisor',
240 | 'active': true, 'phoneNumber': '977-661-7403', 'date': '2015-06-10T17:42:38.644Z'
241 | },
242 | {
243 | 'name': 'Henderson Moore', 'email': 'Randi_Corkery@hotmail.com', 'jobTitle': 'Internal Accountability Director',
244 | 'active': true, 'phoneNumber': '078-101-6377', 'date': '2015-09-26T05:14:34.913Z'
245 | },
246 | {
247 | 'name': 'Kelli Schoen', 'email': 'Reva.Kiehn54@yahoo.com', 'jobTitle': 'National Accountability Architect',
248 | 'active': false, 'phoneNumber': '654-591-6561', 'date': '2015-05-04T06:50:37.482Z'
249 | },
250 | {
251 | 'name': 'Kenna Fritsch', 'email': 'Wilburn2@gmail.com', 'jobTitle': 'Legacy Response Administrator',
252 | 'active': true, 'phoneNumber': '790-480-2859', 'date': '2015-10-10T23:37:05.867Z'
253 | },
254 | {
255 | 'name': 'Judge Marquardt', 'email': 'Letha_Champlin69@hotmail.com', 'jobTitle': 'Human Program Specialist',
256 | 'active': true, 'phoneNumber': '100-494-1787', 'date': '2015-04-04T23:29:48.588Z'
257 | },
258 | {
259 | 'name': 'Kurtis Hane', 'email': 'Mona.Gaylord47@yahoo.com', 'jobTitle': 'International Optimization Director',
260 | 'active': false, 'phoneNumber': '008-800-2959', 'date': '2014-12-04T21:09:50.722Z'
261 | },
262 | {
263 | 'name': 'Nicolette Lind', 'email': 'Thurman30@yahoo.com', 'jobTitle': 'Legacy Marketing Facilitator',
264 | 'active': true, 'phoneNumber': '007-908-2460', 'date': '2015-06-22T08:11:57.381Z'
265 | },
266 | {
267 | 'name': 'Idella Green', 'email': 'Fernando_Ward@yahoo.com', 'jobTitle': 'Dynamic Division Orchestrator',
268 | 'active': false, 'phoneNumber': '147-865-1578', 'date': '2015-02-12T23:00:31.283Z'
269 | },
270 | {
271 | 'name': 'Mackenzie Bartell', 'email': 'Price25@yahoo.com', 'jobTitle': 'National Directives Associate',
272 | 'active': false, 'phoneNumber': '235-649-0980', 'date': '2015-06-24T20:21:51.356Z'
273 | },
274 | {
275 | 'name': 'Mose Kohler', 'email': 'Malika56@hotmail.com', 'jobTitle': 'Lead Implementation Executive',
276 | 'active': true, 'phoneNumber': '614-886-4868', 'date': '2015-03-04T13:05:23.698Z'
277 | },
278 | {
279 | 'name': 'Cielo Kuphal', 'email': 'Jude_Terry24@gmail.com', 'jobTitle': 'Dynamic Division Analyst',
280 | 'active': false, 'phoneNumber': '590-976-7492', 'date': '2015-06-02T20:52:32.664Z'
281 | },
282 | {
283 | 'name': 'Haleigh Stokes', 'email': 'Belle_Herman64@yahoo.com', 'jobTitle': 'Global Intranet Executive',
284 | 'active': false, 'phoneNumber': '418-255-9365', 'date': '2015-04-10T00:32:10.283Z'
285 | },
286 | {
287 | 'name': 'Tyrese Walter', 'email': 'Garland.Veum52@hotmail.com', 'jobTitle': 'Senior Web Liason',
288 | 'active': false, 'phoneNumber': '041-555-9831', 'date': '2015-08-18T20:05:08.839Z'
289 | },
290 | {
291 | 'name': 'Barney Shields', 'email': 'Anika27@gmail.com', 'jobTitle': 'District Web Administrator',
292 | 'active': true, 'phoneNumber': '379-438-0217', 'date': '2015-06-01T09:28:46.778Z'
293 | },
294 | {
295 | 'name': 'Favian Abbott Miss', 'email': 'Palma_Little@hotmail.com', 'jobTitle': 'Lead Implementation Facilitator',
296 | 'active': false, 'phoneNumber': '642-808-5400', 'date': '2015-08-09T07:38:06.588Z'
297 | },
298 | {
299 | 'name': 'Carissa Kunze', 'email': 'Merl_Frami@yahoo.com', 'jobTitle': 'Regional Division Technician',
300 | 'active': true, 'phoneNumber': '949-983-0342', 'date': '2015-11-05T08:09:09.463Z'
301 | },
302 | {
303 | 'name': 'Favian Abbott Miss', 'email': 'Palma_Little@hotmail.com', 'jobTitle': 'Lead Implementation Facilitator',
304 | 'active': false, 'phoneNumber': '642-808-5400', 'date': '2015-08-09T07:38:06.588Z'
305 | },
306 | {
307 | 'name': 'Carissa Kunze', 'email': 'Merl_Frami@yahoo.com', 'jobTitle': 'Regional Division Technician',
308 | 'active': true, 'phoneNumber': '949-983-0342', 'date': '2015-11-05T08:09:09.463Z'
309 | },
310 | {
311 | 'name': 'Favian Abbott Miss', 'email': 'Palma_Little@hotmail.com', 'jobTitle': 'Lead Implementation Facilitator',
312 | 'active': false, 'phoneNumber': '642-808-5400', 'date': '2015-08-09T07:38:06.588Z'
313 | },
314 | {
315 | 'name': 'Carissa Kunze', 'email': 'Merl_Frami@yahoo.com', 'jobTitle': 'Regional Division Technician',
316 | 'active': true, 'phoneNumber': '949-983-0342', 'date': '2015-11-05T08:09:09.463Z'
317 | }
318 | ];
319 |
--------------------------------------------------------------------------------