diff --git a/.gitignore b/.gitignore index 2d45d27..93a5ab4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ -/dist +!dist/package.json +dist/* /tmp /node_modules /.idea /typings -npm-debug.log \ No newline at end of file +npm-debug.log +/.vscode +/src/**/*.js* +/src/**/*.ngfactory.ts +**/*.ngsummary.json +/aot \ No newline at end of file diff --git a/.npmignore b/.npmignore index 43abdac..e831038 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1 @@ - -/tmp -/node_modules -/.idea -/typings \ No newline at end of file +src \ No newline at end of file diff --git a/README.md b/README.md index 370776d..1d40a1c 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,22 @@ A simple Angular 2 data table, with built-in solutions for features including: 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. -The templates use bootstrap CSS class names, so the component requires a bootstrap .css file to be present in the application using it. +The templates use bootstrap 4 CSS class names, so the component requires a bootstrap .css file to be present in the application using it. -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. +The templates use Font Awesome CSS class names, so the component requires Font Awesome to be present in the application using it. ## Installing: -`npm install angular-2-data-table --save` +`npm install angular-datatable --save` +## Usage: + +### Custom Sorting + +To make use of custom sort functionality, one must add a `[customSort]` binding to the data-table (if necessary for the default sort) or to the data-column (if necessary to sort that column). One must still specify the `[sortBy]` binding and specify the primary property of the bound data set that is being sorted, as this is fundamental to maintaining sort order (ASC or DESC). + +The `[customSort]` binding must bind to a function that implements the function signature defined by `DataTableSortCallback`. This function is the same as any standard JS Array.sort() callback, and in fact is actually passed to array.sort() when sorting occurs. + +If custom sort is not required, then only the `[sortBy]` binding is necessary. #### Licensing MIT License diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..a9a389f --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,19 @@ +# Versioning Angular-DataTable + +Version maintenance is a critical aspect of maintaining a public NPM package. New versions must be carefully managed to ensure they properly publish. Moreso, proper linting and testing must be performed on each version before publishing to ensure a bad version is never published to the public NPM repository. + +Please follow this procedure to version angular-datatable before publishing: + +## Versioning Procedure + +On your development branch: + +1. Verify that you have the latest changes from development by rebasing or merging into your branch. +2. Run linting to ensure the code conforms to necessary style rules. +3. Run unit tests to ensure the code properly functions. +4. Bump the version number in package.json: + - Use revisions for small updates that fix bugs that do not otherwise change the functionality + - Use minor versions for small updates to functionality, if changes to not break backwards compatibility + - Use major versions for large updates to functionality, particularly if they involve breaking changes to backwards compatibility +5. PR your changes with the updated version into the develop branch +6. Request a repository owner to merge the changes into master, which will automatically invoke a publish to the public NPM repo \ No newline at end of file diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..097975c --- /dev/null +++ b/circle.yml @@ -0,0 +1,23 @@ +## Customize the test machine +machine: + timezone: + America/Denver # Set the timezone + node: + version: 6.9.4 + +## Customize dependencies +dependencies: + pre: + - npm install -g typescript typings + +## customize test commands +test: + override: + - npm run build + +deployment: + production: + branch: master + commands: + - echo -e "$NPM_USERNAME\n$NPM_PASSWORD\n$NPM_EMAIL" | npm login + - npm run publish-to-npm diff --git a/dist/package.json b/dist/package.json new file mode 100644 index 0000000..1b3b39b --- /dev/null +++ b/dist/package.json @@ -0,0 +1,37 @@ +{ + "name": "angular-datatable", + "version": "2.2.1", + "description": "An Angular 2 datatable, with pagination, sorting, expandable rows etc.", + "main": "bundles/datatable.umd.js", + "module": "index.js", + "typings": "index.d.ts", + "keywords": [ + "angular", + "angular2", + "Angular 2", + "ng2", + "ngx", + "datatable", + "data-table", + "data table", + "pagination" + ], + "author": "BrieBug Developers ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git://git@github.com/briebug/angular-datatable.git" + }, + "homepage": "https://github.com/briebug/angular-datatable", + "bugs": { + "url": "https://github.com/briebug/angular-datatable/issues" + }, + "peerDependencies": { + "@angular/core": "^2.4.0", + "@angular/common": "^2.4.0", + "@angular/forms": "^2.4.0", + "reflect-metadata": "^0.1.8", + "rxjs": "^5.0.1", + "zone.js": "^0.7.2" + } +} \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..e0192f9 --- /dev/null +++ b/index.ts @@ -0,0 +1,6 @@ +/** + * @module + * @description + * Entry point for all public APIs of the Angular Module + */ +export * from './src/index'; diff --git a/package.json b/package.json index 570f02c..f35bf53 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { - "name": "angular-2-data-table", - "version": "0.1.2", - "description": "An Angular 2 data table, with pagination, sorting, expandable rows etc.", + "name": "angular-datatable", + "version": "2.2.1", + "description": "An Angular 2 datatable, with pagination, sorting, expandable rows etc.", + "main": "dist/bundles/angular-datatable.umd.js", + "module": "dist/index.js", "keywords": [ "angular", "angular2", @@ -12,27 +14,47 @@ "data table", "pagination" ], - "main": "dist/index.js", - "typings": "dist/index.d.ts", "scripts": { - "build": "rm -rf dist/* && tsc", - "serve": "rm -rf dist/ && tsc -w", - "prepublish": "npm run build" + "cleanup": "rimraf dist/bundles dist/src dist/index.d.ts dist/index.metadata.json dist/index.js dist/index.js.map dist/LICENSE dist/README.md", + "bundling": "rollup -c", + "minify": "uglifyjs dist/bundles/angular-datatable.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/angular-datatable.umd.min.js", + "copy": "copyfiles LICENSE README.md dist", + "build": "npm run cleanup && ngc && npm run bundling && npm run minify && npm run copy", + "build-only": "npm run cleanup && ngc", + "publish-to-npm": "cd dist && npm publish", + "link": "cd dist && npm link && cd .." }, "repository": { "type": "git", - "url": "git://git@github.com/ggmod/angular-2-data-table.git" + "url": "git://git@github.com/briebugconsulting/angular-datatable.git" }, - "peerDependencies": { - "@angular/core": "^2.0.0" - }, - "author": "ggmod ", + "author": "BrieBug Developers ", "license": "MIT", + "dependencies": { + "@angular/common": "^2.4.0", + "@angular/core": "^2.4.0", + "@angular/forms": "^2.4.0", + "reflect-metadata": "^0.1.8", + "rxjs": "^5.0.3", + "zone.js": "^0.7.2" + }, "devDependencies": { - "@angular/common": "^2.0.0", - "@angular/core": "^2.0.0", - "@angular/forms": "^2.0.0", - "rxjs": "^5.0.0-beta.12", - "typescript": "^2.0.2" + "@angular/compiler": "^2.4.0", + "@angular/compiler-cli": "^2.4.0", + "copyfiles": "^1.2.0", + "cz-conventional-changelog": "1.2.0", + "rimraf": "^2.6.1", + "rollup": "^0.37.0", + "typescript": "~2.0.10", + "uglify-js": "^2.7.5", + "validate-commit-msg": "2.8.2" + }, + "config": { + "ghooks": { + "commit-msg": "validate-commit-msg" + }, + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..114ebe6 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,12 @@ +export default { + entry: 'dist/index.js', + dest: 'dist/bundles/angular-datatable.umd.js', + sourceMap: false, + format: 'umd', + moduleName: 'ng.datatable', + globals: { + '@angular/core': 'ng.core', + '@angular/common': 'ng.common', + '@angular/forms': 'ng.forms' + } +} \ No newline at end of file diff --git a/src/components/column.component.ts b/src/components/column.component.ts index 0abb1af..aac4bcb 100644 --- a/src/components/column.component.ts +++ b/src/components/column.component.ts @@ -1,10 +1,10 @@ import { Directive, Input, ContentChild, OnInit } from '@angular/core'; import { DataTableRow } from './row.component'; -import { CellCallback } from './types'; +import { CellCallback, DataTableSortCallback } from './types'; @Directive({ - selector: 'data-table-column' + selector: 'data-table-column' }) export class DataTableColumn implements OnInit { @@ -15,13 +15,14 @@ export class DataTableColumn implements OnInit { @Input() property: string; @Input() styleClass: string; @Input() cellColors: CellCallback; + @Input() customSort: DataTableSortCallback; // init and state: @Input() width: number | string; @Input() visible = true; - @ContentChild('dataTableCell') cellTemplate; - @ContentChild('dataTableHeader') headerTemplate; + @ContentChild('dataTableCell') cellTemplate: any; + @ContentChild('dataTableHeader') headerTemplate: any; getCellColor(row: DataTableRow, index: number) { if (this.cellColors !== undefined) { @@ -50,4 +51,4 @@ export class DataTableColumn implements OnInit { }; } } -} +} \ No newline at end of file diff --git a/src/components/header.template.ts b/src/components/header.template.ts index b299a86..76194fe 100644 --- a/src/components/header.template.ts +++ b/src/components/header.template.ts @@ -4,11 +4,15 @@ export const HEADER_TEMPLATE = `
+
diff --git a/src/components/row.component.ts b/src/components/row.component.ts index 96da1c8..77668b7 100644 --- a/src/components/row.component.ts +++ b/src/components/row.component.ts @@ -23,6 +23,7 @@ export class DataTableRow implements OnDestroy { private _selected: boolean; @Output() selectedChange = new EventEmitter(); + @Output() expandRowChange = new EventEmitter(); get selected() { return this._selected; @@ -50,6 +51,12 @@ export class DataTableRow implements OnDestroy { return ''; } + expandRow(event: Event) { + event.stopPropagation(); + this.expanded = !this.expanded; + this.expandRowChange.emit(); + } + constructor(@Inject(forwardRef(() => DataTable)) public dataTable: DataTable) {} ngOnDestroy() { diff --git a/src/components/row.template.ts b/src/components/row.template.ts index 7e881ea..e231ce6 100644 --- a/src/components/row.template.ts +++ b/src/components/row.template.ts @@ -9,9 +9,9 @@ export const ROW_TEMPLATE = ` (dblclick)="dataTable.rowDoubleClicked(_this, $event)" (click)="dataTable.rowClicked(_this, $event)" > - - - + + @@ -23,7 +23,7 @@ export const ROW_TEMPLATE = `
- +
diff --git a/src/components/table.component.ts b/src/components/table.component.ts index e78cdf7..3f7f00c 100644 --- a/src/components/table.component.ts +++ b/src/components/table.component.ts @@ -2,21 +2,20 @@ import { Component, Input, Output, EventEmitter, ContentChildren, QueryList, TemplateRef, ContentChild, ViewChildren, OnInit } from '@angular/core'; -import { DataTableColumn } from './column.component'; -import { DataTableRow } from './row.component'; -import { DataTableParams } from './types'; -import { RowCallback } from './types'; -import { DataTableTranslations, defaultTranslations } from './types'; -import { drag } from '../utils/drag'; -import { TABLE_TEMPLATE } from './table.template'; -import { TABLE_STYLE } from "./table.style"; - +import {DataTableColumn} from './column.component'; +import {DataTableRow} from './row.component'; +import {DataTableParams, DataTableSortCallback} from './types'; +import {RowCallback} from './types'; +import {DataTableTranslations, defaultTranslations} from './types'; +import {drag} from '../utils/drag'; +import {TABLE_TEMPLATE} from './table.template'; +import {TABLE_STYLE} from "./table.style"; @Component({ - selector: 'data-table', - template: TABLE_TEMPLATE, - styles: [TABLE_STYLE] + selector: 'data-table', + template: TABLE_TEMPLATE, + styles: [TABLE_STYLE] }) export class DataTable implements DataTableParams, OnInit { @@ -56,6 +55,7 @@ export class DataTable implements DataTableParams, OnInit { @Input() selectOnRowClick = false; @Input() autoReload = true; @Input() showReloading = false; + @Input() showDownloadButton = false; // UI state without input: @@ -67,6 +67,7 @@ export class DataTable implements DataTableParams, OnInit { private _sortBy: string; private _sortAsc = true; + private _customSort: DataTableSortCallback; private _offset = 0; private _limit = 10; @@ -91,6 +92,16 @@ export class DataTable implements DataTableParams, OnInit { this._triggerReload(); } + @Input() + get customSort() { + return this._customSort; + } + + set customSort(value) { + this._customSort = value; + this._triggerReload(); + } + @Input() get offset() { return this._offset; @@ -128,9 +139,10 @@ export class DataTable implements DataTableParams, OnInit { // setting multiple observable properties simultaneously - sort(sortBy: string, asc: boolean) { + sort(sortBy: string, asc: boolean, customSort: DataTableSortCallback) { this.sortBy = sortBy; this.sortAsc = asc; + this.customSort = customSort; } // init @@ -152,9 +164,9 @@ export class DataTable implements DataTableParams, OnInit { } private _initDefaultClickEvents() { - this.headerClick.subscribe(tableEvent => this.sortColumn(tableEvent.column)); + this.headerClick.subscribe((tableEvent: any) => this.sortColumn(tableEvent.column)); if (this.selectOnRowClick) { - this.rowClick.subscribe(tableEvent => tableEvent.row.selected = !tableEvent.row.selected); + this.rowClick.subscribe((tableEvent: any) => tableEvent.row.selected = !tableEvent.row.selected); } } @@ -189,13 +201,14 @@ export class DataTable implements DataTableParams, OnInit { _updateDisplayParams() { this._displayParams = { sortBy: this.sortBy, + customSort: this.customSort, sortAsc: this.sortAsc, offset: this.offset, limit: this.limit }; } - _scheduledReload = null; + _scheduledReload: any = null; // for avoiding cascading reloads if multiple params are set at once: _triggerReload() { @@ -207,31 +220,39 @@ export class DataTable implements DataTableParams, OnInit { }); } + // Download + @Output() download = new EventEmitter(); + + downloadItems() { + this.download.emit(this._getRemoteParameters()); + } + // event handlers: @Output() rowClick = new EventEmitter(); @Output() rowDoubleClick = new EventEmitter(); @Output() headerClick = new EventEmitter(); @Output() cellClick = new EventEmitter(); + @Output() rowExpandChange = new EventEmitter(); - private rowClicked(row: DataTableRow, event) { - this.rowClick.emit({ row, event }); + private rowClicked(row: DataTableRow, event: Event) { + this.rowClick.emit({row, event}); } - private rowDoubleClicked(row: DataTableRow, event) { - this.rowDoubleClick.emit({ row, event }); + private rowDoubleClicked(row: DataTableRow, event: Event) { + this.rowDoubleClick.emit({row, event}); } private headerClicked(column: DataTableColumn, event: MouseEvent) { if (!this._resizeInProgress) { - this.headerClick.emit({ column, event }); + this.headerClick.emit({column, event}); } else { this._resizeInProgress = false; // this is because I can't prevent click from mousup of the drag end } } private cellClicked(column: DataTableColumn, row: DataTableRow, event: MouseEvent) { - this.cellClick.emit({ row, column, event }); + this.cellClick.emit({row, column, event}); } // functions: @@ -241,6 +262,7 @@ export class DataTable implements DataTableParams, OnInit { if (this.sortBy) { params.sortBy = this.sortBy; + params.customSort = this.customSort; params.sortAsc = this.sortAsc; } if (this.pagination) { @@ -253,7 +275,7 @@ export class DataTable implements DataTableParams, OnInit { private sortColumn(column: DataTableColumn) { if (column.sortable) { let ascending = this.sortBy === column.property ? !this.sortAsc : true; - this.sort(column.property, ascending); + this.sort(column.property, ascending, column.customSort); } } @@ -295,7 +317,6 @@ export class DataTable implements DataTableParams, OnInit { } onRowSelectChanged(row: DataTableRow) { - // maintain the selectedRow(s) view if (this.multiSelect) { let index = this.selectedRows.indexOf(row); @@ -308,7 +329,7 @@ export class DataTable implements DataTableParams, OnInit { if (row.selected) { this.selectedRow = row; } else if (this.selectedRow === row) { - this.selectedRow = undefined; + this.selectedRow = row; } } @@ -322,10 +343,14 @@ export class DataTable implements DataTableParams, OnInit { } } + onRowExpandChanged(row: DataTableRow) { + this.rowExpandChange.emit(row); + } + // other: get substituteItems() { - return Array.from({ length: this.displayParams.limit - this.items.length }); + return Array.from({length: this.displayParams.limit - this.items.length}); } // column resizing: @@ -351,11 +376,10 @@ export class DataTable implements DataTableParams, OnInit { Without the limits, resizing can make the next column disappear completely, and even increase the table width. The current implementation suffers from the fact, that offsetWidth sometimes contains out-of-date values. */ - if ((dx < 0 && (columnElement.offsetWidth + dx) <= this.resizeLimit) || - !columnElement.nextElementSibling || // resizing doesn't make sense for the last visible column + if ((dx < 0 && (columnElement.offsetWidth + dx) <= this.resizeLimit) || !columnElement.nextElementSibling || // resizing doesn't make sense for the last visible column (dx >= 0 && (( columnElement.nextElementSibling).offsetWidth + dx) <= this.resizeLimit)) { return false; } return true; } -} +} \ No newline at end of file diff --git a/src/components/table.template.ts b/src/components/table.template.ts index 14cdd8c..a32828c 100644 --- a/src/components/table.template.ts +++ b/src/components/table.template.ts @@ -19,10 +19,10 @@ export const TABLE_TEMPLATE = ` - + - - + + @@ -30,7 +30,8 @@ export const TABLE_TEMPLATE = ` + dataTableRow #row [item]="item" [index]="index" (selectedChange)="onRowSelectChanged(row)" + (expandRowChange)="onRowExpandChanged(row)"> string; +export type DataTableSortCallback = (a: any, b: any) => number; export interface DataTableTranslations { indexColumn: string; @@ -30,5 +31,6 @@ export interface DataTableParams { offset?: number; limit?: number; sortBy?: string; + customSort?: DataTableSortCallback; sortAsc?: boolean; -} +} \ No newline at end of file diff --git a/src/data-table.module.ts b/src/data-table.module.ts new file mode 100644 index 0000000..bcfa164 --- /dev/null +++ b/src/data-table.module.ts @@ -0,0 +1,42 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; + +import {DataTable} from './components/table.component'; +import {DataTableColumn} from './components/column.component'; +import {DataTableRow} from './components/row.component'; +import {DataTablePagination} from './components/pagination.component'; +import {DataTableHeader} from './components/header.component'; + +import {PixelConverter} from './utils/px'; +import {Hide} from './utils/hide'; +import {MinPipe} from './utils/min'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule + ], + declarations: [ + DataTable, + DataTableColumn, + DataTableRow, + DataTablePagination, + DataTableHeader, + PixelConverter, + Hide, + MinPipe + ], + exports: [ + DataTable, + DataTableColumn, + DataTableRow, + DataTablePagination, + DataTableHeader, + PixelConverter, + Hide, + MinPipe + ] +}) +export class DataTableModule { +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ac90400..c9e504e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,31 +1 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; - -import { DataTable} from './components/table.component'; -import { DataTableColumn } from './components/column.component'; -import { DataTableRow } from './components/row.component'; -import { DataTablePagination } from './components/pagination.component'; -import { DataTableHeader } from './components/header.component'; - -import { PixelConverter } from './utils/px'; -import { Hide } from './utils/hide'; -import { MinPipe } from './utils/min'; - -export * from './components/types'; -export * from './tools/data-table-resource'; - -export { DataTable, DataTableColumn, DataTableRow, DataTablePagination, DataTableHeader }; -export const DATA_TABLE_DIRECTIVES = [ DataTable, DataTableColumn ]; - - -@NgModule({ - imports: [ CommonModule, FormsModule ], - declarations: [ - DataTable, DataTableColumn, - DataTableRow, DataTablePagination, DataTableHeader, - PixelConverter, Hide, MinPipe - ], - exports: [ DataTable, DataTableColumn ] -}) -export class DataTableModule { } \ No newline at end of file +export {DataTableModule} from './data-table.module'; \ No newline at end of file diff --git a/src/tools/data-table-resource.ts b/src/tools/data-table-resource.ts index 31a2768..fff2e84 100644 --- a/src/tools/data-table-resource.ts +++ b/src/tools/data-table-resource.ts @@ -15,13 +15,17 @@ export class DataTableResource { } if (params.sortBy) { - result.sort((a, b) => { - if (typeof a[params.sortBy] === 'string') { - return a[params.sortBy].localeCompare(b[params.sortBy]); - } else { - return a[params.sortBy] - b[params.sortBy]; - } - }); + if (!params.customSort) { + result.sort((a: any, b: any) => { + if (typeof a[ params.sortBy] === 'string') { + return a[ params.sortBy].localeCompare(b[ params.sortBy]); + } else { + return a[ params.sortBy] - b[ params.sortBy]; + } + }); + } else { + result.sort(params.customSort); + } if (params.sortAsc === false) { result.reverse(); } @@ -45,4 +49,4 @@ export class DataTableResource { }); } -} +} \ No newline at end of file diff --git a/src/utils/hide.ts b/src/utils/hide.ts index 0c98ec8..caa01f1 100644 --- a/src/utils/hide.ts +++ b/src/utils/hide.ts @@ -8,7 +8,7 @@ function isBlank(obj: any): boolean { @Directive({ selector: '[hide]', inputs: ['hide'] }) export class Hide { - private _prevCondition: boolean = null; + private _prevCondition: boolean = false; private _displayStyle: string; constructor(private _elementRef: ElementRef, private _renderer: Renderer) { } diff --git a/tsconfig.json b/tsconfig.json index afa16d1..5db88eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,33 @@ { - "compileOnSave": false, "compilerOptions": { + "baseUrl": ".", "declaration": true, - "emitDecoratorMetadata": true, + "stripInternal": true, "experimentalDecorators": true, - "mapRoot": "/", - "module": "commonjs", + "strictNullChecks": false, + "noImplicitAny": true, + "module": "es2015", "moduleResolution": "node", - "noEmitOnError": true, - "noImplicitAny": false, - "outDir": "dist/", - "rootDir": "src/", + "paths": { + "@angular/core": ["node_modules/@angular/core"], + "@angular/common": ["node_modules/@angular/common"], + "@angular/forms": ["node_modules/@angular/forms"] + }, + "rootDir": ".", + "outDir": "dist", "sourceMap": true, + "inlineSources": true, "target": "es5", - "inlineSources": true + "skipLibCheck": true, + "lib": [ + "es2016", + "dom" + ] }, - "exclude": [ - "dist", - "node_modules", + "files": [ "index.ts" - ] -} + ], + "angularCompilerOptions": { + "strictMetadataEmit": true + } +} \ No newline at end of file diff --git a/typings.json b/typings.json deleted file mode 100644 index b0bb089..0000000 --- a/typings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "angular-2-data-table", - "dependencies": {}, - "globalDevDependencies": { - "es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504" - } -}