diff --git a/package.json b/package.json index a42d655546..875623c756 100644 --- a/package.json +++ b/package.json @@ -53,14 +53,16 @@ "@angular/core": "^2.0.0", "@angular/forms": "^2.0.0", "@angular/http": "^2.0.0", + "@angular/material": "2.0.0-alpha.9-3", "@angular/platform-browser": "^2.0.0", "@angular/platform-browser-dynamic": "^2.0.0", "@angular/platform-server": "^2.0.0", "@angular/router": "^3.0.0", - "@angular/material": "2.0.0-alpha.9-3", + "@types/lodash": "^4.14.36", "core-js": "^2.4.1", "hammerjs": "^2.0.8", "highlight.js": "9.6.0", + "lodash": "^4.16.3", "rxjs": "5.0.0-beta.12", "showdown": "1.4.2", "zone.js": "0.6.21" diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6579ffe005..0505324e12 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,6 +16,7 @@ import { CovalentHttpModule } from '../platform/http'; import { CovalentMarkdownModule } from '../platform/markdown'; import { CovalentJsonFormatterModule } from '../platform/json-formatter'; import { CovalentChipsModule } from '../platform/chips'; +import { CovalentDataTableModule } from '../platform/data-table'; @NgModule({ declarations: [ @@ -35,6 +36,7 @@ import { CovalentChipsModule } from '../platform/chips'; CovalentMarkdownModule.forRoot(), CovalentJsonFormatterModule.forRoot(), CovalentChipsModule.forRoot(), + CovalentDataTableModule.forRoot(), appRoutes, ], // modules needed to run this module providers: [ diff --git a/src/app/components/components/components.component.ts b/src/app/components/components/components.component.ts index f14901768a..09508e57a9 100644 --- a/src/app/components/components/components.component.ts +++ b/src/app/components/components/components.component.ts @@ -42,6 +42,11 @@ export class ComponentsComponent { icon: 'open_in_browser', route: 'dialogs', title: 'Simple Dialogs', + }, { + description: 'Data Table', + icon: 'grid_on', + route: 'data-table', + title: 'Data Table', }, { description: 'Highlighting your code snippets', icon: 'code', diff --git a/src/app/components/components/components.module.ts b/src/app/components/components/components.module.ts index 748ce717ae..66412f4e68 100644 --- a/src/app/components/components/components.module.ts +++ b/src/app/components/components/components.module.ts @@ -17,6 +17,7 @@ import { ChipsDemoComponent } from './chips/chips.component'; import { DialogsDemoComponent } from './dialogs/dialogs.component'; import { DirectivesComponent } from './directives/directives.component'; import { PipesComponent } from './pipes/pipes.component'; +import { DataTableDemoComponent } from './data-table/data-table.component'; import { CovalentCoreModule } from '../../../platform/core'; import { CovalentFileModule } from '../../../platform/file-upload'; @@ -24,6 +25,7 @@ import { CovalentHighlightModule } from '../../../platform/highlight'; import { CovalentMarkdownModule } from '../../../platform/markdown'; import { CovalentJsonFormatterModule } from '../../../platform/json-formatter'; import { CovalentChipsModule } from '../../../platform/chips'; +import { CovalentDataTableModule } from '../../../platform/data-table'; @NgModule({ declarations: [ @@ -42,6 +44,7 @@ import { CovalentChipsModule } from '../../../platform/chips'; DialogsDemoComponent, DirectivesComponent, PipesComponent, + DataTableDemoComponent, ], imports: [ CovalentCoreModule.forRoot(), @@ -50,6 +53,7 @@ import { CovalentChipsModule } from '../../../platform/chips'; CovalentMarkdownModule.forRoot(), CovalentJsonFormatterModule.forRoot(), CovalentChipsModule.forRoot(), + CovalentDataTableModule.forRoot(), componentsRoutes, ], }) diff --git a/src/app/components/components/components.routes.ts b/src/app/components/components/components.routes.ts index 7b64fbd594..a9971a59d2 100644 --- a/src/app/components/components/components.routes.ts +++ b/src/app/components/components/components.routes.ts @@ -15,6 +15,7 @@ import { ChipsDemoComponent } from './chips/chips.component'; import { DialogsDemoComponent } from './dialogs/dialogs.component'; import { DirectivesComponent } from './directives/directives.component'; import { PipesComponent } from './pipes/pipes.component'; +import { DataTableDemoComponent } from './data-table/data-table.component'; const routes: Routes = [{ children: [{ @@ -59,6 +60,9 @@ const routes: Routes = [{ }, { component: PipesComponent, path: 'pipes', + }, { + component: DataTableDemoComponent, + path: 'data-table', }, ], component: ComponentsComponent, diff --git a/src/app/components/components/data-table/data-table.component.html b/src/app/components/components/data-table/data-table.component.html new file mode 100644 index 0000000000..0c778f0e97 --- /dev/null +++ b/src/app/components/components/data-table/data-table.component.html @@ -0,0 +1,135 @@ + + Data Table + Data driven table layout with search, sorting and pagination + + +

Basic Demo

+ + + + +
+ + + + + + + + + + + + + + + + + + +
+ + TdDataTableComponent + How to use this component + + +

]]>

