├── .gitignore ├── .npmignore ├── README.md ├── package.json ├── src ├── components │ ├── column.component.ts │ ├── header.component.ts │ ├── header.style.ts │ ├── header.template.ts │ ├── pagination.component.ts │ ├── pagination.style.ts │ ├── pagination.template.ts │ ├── row.component.ts │ ├── row.style.ts │ ├── row.template.ts │ ├── table.component.ts │ ├── table.style.ts │ ├── table.template.ts │ └── types.ts ├── index.ts ├── tools │ └── data-table-resource.ts └── utils │ ├── drag.ts │ ├── hide.ts │ ├── min.ts │ └── px.ts ├── tsconfig.json └── typings.json /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /tmp 3 | /node_modules 4 | /.idea 5 | /typings 6 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | /tmp 3 | /node_modules 4 | /.idea 5 | /typings -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Angular 2 Data Table 2 | ### ** Updated for Angular 5 [here](https://github.com/ggmod/angular-5-data-table). This repo will not be updated ** 3 | 4 | A simple Angular 2 data table, with built-in solutions for features including: 5 | 6 | * pagination 7 | * sorting 8 | * row selection (single/multi) 9 | * expandable rows 10 | * column resizing 11 | * selecting visible columns 12 | 13 | The component can be used not just with local data, but remote resources too: for example if the sorting and paging happen in the database. 14 | 15 | The templates use bootstrap CSS class names, so the component requires a bootstrap .css file to be present in the application using it. 16 | 17 | Check out the [demo](https://ggmod.github.io/angular-2-data-table-demo) and its [code](https://github.com/ggmod/angular-2-data-table-demo) for examples of how to use it. 18 | 19 | ## Installing: 20 | `npm install angular-2-data-table --save` 21 | 22 | 23 | #### Licensing 24 | MIT License 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-2-data-table", 3 | "version": "0.1.2", 4 | "description": "An Angular 2 data table, with pagination, sorting, expandable rows etc.", 5 | "keywords": [ 6 | "angular", 7 | "angular2", 8 | "Angular 2", 9 | "ng2", 10 | "datatable", 11 | "data-table", 12 | "data table", 13 | "pagination" 14 | ], 15 | "main": "dist/index.js", 16 | "typings": "dist/index.d.ts", 17 | "scripts": { 18 | "build": "rm -rf dist/* && tsc", 19 | "serve": "rm -rf dist/ && tsc -w", 20 | "prepublish": "npm run build" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git://git@github.com/ggmod/angular-2-data-table.git" 25 | }, 26 | "peerDependencies": { 27 | "@angular/core": "^2.0.0" 28 | }, 29 | "author": "ggmod ", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@angular/common": "^2.0.0", 33 | "@angular/core": "^2.0.0", 34 | "@angular/forms": "^2.0.0", 35 | "rxjs": "^5.0.0-beta.12", 36 | "typescript": "^2.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/column.component.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, ContentChild, OnInit } from '@angular/core'; 2 | import { DataTableRow } from './row.component'; 3 | import { CellCallback } from './types'; 4 | 5 | 6 | @Directive({ 7 | selector: 'data-table-column' 8 | }) 9 | export class DataTableColumn implements OnInit { 10 | 11 | // init: 12 | @Input() header: string; 13 | @Input() sortable = false; 14 | @Input() resizable = false; 15 | @Input() property: string; 16 | @Input() styleClass: string; 17 | @Input() cellColors: CellCallback; 18 | 19 | // init and state: 20 | @Input() width: number | string; 21 | @Input() visible = true; 22 | 23 | @ContentChild('dataTableCell') cellTemplate; 24 | @ContentChild('dataTableHeader') headerTemplate; 25 | 26 | getCellColor(row: DataTableRow, index: number) { 27 | if (this.cellColors !== undefined) { 28 | return (this.cellColors)(row.item, row, this, index); 29 | } 30 | } 31 | 32 | private styleClassObject = {}; // for [ngClass] 33 | 34 | ngOnInit() { 35 | this._initCellClass(); 36 | } 37 | 38 | private _initCellClass() { 39 | if (!this.styleClass && this.property) { 40 | if (/^[a-zA-Z0-9_]+$/.test(this.property)) { 41 | this.styleClass = 'column-' + this.property; 42 | } else { 43 | this.styleClass = 'column-' + this.property.replace(/[^a-zA-Z0-9_]/g, ''); 44 | } 45 | } 46 | 47 | if (this.styleClass != null) { 48 | this.styleClassObject = { 49 | [this.styleClass]: true 50 | }; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, forwardRef } from '@angular/core'; 2 | import { DataTable } from './table.component'; 3 | import { HEADER_TEMPLATE } from './header.template'; 4 | import { HEADER_STYLE } from "./header.style"; 5 | 6 | 7 | @Component({ 8 | selector: 'data-table-header', 9 | template: HEADER_TEMPLATE, 10 | styles: [HEADER_STYLE], 11 | host: { 12 | '(document:click)': '_closeSelector()' 13 | } 14 | }) 15 | export class DataTableHeader { 16 | 17 | columnSelectorOpen = false; 18 | 19 | _closeSelector() { 20 | this.columnSelectorOpen = false; 21 | } 22 | 23 | constructor(@Inject(forwardRef(() => DataTable)) public dataTable: DataTable) {} 24 | } 25 | -------------------------------------------------------------------------------- /src/components/header.style.ts: -------------------------------------------------------------------------------- 1 | export const HEADER_STYLE = ` 2 | .data-table-header { 3 | min-height: 25px; 4 | margin-bottom: 10px; 5 | } 6 | .title { 7 | display: inline-block; 8 | margin: 5px 0 0 5px; 9 | } 10 | .button-panel { 11 | float: right; 12 | } 13 | .button-panel button { 14 | outline: none !important; 15 | } 16 | 17 | .column-selector-wrapper { 18 | position: relative; 19 | } 20 | .column-selector-box { 21 | box-shadow: 0 0 10px lightgray; 22 | width: 150px; 23 | padding: 10px; 24 | position: absolute; 25 | right: 0; 26 | top: 1px; 27 | z-index: 1060; 28 | } 29 | .column-selector-box .checkbox { 30 | margin-bottom: 4px; 31 | } 32 | .column-selector-fixed-column { 33 | font-style: italic; 34 | } 35 | `; -------------------------------------------------------------------------------- /src/components/header.template.ts: -------------------------------------------------------------------------------- 1 | export const HEADER_TEMPLATE = ` 2 |
3 |