+

Use ]]> element to generate a table.

+

Pass an array of javascript objects with the information to be displayed on the table to the [data] attribute.

+

Properties:

+

The ]]> component has {{dataTableAttrs.length}} properties:

+ + + +

Example:

+

HTML:

+ + + + ]]> + +

Typescript:

+ + v.toFixed(2) }, + ]; + } + ]]> + +

Setup:

+

Import the [CovalentDataTableModule] using the forRoot() method in your NgModule:

+ + + +
+
diff --git a/src/app/components/components/data-table/data-table.component.scss b/src/app/components/components/data-table/data-table.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/components/components/data-table/data-table.component.ts b/src/app/components/components/data-table/data-table.component.ts new file mode 100644 index 0000000000..8838ebbb10 --- /dev/null +++ b/src/app/components/components/data-table/data-table.component.ts @@ -0,0 +1,232 @@ +import { Component } from '@angular/core'; + +import { TdDataTableSortingOrder } from '../../../../platform/data-table'; + +const NUMBER_FORMAT: any = (v: {value: number}) => v.value; +const DECIMAL_FORMAT: any = (v: {value: number}) => v.value.toFixed(2); + +@Component({ + selector: 'data-table-demo', + styleUrls: ['data-table.component.scss'], + templateUrl: 'data-table.component.html', +}) +export class DataTableDemoComponent { + + dataTableAttrs: Object[] = [{ + description: `Rows of data to be displayed`, + name: 'data', + type: 'any[]', + }, { + description: `List of columns to be displayed`, + name: 'columns?', + type: 'ITdDataTableColumn[]', + }, { + description: `If present will display a title before the table`, + name: 'title?', + type: 'string', + }, { + description: `Enables pagination`, + name: 'pagination?', + type: 'boolean', + }, { + description: `Number of rows per page, when omitted defaults to 10`, + name: 'pageSize?', + type: 'number', + }, { + description: `Enables sorting by column`, + name: 'sorting?', + type: 'boolean', + }, { + description: `Name of the column to use for sorting`, + name: 'sortBy?', + type: 'string', + }, { + description: `Sorting order - ascending or descending`, + name: 'sortOrder?', + type: `'ASC' | 'DESC'`, + }, { + description: `Enables search`, + name: 'search?', + type: `boolean`, + }, { + description: `Adds a checkbox column to allow user to select rows`, + name: 'rowSelection?', + type: 'boolean', + }, { + description: `Toggles between multiple or single row selection`, + name: 'multiple?', + type: 'boolean', + }]; + + columns: any[] = [ + { name: 'name', label: 'Dessert (100g serving)' }, + { name: 'type', label: 'Type' }, + { name: 'calories', label: 'Calories', numeric: true, format: NUMBER_FORMAT }, + { name: 'fat', label: 'Fat (g)', numeric: true, format: DECIMAL_FORMAT }, + { name: 'carbs', label: 'Carbs (g)', numeric: true, format: NUMBER_FORMAT }, + { name: 'protein', label: 'Protein (g)', numeric: true, format: DECIMAL_FORMAT }, + { name: 'sodium', label: 'Sodium (mg)', numeric: true, format: NUMBER_FORMAT }, + { name: 'calcium', label: 'Calcium (%)', numeric: true, format: NUMBER_FORMAT }, + { name: 'iron', label: 'Iron (%)', numeric: true, format: NUMBER_FORMAT }, + ]; + + sorting: boolean = true; + pagination: boolean = true; + pageSize: number = 5; + + data: any[] = [ + { + 'name': 'Frozen yogurt', + 'type': 'Ice cream', + 'calories': { 'value': 159.0 }, + 'fat': { 'value': 6.0 }, + 'carbs': { 'value': 24.0 }, + 'protein': { 'value': 4.0 }, + 'sodium': { 'value': 87.0 }, + 'calcium': { 'value': 14.0 }, + 'iron': { 'value': 1.0 }, + }, { + 'name': 'Ice cream sandwich', + 'type': 'Ice cream', + 'calories': { 'value': 237.0 }, + 'fat': { 'value': 9.0 }, + 'carbs': { 'value': 37.0 }, + 'protein': { 'value': 4.3 }, + 'sodium': { 'value': 129.0 }, + 'calcium': { 'value': 8.0 }, + 'iron': { 'value': 1.0 }, + }, { + 'name': 'Eclair', + 'type': 'Pastry', + 'calories': { 'value': 262.0 }, + 'fat': { 'value': 16.0 }, + 'carbs': { 'value': 24.0 }, + 'protein': { 'value': 6.0 }, + 'sodium': { 'value': 337.0 }, + 'calcium': { 'value': 6.0 }, + 'iron': { 'value': 7.0 }, + }, { + 'name': 'Cupcake', + 'type': 'Pastry', + 'calories': { 'value': 305.0 }, + 'fat': { 'value': 3.7 }, + 'carbs': { 'value': 67.0 }, + 'protein': { 'value': 4.3 }, + 'sodium': { 'value': 413.0 }, + 'calcium': { 'value': 3.0 }, + 'iron': { 'value': 8.0 }, + }, { + 'name': 'Jelly bean', + 'type': 'Candy', + 'calories': { 'value': 375.0 }, + 'fat': { 'value': 0.0 }, + 'carbs': { 'value': 94.0 }, + 'protein': { 'value': 0.0 }, + 'sodium': { 'value': 50.0 }, + 'calcium': { 'value': 0.0 }, + 'iron': { 'value': 0.0 }, + }, { + 'name': 'Lollipop', + 'type': 'Candy', + 'calories': { 'value': 392.0 }, + 'fat': { 'value': 0.2 }, + 'carbs': { 'value': 98.0 }, + 'protein': { 'value': 0.0 }, + 'sodium': { 'value': 38.0 }, + 'calcium': { 'value': 0.0 }, + 'iron': { 'value': 2.0 }, + }, { + 'name': 'Honeycomb', + 'type': 'Other', + 'calories': { 'value': 408.0 }, + 'fat': { 'value': 3.2 }, + 'carbs': { 'value': 87.0 }, + 'protein': { 'value': 6.5 }, + 'sodium': { 'value': 562.0 }, + 'calcium': { 'value': 0.0 }, + 'iron': { 'value': 45.0 }, + }, { + 'name': 'Donut', + 'type': 'Pastry', + 'calories': { 'value': 452.0 }, + 'fat': { 'value': 25.0 }, + 'carbs': { 'value': 51.0 }, + 'protein': { 'value': 4.9 }, + 'sodium': { 'value': 326.0 }, + 'calcium': { 'value': 2.0 }, + 'iron': { 'value': 22.0 }, + }, { + 'name': 'KitKat', + 'type': 'Candy', + 'calories': { 'value': 518.0 }, + 'fat': { 'value': 26.0 }, + 'carbs': { 'value': 65.0 }, + 'protein': { 'value': 7.0 }, + 'sodium': { 'value': 54.0 }, + 'calcium': { 'value': 12.0 }, + 'iron': { 'value': 6.0 }, + }, + ]; + + sortBy: string = 'name'; + sortOrder: string = 'ASC'; + + rowSelection: boolean = false; + multiple: boolean = true; + + toggleRowSelection(): void { + this.rowSelection = !this.rowSelection; + } + + toggleRowSelectionMultiple(): void { + this.multiple = !this.multiple; + } + + toggleSorting(): void { + this.sorting = !this.sorting; + } + + toggleSortBy(): void { + const columns: any[] = this.columns.map((c: any) => c.name); + const idx: number = columns.indexOf(this.sortBy); + if (idx < columns.length - 1) { + this.sortBy = columns[idx + 1]; + } else { + this.sortBy = columns[0]; + } + } + + toggleSortOrder(): void { + this.sortOrder = this.sortOrder === 'ASC' ? 'DESC' : 'ASC'; + } + + areTooltipsOn(): boolean { + return this.columns[0].hasOwnProperty('tooltip'); + } + + toggleTooltips(): void { + if (this.columns[0].tooltip) { + this.columns.forEach((c: any) => delete c.tooltip); + } else { + this.columns.forEach((c: any) => c.tooltip = `This is ${c.label}!`); + } + } + + togglePagination(): void { + this.pagination = !this.pagination; + } + + togglePageSize(): void { + this.pageSize += 5; + if (this.pageSize > 15) { + this.pageSize = 5; + } + } + + sortChanged(changes: any): void { + const { column, order }: any = changes; + + this.sortBy = column.name; + this.sortOrder = order === TdDataTableSortingOrder.Ascending ? 'ASC' : 'DESC'; + } +} diff --git a/src/app/components/components/overview/overview.component.ts b/src/app/components/components/overview/overview.component.ts index 5704b67aed..dd169d7f05 100644 --- a/src/app/components/components/overview/overview.component.ts +++ b/src/app/components/components/overview/overview.component.ts @@ -36,6 +36,11 @@ export class ComponentsOverviewComponent { icon: 'open_in_browser', route: 'dialogs', title: 'Simple Dialogs', + }, { + color: 'green-700', + icon: 'grid_on', + route: 'data-table', + title: 'Data Table', }, { color: 'pink-700', icon: 'code', diff --git a/src/platform/data-table/README.md b/src/platform/data-table/README.md new file mode 100644 index 0000000000..91798afa37 --- /dev/null +++ b/src/platform/data-table/README.md @@ -0,0 +1,53 @@ +# td-data-table + +`td-data-table` element generates a data driven table layout with search, sorting and pagination. + +## API Summary + +Properties: + +| Name | Type | Description | +| --- | --- | --- | +| `data` | `any[]` | Rows of data to be displayed +| `columns` | `ITdDataTableColumn[]` | List of columns to be displayed +| `title?` | `string` | If present will display a title before the table +| `pagination?` | `boolean` | Enables pagination +| `pageSize?` | `number` | Number of rows per page, when omitted defaults to 10 +| `sorting?` | `boolean` | Enables sorting by column +| `sortBy?` | `string` | Name of the column to use for sorting +| `sortOrder?` | `'ASC' | 'DESC'` | Sorting order - ascending or descending +| `search?` | `boolean` | Enables search +| `rowSelection?` | `boolean` | Adds a checkbox column to allow user to select rows +| `multiple?` | `boolean` | Toggles between multiple or single row selection + +## Setup + +Import the [CovalentDataTableModule] using the forRoot() method in your NgModule: + +```typescript +import { CovalentDataTableModule } from '@covalent/chips'; +@NgModule({ + imports: [ + CovalentDataTableModule.forRoot(), + ... + ], + ... +}) +export class MyModule {} +``` + +## Usage + +Example for HTML usage: + + ```html + + + ``` diff --git a/src/platform/data-table/_data-table-theme.scss b/src/platform/data-table/_data-table-theme.scss new file mode 100644 index 0000000000..b800380c84 --- /dev/null +++ b/src/platform/data-table/_data-table-theme.scss @@ -0,0 +1,62 @@ +@import '~@angular/material/core/theming/palette'; +@import '~@angular/material/core/theming/theming'; + +// TODO properly wrap in mixin to apply theme like material2 components +$foreground: $md-light-theme-foreground; +$background: $md-light-theme-background; +$accent: md-palette($md-light-blue, 50); + +// Table +.md-row { + border-bottom-color: md-color($foreground, divider); +} +.md-checkbox-cell, +.md-checkbox-column { + color: md-color($foreground, secondary-text); +} +.md-column { + color: md-color($foreground, secondary-text); + md-icon { + &.md-sort-icon { + color: md-color($foreground, disabled); + } + } + &.md-active, + &.md-active md-icon { + color: md-color($foreground, base); + } +} +&.md-row-select tbody.md-body > tr.md-row { + &:not([disabled]):hover { + background-color: md-color($background, 'hover'); + } + &.md-selected { + background-color: md-color($accent, 0.12); + } +} + +// Pagination +.md-table-pagination { + color: md-color($foreground, secondary-text); + + md-select { + &:not([disabled]):focus .md-select-value { + color: md-color($foreground, secondary-text); + } + } +} + +// Dialog +md-edit-dialog { + background-color: md-color($background, dialog); + > .md-content { + .md-title { + color: md-color($foreground, text); + } + md-input-container { + .md-errors-spacer { + color: md-color($foreground, secondary-text); + } + } + } +} \ No newline at end of file diff --git a/src/platform/data-table/data-table.component.html b/src/platform/data-table/data-table.component.html new file mode 100644 index 0000000000..7ea7842934 --- /dev/null +++ b/src/platform/data-table/data-table.component.html @@ -0,0 +1,100 @@ +
+ {{_title}} + + +
+ +
+ search +
+ + +
+ +
+ +
+ + + + + + + + + + + + + +
+ + + + + arrow_upward + + + {{column.label}} + {{column.label}} + + + arrow_upward + +
+ + + + {{ row[column.name] }} +
+
+ +
+ No results +
+ +
+ + {{ _currentPage }} of {{ _totalPages }} + + + +
+ +
+ No pages +
\ No newline at end of file diff --git a/src/platform/data-table/data-table.component.scss b/src/platform/data-table/data-table.component.scss new file mode 100644 index 0000000000..4a97a63a4c --- /dev/null +++ b/src/platform/data-table/data-table.component.scss @@ -0,0 +1,364 @@ +@import 'data-table-theme'; + +$backdrop-index: 80; +$checkbox-size: 18px; + +.md-table-container { + display: block; + max-width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +table.md-table { + width: 100%; + border-spacing: 0; + overflow: hidden; + border-collapse: collapse; + + tr.md-row { + border-bottom-style: solid; + border-bottom-width: 1px; + } + + thead.md-head > tr.md-row { + height: 56px; + } + + tbody.md-body > tr.md-row, + tfoot.md-foot > tr.md-row { + height: 48px; + } + + thead.md-head + .md-table-progress md-progress-linear { + top: -3px; + } + + .md-table-progress th { + padding: 0; + + md-progress-linear { + height: 0; + transition: opacity 1s; + + &.ng-hide { + opacity: 0; + } + + > .md-container { + height: 3px; + top: 0; + transition: none; + + > .md-bar { + height: 3px; + } + } + } + } + + th.md-column { + //color: $md-dark-secondary; + font-size: 12px; + font-weight: bold; + white-space: nowrap; + + &.md-sort { + cursor: pointer; + } + + md-icon { + height: 16px; + width: 16px; + font-size: 16px !important; + line-height: 16px !important; + + &.md-sort-icon { + opacity: 0; + transition: transform 0.25s, opacity 0.25s; + + &.md-asc { + transform: rotate(0deg); + } + + &.md-desc { + transform: rotate(180deg); + } + } + + &:not(:first-child) { + margin-left: 8px; + } + + &:not(:last-child) { + margin-right: 8px; + } + } + + &:hover md-icon.md-sort-icon, + &.md-active md-icon.md-sort-icon { + opacity: 1; + } + } + + tr.md-row { + &[ng\:repeat].ng-leave, + &[ng-repeat].ng-leave, + &[x-ng-repeat].ng-leave, + &[data-ng-repeat].ng-leave { + display: none; + } + } + + &.md-row-select tbody.md-body > tr.md-row { + transition: background-color 0.2s; + } + + &.md-row-select td.md-cell, + &.md-row-select th.md-column { + &:first-child { + width: 20px; + padding: 0 0 0 24px; + } + + &:nth-child(2) { + padding: 0 24px; + } + + &:nth-child(n+3):nth-last-child(n+2) { + padding: 0 56px 0 0; + } + } + + &:not(.md-row-select) td.md-cell, + &:not(.md-row-select) th.md-column { + &:first-child { + padding: 0 24px; + } + + &:nth-child(n+2):nth-last-child(n+2) { + padding: 0 56px 0 0; + } + } + + td.md-cell, + th.md-column { + vertical-align: middle; + text-align: left; + + > * { + vertical-align: middle; + } + + &:last-child { + padding: 0 24px 0 0; + } + + &.md-clickable { + cursor: pointer; + + &:focus { + outline: none; + } + } + + &.md-numeric { + text-align: right; + } + } + + td.md-checkbox-cell, + th.md-checkbox-column { + width: $checkbox-size; + font-size: 0 !important; + md-checkbox { + /deep/ .md-checkbox-inner-container { + width: $checkbox-size; + height: $checkbox-size; + margin: 0; + } + } + } + + td.md-cell { + font-size: 13px; + //border-top: 1px $md-dark-divider solid; + + &.md-numeric md-select { + justify-content: flex-end; + + .md-select-value { + flex: 0 0 auto; + } + } + + // &.md-placeholder { + //color: $md-dark-disabled; + // } + + md-select > .md-select-value > span.md-select-icon { + justify-content: flex-end; + //color: $md-dark-secondary; + width: 18px; + text-align: right; + + &:after { + transform: scaleY(0.4) scaleX(0.8); + } + } + } +} + +// Table in a card +md-card { + > md-toolbar.md-table-toolbar:first-child, + > md-table-container:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } + + > md-toolbar.md-table-toolbar:last-child, + > md-table-container:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } +} + +// Pagination +.md-table-pagination { + + md-select { + justify-content: flex-end; + min-width: 64px; + + .md-select-value { + flex: 0 0 auto; + + span.md-select-icon { + justify-content: center; + text-align: center; + margin-right: -6px !important; + + &:after { + top: initial; + transform: scaleY(0.5) scaleX(1); + } + } + } + } +} + +// Select +md-select.md-table-select { + margin: 0; + + > .md-select-value { + padding: 0; + min-width: 0; + min-height: 24px; + border-bottom: 0 !important; + + > span { + display: block; + height: auto; + transform: none !important; + + > .md-text { + display: inherit; + height: inherit; + transform: inherit; + } + + &.md-select-icon { + display: flex; + align-items: center; + height: 24px; + margin: 0; + + &:after { + top: initial; + } + } + } + } +} + +.md-select-menu-container.md-table-select, +.md-select-menu-container.md-pagination-select { + margin-left: -2px; + border-radius: 2px; + + md-select-menu, + md-content { + border-radius: inherit; + } + + md-content { + padding: 0; + } +} + +.md-select-menu-container.md-table-select .md-text { + font-size: 13px; +} + +.md-select-menu-container.md-pagination-select .md-text { + font-size: 12px; +} + +// Dialog +md-backdrop.md-edit-dialog-backdrop { + z-index: $backdrop-index; +} + +md-edit-dialog { + display: flex; + flex-direction: column; + position: fixed; + z-index: $backdrop-index + 1; + border-radius: 2px; + cursor: default; + + > .md-content { + padding: 16px 24px 0; + + .md-title { + margin-bottom: 8px; + } + + md-input-container { + margin: 0; + font-size: 13px; + + input { + float: none; + } + + .md-errors-spacer { + min-height: auto; + min-width: auto; + + .md-char-counter { + padding: 5px 2px 5px 0; + } + } + + [ng-message] { + padding: 5px 0 5px 2px; + } + } + } + + > .md-actions { + margin: 0 16px 8px; + + .md-button { + margin: 0; + min-width: initial; + + & + .md-button { + margin-left: 8px; + } + } + } +} \ No newline at end of file diff --git a/src/platform/data-table/data-table.component.ts b/src/platform/data-table/data-table.component.ts new file mode 100644 index 0000000000..d248e6d3de --- /dev/null +++ b/src/platform/data-table/data-table.component.ts @@ -0,0 +1,330 @@ +import { Component, OnInit, AfterViewInit, Input, Output, + EventEmitter, ViewChildren, QueryList, Renderer } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { MdInput } from '@angular/material'; +import 'rxjs/add/operator/debounceTime'; + +import * as _ from 'lodash'; + +export enum TdDataTableSortingOrder { + Ascending, Descending +}; + +export interface ITdDataTableColumn { + name: string; + label: string; + tooltip?: string; + numeric?: boolean; + format?: { (value: any): any }; +}; + +export interface ITdDataTableSortEvent { + column: ITdDataTableColumn; + order: TdDataTableSortingOrder; +}; + +@Component({ + selector: 'td-data-table', + styleUrls: [ 'data-table.component.scss' ], + templateUrl: 'data-table.component.html', +}) +export class TdDataTableComponent implements OnInit, AfterViewInit { + + /** internal attributes */ + private _data: any[]; + private _visibleData: any[]; + private _columns: ITdDataTableColumn[]; + private _rowSelection: boolean; + private _rowSelectionField: string = 'selected'; + private _multiple: boolean = true; + private _search: boolean = false; + private _hasData: boolean = false; + private _initialized: boolean = false; + + /** pagination */ + private _pageSize: number = 10; + private _currentPage: number = 0; + private _totalPages: number = 0; + private _pagination: boolean = false; + + /** sorting */ + private _sorting: boolean = false; + private _sortBy: ITdDataTableColumn; + private _sortOrder: TdDataTableSortingOrder = TdDataTableSortingOrder.Ascending; + + /** search by term */ + private _searchVisible: boolean = false; + private _searchTerm: string = ''; + private _searchTermControl: FormControl = new FormControl(); + + @ViewChildren(MdInput) _searchTermInput: QueryList; + + /** events */ + @Output() sortChanged: EventEmitter = new EventEmitter(); + + /** td-data-table element attributes */ + @Input('title') _title: string; + + @Input('data') + set data(data: Object[]) { + this._data = data; + this.resetPagination(); + } + + @Input('pageSize') + set pageSize(size: string | number) { + if (typeof size === 'string') { + this._pageSize = parseInt(size, 10); + } else { + this._pageSize = size; + } + this._pagination = true; + this.resetPagination(); + } + + @Input('rowSelectionField') + set rowSelectionField(field: string) { + this._rowSelectionField = field; + } + + @Input('rowSelection') + set rowSelection(selection: string | boolean) { + const settingAsString: boolean = typeof selection === 'string' + && selection !== 'true' && selection !== 'false'; + + if (settingAsString) { + this._rowSelection = true; + this._rowSelectionField = '' + selection; + } else { + this._rowSelection = selection !== '' ? (selection === 'true' || selection === true) : true; + } + } + + @Input('multiple') + set multiple(multiple: string | boolean) { + this._multiple = multiple !== '' ? (multiple === 'true' || multiple === true) : true; + } + + @Input('search') + set search(search: string | boolean) { + this._search = search !== '' ? (search === 'true' || search === true) : true; + } + + @Input('columns') + set columns(cols: ITdDataTableColumn[]) { + this._columns = cols; + } + get columns(): ITdDataTableColumn[] { + if (this._columns) { + return this._columns; + } + + if (!this._data) { + return []; + } + + this._columns = []; + this._data.forEach((row: any) => { + Object.keys(row).forEach((k: string) => { + if (!this._columns.find((c: any) => c.name === k)) { + this._columns.push({ name: k, label: k }); + } + }); + }); + + return this._columns; + } + + @Input('sorting') + set sorting(sorting: string | boolean) { + this._sorting = sorting !== '' ? (sorting === 'true' || sorting === true) : true; + } + + @Input('sortBy') + set sortBy(columnName: string) { + if (!columnName) { + return; + } + const column: ITdDataTableColumn = this.columns.find((c: any) => c.name === columnName); + if (!column) { + throw '[sortBy] must be a valid column name'; + } + + this._sortBy = column; + this._sorting = true; + this.filterData(); + } + + @Input('sortOrder') + set sortOrder(order: 'ASC' | 'DESC') { + let sortOrder: string = order ? order.toUpperCase() : 'ASC'; + if (sortOrder !== 'DESC' && sortOrder !== 'ASC') { + throw '[sortOrder] must be empty, ASC or DESC'; + } + + this._sortOrder = sortOrder === 'ASC' ? + TdDataTableSortingOrder.Ascending : TdDataTableSortingOrder.Descending; + this.filterData(); + } + + @Input('pagination') + set pagination(pagination: string | boolean) { + this._pagination = pagination !== '' ? (pagination === 'true' || pagination === true) : true; + this.resetPagination(); + } + + get hasData(): boolean { + return this._hasData; + } + + constructor(private renderer: Renderer) {} + + ngOnInit(): void { + this._searchTermControl.valueChanges + .debounceTime(250) + .subscribe(this.searchTermChanged.bind(this)); + } + + ngAfterViewInit(): void { + this.preprocessData(); + this._initialized = true; + this.filterData(); + } + + areAllSelected(): boolean { + const match: string = + this._data.find((d: any) => !d[this._rowSelectionField]); + return typeof match === 'undefined'; + } + + selectAll(checked: boolean): void { + this._data.forEach((d: any) => d[this._rowSelectionField] = checked); + } + + select(row: any, checked: boolean, event: Event): void { + event.preventDefault(); + // clears all the fields for the dataset + if (!this._multiple) { + this.selectAll(false); + } + + // toggles the selection field + row[this._rowSelectionField] = checked; + } + + toggleSearch(): void { + this._searchVisible = !this._searchVisible; + + if (this._searchVisible) { + setTimeout(this.focusOnSearch.bind(this)); + } else { + setTimeout(this.clearSearch.bind(this)); + } + } + + focusOnSearch(): void { + this._searchTermInput.first.focus(); + } + + clearSearch(): void { + this._searchTermControl.setValue(''); + } + + setSorting(column: ITdDataTableColumn): void { + if (this._sortBy === column) { + this._sortOrder = this._sortOrder === TdDataTableSortingOrder.Ascending ? + TdDataTableSortingOrder.Descending : TdDataTableSortingOrder.Ascending; + } else { + this._sortBy = column; + this._sortOrder = TdDataTableSortingOrder.Ascending; + } + this.notifySortChanged(); + this.filterData(); + } + + notifySortChanged(): void { + if (!this._initialized) { + return; + } + this.sortChanged.next({ column: this._sortBy, order: this._sortOrder }); + } + + nextPage(): void { + if (this._currentPage < this._totalPages) { + this._currentPage = this._currentPage + 1; + this.filterData(); + } + } + + prevPage(): void { + this._currentPage = Math.max(this._currentPage - 1, 1); + this.filterData(); + } + + isAscending(): boolean { + return this._sortOrder === TdDataTableSortingOrder.Ascending; + } + + isDescending(): boolean { + return this._sortOrder === TdDataTableSortingOrder.Descending; + } + + private preprocessData(): void { + this._data = _.cloneDeep(this._data); + this._data = this._data.map((row: any) => { + this.columns.filter((c: any) => c.format).forEach((c: any) => { + row[c.name] = c.format(row[c.name]); + }); + return row; + }); + } + + private searchTermChanged(value: string): void { + this._searchTerm = value; + this.resetPagination(); + } + + private resetPagination(): void { + this._currentPage = 1; + this._totalPages = 0; + + this.filterData(); + } + + private filterData(): void { + this._visibleData = this._data; + + if (!this._initialized) { + return; + } + + let filter: string = this._searchTerm.toLowerCase(); + if (filter) { + this._visibleData = this._visibleData.filter((item: any) => { + const res: any = Object.keys(item).find((key: string) => { + const itemValue: string = ('' + item[key]).toLowerCase(); + return itemValue.indexOf(filter) > -1; + }); + return !(typeof res === 'undefined'); + }); + } + + if (this._sorting && this._sortBy) { + this._visibleData = _.sortBy(this._visibleData, this._sortBy.name); + if (this._sortOrder === TdDataTableSortingOrder.Descending) { + this._visibleData = _.reverse(this._visibleData); + } + } + + if (this._pagination) { + const pageStart: number = (this._currentPage - 1) * this._pageSize; + const pageEnd: number = Math.min(pageStart + this._pageSize, this._visibleData.length); + + this._totalPages = Math.ceil(this._visibleData.length / this._pageSize); + this._visibleData = this._visibleData.slice(pageStart, pageEnd); + } + + this._hasData = this._visibleData.length > 0; + } + +} diff --git a/src/platform/data-table/data-table.module.ts b/src/platform/data-table/data-table.module.ts new file mode 100644 index 0000000000..6676426bcc --- /dev/null +++ b/src/platform/data-table/data-table.module.ts @@ -0,0 +1,48 @@ +import { NgModule, ModuleWithProviders, Type } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +import { MaterialModule } from '@angular/material'; + +import { TdDataTableComponent } from './data-table.component'; + +import { TdDataTableTableDirective } from './directives/table.directive'; +import { TdDataTableHeadDirective } from './directives/head.directive'; +import { TdDataTableBodyDirective } from './directives/body.directive'; +import { TdDataTableRowDirective } from './directives/row.directive'; +import { TdDataTableColumnDirective } from './directives/column.directive'; +import { TdDataTableCellDirective } from './directives/cell.directive'; + +export const TD_DATA_TABLE_DIRECTIVES: Type[] = [ + TdDataTableComponent, + + TdDataTableTableDirective, + TdDataTableHeadDirective, + TdDataTableBodyDirective, + TdDataTableRowDirective, + TdDataTableColumnDirective, + TdDataTableCellDirective, +]; + +@NgModule({ + imports: [ + MaterialModule.forRoot(), + CommonModule, + FormsModule, + ReactiveFormsModule, + ], + declarations: [ + TD_DATA_TABLE_DIRECTIVES, + ], + exports: [ + TD_DATA_TABLE_DIRECTIVES, + ], +}) +export class CovalentDataTableModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: CovalentDataTableModule, + providers: [], + }; + } +} diff --git a/src/platform/data-table/directives/body.directive.ts b/src/platform/data-table/directives/body.directive.ts new file mode 100644 index 0000000000..4d586b97ca --- /dev/null +++ b/src/platform/data-table/directives/body.directive.ts @@ -0,0 +1,13 @@ +import { Directive, Renderer, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[td-body]', +}) +export class TdDataTableBodyDirective { + + private _class: string = 'md-body'; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer) { + this._renderer.setElementClass(this._elementRef.nativeElement, this._class, true); + } +} diff --git a/src/platform/data-table/directives/cell.directive.ts b/src/platform/data-table/directives/cell.directive.ts new file mode 100644 index 0000000000..8df8a7ecc0 --- /dev/null +++ b/src/platform/data-table/directives/cell.directive.ts @@ -0,0 +1,13 @@ +import { Directive, Renderer, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[td-cell]', +}) +export class TdDataTableCellDirective { + + private _class: string = 'md-cell'; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer) { + this._renderer.setElementClass(this._elementRef.nativeElement, this._class, true); + } +} diff --git a/src/platform/data-table/directives/column.directive.ts b/src/platform/data-table/directives/column.directive.ts new file mode 100644 index 0000000000..31e2672fa3 --- /dev/null +++ b/src/platform/data-table/directives/column.directive.ts @@ -0,0 +1,13 @@ +import { Directive, Renderer, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[td-column]', +}) +export class TdDataTableColumnDirective { + + private _class: string = 'md-column'; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer) { + this._renderer.setElementClass(this._elementRef.nativeElement, this._class, true); + } +} diff --git a/src/platform/data-table/directives/head.directive.ts b/src/platform/data-table/directives/head.directive.ts new file mode 100644 index 0000000000..b39343f878 --- /dev/null +++ b/src/platform/data-table/directives/head.directive.ts @@ -0,0 +1,13 @@ +import { Directive, Renderer, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[td-head]', +}) +export class TdDataTableHeadDirective { + + private _class: string = 'md-head'; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer) { + this._renderer.setElementClass(this._elementRef.nativeElement, this._class, true); + } +} diff --git a/src/platform/data-table/directives/row.directive.ts b/src/platform/data-table/directives/row.directive.ts new file mode 100644 index 0000000000..df55c3e117 --- /dev/null +++ b/src/platform/data-table/directives/row.directive.ts @@ -0,0 +1,13 @@ +import { Directive, Renderer, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[td-row]', +}) +export class TdDataTableRowDirective { + + private _class: string = 'md-row'; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer) { + this._renderer.setElementClass(this._elementRef.nativeElement, this._class, true); + } +} diff --git a/src/platform/data-table/directives/table.directive.ts b/src/platform/data-table/directives/table.directive.ts new file mode 100644 index 0000000000..9d572c3e81 --- /dev/null +++ b/src/platform/data-table/directives/table.directive.ts @@ -0,0 +1,13 @@ +import { Directive, Renderer, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[td-table]', +}) +export class TdDataTableTableDirective { + + private _class: string = 'md-table'; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer) { + this._renderer.setElementClass(this._elementRef.nativeElement, this._class, true); + } +} diff --git a/src/platform/data-table/index.ts b/src/platform/data-table/index.ts new file mode 100644 index 0000000000..85ed36f4b9 --- /dev/null +++ b/src/platform/data-table/index.ts @@ -0,0 +1,2 @@ +export { TdDataTableComponent, TdDataTableSortingOrder } from './data-table.component'; +export { CovalentDataTableModule } from './data-table.module'; diff --git a/src/platform/data-table/package.json b/src/platform/data-table/package.json new file mode 100644 index 0000000000..4cd52628b0 --- /dev/null +++ b/src/platform/data-table/package.json @@ -0,0 +1,38 @@ +{ + "name": "@covalent/data-table", + "version": "0.7.0", + "description": "Teradata UI Platform Data Table Module", + "keywords": [ + "angular", + "components", + "reusable", + "data-table" + ], + "scripts": { + }, + "engines": { + "node": ">4.4 < 5", + "npm": ">= 3" + }, + "repository": { + "type": "git", + "url": "https://github.com/teradata/covalent.git" + }, + "bugs": { + "url": "https://github.com/teradata/covalent/issues" + }, + "license": "MIT", + "author": "Teradata UX", + "contributors": [ + "Kyle Ledbetter ", + "Richa Vyas ", + "Ed Morales ", + "Jason Weaver ", + "Jeremy Wilken " + ], + "dependencies": { + "@covalent/core": "0.7.0", + "@types/lodash": "^4.14.36", + "lodash": "^4.16.2" + } +}