4 |
5 | 9 | 13 |
14 |
15 |
16 | 20 |
21 |
22 | 26 |
27 |
28 | 32 |
33 |
34 | 38 |
39 |
40 |
41 |
42 |
43 | `; -------------------------------------------------------------------------------- /src/components/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, forwardRef } from '@angular/core'; 2 | import { DataTable } from './table.component'; 3 | import { PAGINATION_TEMPLATE } from './pagination.template'; 4 | import { PAGINATION_STYLE } from "./pagination.style"; 5 | 6 | 7 | @Component({ 8 | selector: 'data-table-pagination', 9 | template: PAGINATION_TEMPLATE, 10 | styles: [PAGINATION_STYLE] 11 | }) 12 | export class DataTablePagination { 13 | 14 | constructor(@Inject(forwardRef(() => DataTable)) public dataTable: DataTable) {} 15 | 16 | pageBack() { 17 | this.dataTable.offset -= Math.min(this.dataTable.limit, this.dataTable.offset); 18 | } 19 | 20 | pageForward() { 21 | this.dataTable.offset += this.dataTable.limit; 22 | } 23 | 24 | pageFirst() { 25 | this.dataTable.offset = 0; 26 | } 27 | 28 | pageLast() { 29 | this.dataTable.offset = (this.maxPage - 1) * this.dataTable.limit; 30 | } 31 | 32 | get maxPage() { 33 | return Math.ceil(this.dataTable.itemCount / this.dataTable.limit); 34 | } 35 | 36 | get limit() { 37 | return this.dataTable.limit; 38 | } 39 | 40 | set limit(value) { 41 | this.dataTable.limit = Number(value); // TODO better way to handle that value of number is string? 42 | } 43 | 44 | get page() { 45 | return this.dataTable.page; 46 | } 47 | 48 | set page(value) { 49 | this.dataTable.page = Number(value); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/pagination.style.ts: -------------------------------------------------------------------------------- 1 | export const PAGINATION_STYLE = ` 2 | .pagination-box { 3 | position: relative; 4 | margin-top: -10px; 5 | } 6 | .pagination-range { 7 | margin-top: 7px; 8 | margin-left: 3px; 9 | display: inline-block; 10 | } 11 | .pagination-controllers { 12 | float: right; 13 | } 14 | .pagination-controllers input { 15 | min-width: 60px; 16 | /*padding: 1px 0px 0px 5px;*/ 17 | text-align: right; 18 | } 19 | 20 | .pagination-limit { 21 | margin-right: 25px; 22 | display: inline-table; 23 | width: 150px; 24 | } 25 | .pagination-pages { 26 | display: inline-block; 27 | } 28 | .pagination-page { 29 | width: 110px; 30 | display: inline-table; 31 | } 32 | .pagination-box button { 33 | outline: none !important; 34 | } 35 | .pagination-prevpage, 36 | .pagination-nextpage, 37 | .pagination-firstpage, 38 | .pagination-lastpage { 39 | vertical-align: top; 40 | } 41 | .pagination-reload { 42 | color: gray; 43 | font-size: 12px; 44 | } 45 | `; -------------------------------------------------------------------------------- /src/components/pagination.template.ts: -------------------------------------------------------------------------------- 1 | export const PAGINATION_TEMPLATE = ` 2 |
3 |
4 | {{dataTable.translations.paginationRange}}: 5 | 6 | - 7 | 8 | / 9 | 10 |
11 |
12 |
13 |
14 | {{dataTable.translations.paginationLimit}}: 15 | 18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 | 28 |
29 | / 30 | 31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | `; -------------------------------------------------------------------------------- /src/components/row.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, Input, Inject, forwardRef, Output, EventEmitter, OnDestroy 3 | } from '@angular/core'; 4 | import { DataTable } from './table.component'; 5 | import { ROW_TEMPLATE } from './row.template'; 6 | import { ROW_STYLE } from "./row.style"; 7 | 8 | 9 | @Component({ 10 | selector: '[dataTableRow]', 11 | template: ROW_TEMPLATE, 12 | styles: [ROW_STYLE] 13 | }) 14 | export class DataTableRow implements OnDestroy { 15 | 16 | @Input() item: any; 17 | @Input() index: number; 18 | 19 | expanded: boolean; 20 | 21 | // row selection: 22 | 23 | private _selected: boolean; 24 | 25 | @Output() selectedChange = new EventEmitter(); 26 | 27 | get selected() { 28 | return this._selected; 29 | } 30 | 31 | set selected(selected) { 32 | this._selected = selected; 33 | this.selectedChange.emit(selected); 34 | } 35 | 36 | // other: 37 | 38 | get displayIndex() { 39 | if (this.dataTable.pagination) { 40 | return this.dataTable.displayParams.offset + this.index + 1; 41 | } else { 42 | return this.index + 1; 43 | } 44 | } 45 | 46 | getTooltip() { 47 | if (this.dataTable.rowTooltip) { 48 | return this.dataTable.rowTooltip(this.item, this, this.index); 49 | } 50 | return ''; 51 | } 52 | 53 | constructor(@Inject(forwardRef(() => DataTable)) public dataTable: DataTable) {} 54 | 55 | ngOnDestroy() { 56 | this.selected = false; 57 | } 58 | 59 | private _this = this; // FIXME is there no template keyword for this in angular 2? 60 | } 61 | -------------------------------------------------------------------------------- /src/components/row.style.ts: -------------------------------------------------------------------------------- 1 | export const ROW_STYLE = ` 2 | .select-column { 3 | text-align: center; 4 | } 5 | 6 | .row-expand-button { 7 | cursor: pointer; 8 | text-align: center; 9 | } 10 | 11 | .clickable { 12 | cursor: pointer; 13 | } 14 | `; -------------------------------------------------------------------------------- /src/components/row.template.ts: -------------------------------------------------------------------------------- 1 | export const ROW_TEMPLATE = ` 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 |
23 |
24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | `; -------------------------------------------------------------------------------- /src/components/table.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, Input, Output, EventEmitter, ContentChildren, QueryList, 3 | TemplateRef, ContentChild, ViewChildren, OnInit 4 | } from '@angular/core'; 5 | import { DataTableColumn } from './column.component'; 6 | import { DataTableRow } from './row.component'; 7 | import { DataTableParams } from './types'; 8 | import { RowCallback } from './types'; 9 | import { DataTableTranslations, defaultTranslations } from './types'; 10 | import { drag } from '../utils/drag'; 11 | import { TABLE_TEMPLATE } from './table.template'; 12 | import { TABLE_STYLE } from "./table.style"; 13 | 14 | 15 | 16 | @Component({ 17 | selector: 'data-table', 18 | template: TABLE_TEMPLATE, 19 | styles: [TABLE_STYLE] 20 | }) 21 | export class DataTable implements DataTableParams, OnInit { 22 | 23 | private _items: any[] = []; 24 | 25 | @Input() get items() { 26 | return this._items; 27 | } 28 | 29 | set items(items: any[]) { 30 | this._items = items; 31 | this._onReloadFinished(); 32 | } 33 | 34 | @Input() itemCount: number; 35 | 36 | // UI components: 37 | 38 | @ContentChildren(DataTableColumn) columns: QueryList; 39 | @ViewChildren(DataTableRow) rows: QueryList; 40 | @ContentChild('dataTableExpand') expandTemplate: TemplateRef; 41 | 42 | // One-time optional bindings with default values: 43 | 44 | @Input() headerTitle: string; 45 | @Input() header = true; 46 | @Input() pagination = true; 47 | @Input() indexColumn = true; 48 | @Input() indexColumnHeader = ''; 49 | @Input() rowColors: RowCallback; 50 | @Input() rowTooltip: RowCallback; 51 | @Input() selectColumn = false; 52 | @Input() multiSelect = true; 53 | @Input() substituteRows = true; 54 | @Input() expandableRows = false; 55 | @Input() translations: DataTableTranslations = defaultTranslations; 56 | @Input() selectOnRowClick = false; 57 | @Input() autoReload = true; 58 | @Input() showReloading = false; 59 | 60 | // UI state without input: 61 | 62 | indexColumnVisible: boolean; 63 | selectColumnVisible: boolean; 64 | expandColumnVisible: boolean; 65 | 66 | // UI state: visible ge/set for the outside with @Input for one-time initial values 67 | 68 | private _sortBy: string; 69 | private _sortAsc = true; 70 | 71 | private _offset = 0; 72 | private _limit = 10; 73 | 74 | @Input() 75 | get sortBy() { 76 | return this._sortBy; 77 | } 78 | 79 | set sortBy(value) { 80 | this._sortBy = value; 81 | this._triggerReload(); 82 | } 83 | 84 | @Input() 85 | get sortAsc() { 86 | return this._sortAsc; 87 | } 88 | 89 | set sortAsc(value) { 90 | this._sortAsc = value; 91 | this._triggerReload(); 92 | } 93 | 94 | @Input() 95 | get offset() { 96 | return this._offset; 97 | } 98 | 99 | set offset(value) { 100 | this._offset = value; 101 | this._triggerReload(); 102 | } 103 | 104 | @Input() 105 | get limit() { 106 | return this._limit; 107 | } 108 | 109 | set limit(value) { 110 | this._limit = value; 111 | this._triggerReload(); 112 | } 113 | 114 | // calculated property: 115 | 116 | @Input() 117 | get page() { 118 | return Math.floor(this.offset / this.limit) + 1; 119 | } 120 | 121 | set page(value) { 122 | this.offset = (value - 1) * this.limit; 123 | } 124 | 125 | get lastPage() { 126 | return Math.ceil(this.itemCount / this.limit); 127 | } 128 | 129 | // setting multiple observable properties simultaneously 130 | 131 | sort(sortBy: string, asc: boolean) { 132 | this.sortBy = sortBy; 133 | this.sortAsc = asc; 134 | } 135 | 136 | // init 137 | 138 | ngOnInit() { 139 | this._initDefaultValues(); 140 | this._initDefaultClickEvents(); 141 | this._updateDisplayParams(); 142 | 143 | if (this.autoReload && this._scheduledReload == null) { 144 | this.reloadItems(); 145 | } 146 | } 147 | 148 | private _initDefaultValues() { 149 | this.indexColumnVisible = this.indexColumn; 150 | this.selectColumnVisible = this.selectColumn; 151 | this.expandColumnVisible = this.expandableRows; 152 | } 153 | 154 | private _initDefaultClickEvents() { 155 | this.headerClick.subscribe(tableEvent => this.sortColumn(tableEvent.column)); 156 | if (this.selectOnRowClick) { 157 | this.rowClick.subscribe(tableEvent => tableEvent.row.selected = !tableEvent.row.selected); 158 | } 159 | } 160 | 161 | // Reloading: 162 | 163 | _reloading = false; 164 | 165 | get reloading() { 166 | return this._reloading; 167 | } 168 | 169 | @Output() reload = new EventEmitter(); 170 | 171 | reloadItems() { 172 | this._reloading = true; 173 | this.reload.emit(this._getRemoteParameters()); 174 | } 175 | 176 | private _onReloadFinished() { 177 | this._updateDisplayParams(); 178 | 179 | this._selectAllCheckbox = false; 180 | this._reloading = false; 181 | } 182 | 183 | _displayParams = {}; // params of the last finished reload 184 | 185 | get displayParams() { 186 | return this._displayParams; 187 | } 188 | 189 | _updateDisplayParams() { 190 | this._displayParams = { 191 | sortBy: this.sortBy, 192 | sortAsc: this.sortAsc, 193 | offset: this.offset, 194 | limit: this.limit 195 | }; 196 | } 197 | 198 | _scheduledReload = null; 199 | 200 | // for avoiding cascading reloads if multiple params are set at once: 201 | _triggerReload() { 202 | if (this._scheduledReload) { 203 | clearTimeout(this._scheduledReload); 204 | } 205 | this._scheduledReload = setTimeout(() => { 206 | this.reloadItems(); 207 | }); 208 | } 209 | 210 | // event handlers: 211 | 212 | @Output() rowClick = new EventEmitter(); 213 | @Output() rowDoubleClick = new EventEmitter(); 214 | @Output() headerClick = new EventEmitter(); 215 | @Output() cellClick = new EventEmitter(); 216 | 217 | private rowClicked(row: DataTableRow, event) { 218 | this.rowClick.emit({ row, event }); 219 | } 220 | 221 | private rowDoubleClicked(row: DataTableRow, event) { 222 | this.rowDoubleClick.emit({ row, event }); 223 | } 224 | 225 | private headerClicked(column: DataTableColumn, event: MouseEvent) { 226 | if (!this._resizeInProgress) { 227 | this.headerClick.emit({ column, event }); 228 | } else { 229 | this._resizeInProgress = false; // this is because I can't prevent click from mousup of the drag end 230 | } 231 | } 232 | 233 | private cellClicked(column: DataTableColumn, row: DataTableRow, event: MouseEvent) { 234 | this.cellClick.emit({ row, column, event }); 235 | } 236 | 237 | // functions: 238 | 239 | private _getRemoteParameters(): DataTableParams { 240 | let params = {}; 241 | 242 | if (this.sortBy) { 243 | params.sortBy = this.sortBy; 244 | params.sortAsc = this.sortAsc; 245 | } 246 | if (this.pagination) { 247 | params.offset = this.offset; 248 | params.limit = this.limit; 249 | } 250 | return params; 251 | } 252 | 253 | private sortColumn(column: DataTableColumn) { 254 | if (column.sortable) { 255 | let ascending = this.sortBy === column.property ? !this.sortAsc : true; 256 | this.sort(column.property, ascending); 257 | } 258 | } 259 | 260 | get columnCount() { 261 | let count = 0; 262 | count += this.indexColumnVisible ? 1 : 0; 263 | count += this.selectColumnVisible ? 1 : 0; 264 | count += this.expandColumnVisible ? 1 : 0; 265 | this.columns.toArray().forEach(column => { 266 | count += column.visible ? 1 : 0; 267 | }); 268 | return count; 269 | } 270 | 271 | private getRowColor(item: any, index: number, row: DataTableRow) { 272 | if (this.rowColors !== undefined) { 273 | return (this.rowColors)(item, row, index); 274 | } 275 | } 276 | 277 | // selection: 278 | 279 | selectedRow: DataTableRow; 280 | selectedRows: DataTableRow[] = []; 281 | 282 | private _selectAllCheckbox = false; 283 | 284 | get selectAllCheckbox() { 285 | return this._selectAllCheckbox; 286 | } 287 | 288 | set selectAllCheckbox(value) { 289 | this._selectAllCheckbox = value; 290 | this._onSelectAllChanged(value); 291 | } 292 | 293 | private _onSelectAllChanged(value: boolean) { 294 | this.rows.toArray().forEach(row => row.selected = value); 295 | } 296 | 297 | onRowSelectChanged(row: DataTableRow) { 298 | 299 | // maintain the selectedRow(s) view 300 | if (this.multiSelect) { 301 | let index = this.selectedRows.indexOf(row); 302 | if (row.selected && index < 0) { 303 | this.selectedRows.push(row); 304 | } else if (!row.selected && index >= 0) { 305 | this.selectedRows.splice(index, 1); 306 | } 307 | } else { 308 | if (row.selected) { 309 | this.selectedRow = row; 310 | } else if (this.selectedRow === row) { 311 | this.selectedRow = undefined; 312 | } 313 | } 314 | 315 | // unselect all other rows: 316 | if (row.selected && !this.multiSelect) { 317 | this.rows.toArray().filter(row_ => row_.selected).forEach(row_ => { 318 | if (row_ !== row) { // avoid endless loop 319 | row_.selected = false; 320 | } 321 | }); 322 | } 323 | } 324 | 325 | // other: 326 | 327 | get substituteItems() { 328 | return Array.from({ length: this.displayParams.limit - this.items.length }); 329 | } 330 | 331 | // column resizing: 332 | 333 | private _resizeInProgress = false; 334 | 335 | private resizeColumnStart(event: MouseEvent, column: DataTableColumn, columnElement: HTMLElement) { 336 | this._resizeInProgress = true; 337 | 338 | drag(event, { 339 | move: (moveEvent: MouseEvent, dx: number) => { 340 | if (this._isResizeInLimit(columnElement, dx)) { 341 | column.width = columnElement.offsetWidth + dx; 342 | } 343 | }, 344 | }); 345 | } 346 | 347 | resizeLimit = 30; 348 | 349 | private _isResizeInLimit(columnElement: HTMLElement, dx: number) { 350 | /* This is needed because CSS min-width didn't work on table-layout: fixed. 351 | Without the limits, resizing can make the next column disappear completely, 352 | and even increase the table width. The current implementation suffers from the fact, 353 | that offsetWidth sometimes contains out-of-date values. */ 354 | if ((dx < 0 && (columnElement.offsetWidth + dx) <= this.resizeLimit) || 355 | !columnElement.nextElementSibling || // resizing doesn't make sense for the last visible column 356 | (dx >= 0 && (( columnElement.nextElementSibling).offsetWidth + dx) <= this.resizeLimit)) { 357 | return false; 358 | } 359 | return true; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/components/table.style.ts: -------------------------------------------------------------------------------- 1 | export const TABLE_STYLE = ` 2 | /* bootstrap override: */ 3 | 4 | :host /deep/ .data-table.table > tbody+tbody { 5 | border-top: none; 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 | /* I can't use the bootstrap striped table, because of the expandable rows */ 17 | :host /deep/ .row-odd { 18 | background-color: #F6F6F6; 19 | } 20 | :host /deep/ .row-even { 21 | } 22 | 23 | .data-table .substitute-rows > tr:hover, 24 | :host /deep/ .data-table .data-table-row:hover { 25 | background-color: #ECECEC; 26 | } 27 | /* table itself: */ 28 | 29 | .data-table { 30 | box-shadow: 0 0 15px rgb(236, 236, 236); 31 | table-layout: fixed; 32 | } 33 | 34 | /* header cells: */ 35 | 36 | .column-header { 37 | position: relative; 38 | } 39 | .expand-column-header { 40 | width: 50px; 41 | } 42 | .select-column-header { 43 | width: 50px; 44 | text-align: center; 45 | } 46 | .index-column-header { 47 | width: 40px; 48 | } 49 | .column-header.sortable { 50 | cursor: pointer; 51 | } 52 | .column-header .column-sort-icon { 53 | float: right; 54 | } 55 | .column-header.resizable .column-sort-icon { 56 | margin-right: 8px; 57 | } 58 | .column-header .column-sort-icon .column-sortable-icon { 59 | color: lightgray; 60 | } 61 | .column-header .column-resize-handle { 62 | position: absolute; 63 | top: 0; 64 | right: 0; 65 | margin: 0; 66 | padding: 0; 67 | width: 8px; 68 | height: 100%; 69 | cursor: col-resize; 70 | } 71 | 72 | /* cover: */ 73 | 74 | .data-table-box { 75 | position: relative; 76 | } 77 | 78 | .loading-cover { 79 | position: absolute; 80 | width: 100%; 81 | height: 100%; 82 | background-color: rgba(255, 255, 255, 0.3); 83 | top: 0; 84 | } 85 | `; -------------------------------------------------------------------------------- /src/components/table.template.ts: -------------------------------------------------------------------------------- 1 | export const TABLE_TEMPLATE = ` 2 |
3 | 4 | 5 |
6 | 7 | 8 | 9 | 13 | 16 | 30 | 31 | 32 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 45 | 46 |
10 | 11 | 12 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
  44 |
47 |
48 |
49 | 50 | 51 |
52 | `; -------------------------------------------------------------------------------- /src/components/types.ts: -------------------------------------------------------------------------------- 1 | import { DataTableRow } from './row.component'; 2 | import { DataTableColumn } from './column.component'; 3 | 4 | 5 | export type RowCallback = (item: any, row: DataTableRow, index: number) => string; 6 | 7 | export type CellCallback = (item: any, row: DataTableRow, column: DataTableColumn, index: number) => string; 8 | 9 | // export type HeaderCallback = (column: DataTableColumn) => string; 10 | 11 | 12 | export interface DataTableTranslations { 13 | indexColumn: string; 14 | selectColumn: string; 15 | expandColumn: string; 16 | paginationLimit: string; 17 | paginationRange: string; 18 | } 19 | 20 | export var defaultTranslations = { 21 | indexColumn: 'index', 22 | selectColumn: 'select', 23 | expandColumn: 'expand', 24 | paginationLimit: 'Limit', 25 | paginationRange: 'Results' 26 | }; 27 | 28 | 29 | export interface DataTableParams { 30 | offset?: number; 31 | limit?: number; 32 | sortBy?: string; 33 | sortAsc?: boolean; 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { DataTable} from './components/table.component'; 6 | import { DataTableColumn } from './components/column.component'; 7 | import { DataTableRow } from './components/row.component'; 8 | import { DataTablePagination } from './components/pagination.component'; 9 | import { DataTableHeader } from './components/header.component'; 10 | 11 | import { PixelConverter } from './utils/px'; 12 | import { Hide } from './utils/hide'; 13 | import { MinPipe } from './utils/min'; 14 | 15 | export * from './components/types'; 16 | export * from './tools/data-table-resource'; 17 | 18 | export { DataTable, DataTableColumn, DataTableRow, DataTablePagination, DataTableHeader }; 19 | export const DATA_TABLE_DIRECTIVES = [ DataTable, DataTableColumn ]; 20 | 21 | 22 | @NgModule({ 23 | imports: [ CommonModule, FormsModule ], 24 | declarations: [ 25 | DataTable, DataTableColumn, 26 | DataTableRow, DataTablePagination, DataTableHeader, 27 | PixelConverter, Hide, MinPipe 28 | ], 29 | exports: [ DataTable, DataTableColumn ] 30 | }) 31 | export class DataTableModule { } -------------------------------------------------------------------------------- /src/tools/data-table-resource.ts: -------------------------------------------------------------------------------- 1 | import { DataTableParams } from '../components/types'; 2 | 3 | 4 | export class DataTableResource { 5 | 6 | constructor(private items: T[]) {} 7 | 8 | query(params: DataTableParams, filter?: (item: T, index: number, items: T[]) => boolean): Promise { 9 | 10 | let result: T[] = []; 11 | if (filter) { 12 | result = this.items.filter(filter); 13 | } else { 14 | result = this.items.slice(); // shallow copy to use for sorting instead of changing the original 15 | } 16 | 17 | if (params.sortBy) { 18 | result.sort((a, b) => { 19 | if (typeof a[params.sortBy] === 'string') { 20 | return a[params.sortBy].localeCompare(b[params.sortBy]); 21 | } else { 22 | return a[params.sortBy] - b[params.sortBy]; 23 | } 24 | }); 25 | if (params.sortAsc === false) { 26 | result.reverse(); 27 | } 28 | } 29 | if (params.offset !== undefined) { 30 | if (params.limit === undefined) { 31 | result = result.slice(params.offset, result.length); 32 | } else { 33 | result = result.slice(params.offset, params.offset + params.limit); 34 | } 35 | } 36 | 37 | return new Promise((resolve, reject) => { 38 | setTimeout(() => resolve(result)); 39 | }); 40 | } 41 | 42 | count(): Promise { 43 | return new Promise((resolve, reject) => { 44 | setTimeout(() => resolve(this.items.length)); 45 | }); 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/drag.ts: -------------------------------------------------------------------------------- 1 | 2 | export type MoveHandler = (event: MouseEvent, dx: number, dy: number, x: number, y: number) => void; 3 | export type UpHandler = (event: MouseEvent, x: number, y: number, moved: boolean) => void; 4 | 5 | export function drag(event: MouseEvent, { move: move, up: up}: {move: MoveHandler, up?: UpHandler}) { 6 | 7 | let startX = event.pageX; 8 | let startY = event.pageY; 9 | let x = startX; 10 | let y = startY; 11 | let moved = false; 12 | 13 | function mouseMoveHandler(event: MouseEvent) { 14 | let dx = event.pageX - x; 15 | let dy = event.pageY - y; 16 | x = event.pageX; 17 | y = event.pageY; 18 | if (dx || dy) moved = true; 19 | 20 | move(event, dx, dy, x, y); 21 | 22 | event.preventDefault(); // to avoid text selection 23 | } 24 | 25 | function mouseUpHandler(event: MouseEvent) { 26 | x = event.pageX; 27 | y = event.pageY; 28 | 29 | document.removeEventListener('mousemove', mouseMoveHandler); 30 | document.removeEventListener('mouseup', mouseUpHandler); 31 | 32 | if (up) up(event, x, y, moved); 33 | } 34 | 35 | document.addEventListener('mousemove', mouseMoveHandler); 36 | document.addEventListener('mouseup', mouseUpHandler); 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/hide.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Renderer } from '@angular/core'; 2 | 3 | 4 | function isBlank(obj: any): boolean { 5 | return obj === undefined || obj === null; 6 | } 7 | 8 | @Directive({ selector: '[hide]', inputs: ['hide'] }) 9 | export class Hide { 10 | 11 | private _prevCondition: boolean = null; 12 | private _displayStyle: string; 13 | 14 | constructor(private _elementRef: ElementRef, private _renderer: Renderer) { } 15 | 16 | set hide(newCondition: boolean) { 17 | this.initDisplayStyle(); 18 | 19 | if (newCondition && (isBlank(this._prevCondition) || !this._prevCondition)) { 20 | this._prevCondition = true; 21 | this._renderer.setElementStyle(this._elementRef.nativeElement, 'display', 'none'); 22 | } else if (!newCondition && (isBlank(this._prevCondition) || this._prevCondition)) { 23 | this._prevCondition = false; 24 | this._renderer.setElementStyle(this._elementRef.nativeElement, 'display', this._displayStyle); 25 | } 26 | } 27 | 28 | private initDisplayStyle() { 29 | if (this._displayStyle === undefined) { 30 | let displayStyle = this._elementRef.nativeElement.style.display; 31 | if (displayStyle && displayStyle !== 'none') { 32 | this._displayStyle = displayStyle; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/min.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | 4 | @Pipe({ 5 | name: 'min' 6 | }) 7 | export class MinPipe implements PipeTransform { 8 | transform(value: number[], args: string[]): any { 9 | return Math.min.apply(null, value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/px.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | 4 | @Pipe({ 5 | name: 'px' 6 | }) 7 | export class PixelConverter implements PipeTransform { 8 | transform(value: string | number, args: string[]): any { 9 | if (value === undefined) { 10 | return; 11 | } 12 | if (typeof value === 'string') { 13 | return value; 14 | } 15 | if (typeof value === 'number') { 16 | return value + 'px'; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "mapRoot": "/", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noImplicitAny": false, 12 | "outDir": "dist/", 13 | "rootDir": "src/", 14 | "sourceMap": true, 15 | "target": "es5", 16 | "inlineSources": true 17 | }, 18 | "exclude": [ 19 | "dist", 20 | "node_modules", 21 | "index.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-2-data-table", 3 | "dependencies": {}, 4 | "globalDevDependencies": { 5 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504" 6 | } 7 | } 8 | --------------------------------------------------------------------------------