From cddfc52aa4c14bdd9941513dfb1d144891299c08 Mon Sep 17 00:00:00 2001 From: KiefTimo Date: Mon, 4 Sep 2017 14:56:02 +0200 Subject: [PATCH 01/17] add virtual table component --- README.md | 1 + config/webpack/webpack.common.js | 8 +- package.json | 1 + src/components/system-config-spec.ts | 6 +- .../virtual-table/column.component.ts | 21 ++ src/components/virtual-table/index.ts | 3 + src/components/virtual-table/module.ts | 29 +++ .../virtual-table/table.component.ts | 219 ++++++++++++++++++ src/e2e-app/app/app.component.html | 1 + src/e2e-app/app/app.component.ts | 2 + src/e2e-app/app/app.module.ts | 6 +- .../virtual-table.component.html | 58 +++++ .../virtual-table/virtual-table.component.ts | 35 +++ 13 files changed, 383 insertions(+), 7 deletions(-) create mode 100644 src/components/virtual-table/column.component.ts create mode 100644 src/components/virtual-table/index.ts create mode 100644 src/components/virtual-table/module.ts create mode 100644 src/components/virtual-table/table.component.ts create mode 100644 src/e2e-app/app/virtual-table/virtual-table.component.html create mode 100644 src/e2e-app/app/virtual-table/virtual-table.component.ts diff --git a/README.md b/README.md index 1971dd89..c52eab86 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Additional components for @angular-mdl/core that are not part of material design | fab-menu | [leojpod](https://github.com/leojpod) | a fab menu component | [![npm version](https://badge.fury.io/js/@angular-mdl%2Ffab-menu.svg)](https://www.npmjs.com/package/@angular-mdl/fab-menu)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/fab-menu) | experimental | [demo](http://mseemann.io/angular2-mdl-ext/fab-menu) | popover | [tb](https://github.com/tb) | popover with arbitrary content | [![npm version](https://badge.fury.io/js/%40angular-mdl%2Fpopover.svg)](https://www.npmjs.com/package/@angular-mdl/popover)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/popover) | experimental | [demo](http://mseemann.io/angular2-mdl-ext/popover) | select | [tb](https://github.com/tb) | a select box | [![npm version](https://badge.fury.io/js/%40angular-mdl%2Fselect.svg)](https://www.npmjs.com/package/@angular-mdl/select)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/select) | experimental | [demo](http://mseemann.io/angular2-mdl-ext/select) +| virtual-table | [kmcs](https://github.com/kmcs) | a virtual table | [![npm version](https://badge.fury.io/js/%40angular-mdl%2Fselect.svg)](https://www.npmjs.com/package/@angular-mdl/select)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/select) | proof of concept | [demo](http://mseemann.io/angular2-mdl-ext/virtual-table) Status means: diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index eb3730af..5a89b408 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -28,10 +28,10 @@ module.exports = { enforce: 'pre', test: /.ts$/, loader: 'string-replace-loader', - options: { - search: 'moduleId: module.id,', - replace: '', - flags: 'g' + query: { + multiple: [ + {search: new RegExp('moduleId: module.id,', 'g'), replace: ''} + ] } }, { diff --git a/package.json b/package.json index 5e5fc9bd..c601ac90 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@angular/platform-browser-dynamic": "4.3.6", "@angular/router": "4.3.6", "@mseemann/prism": "0.0.1", + "angular2-virtual-scroll": "^0.2.1", "core-js": "2.5.1", "custom-event-polyfill": "^0.3.0", "match-sorter": "1.8.1", diff --git a/src/components/system-config-spec.ts b/src/components/system-config-spec.ts index 6c017b58..b8217484 100644 --- a/src/components/system-config-spec.ts +++ b/src/components/system-config-spec.ts @@ -4,7 +4,8 @@ const components = [ 'expansion-panel', 'fab-menu', 'popover', - 'select' + 'select', + 'virtual-table' ]; const angularPackages = [ @@ -42,7 +43,8 @@ System.config({ '@angular': 'vendor/@angular', '@angular-mdl/core': 'vendor/@angular-mdl/core', 'rxjs': 'vendor/rxjs', - 'moment': 'vendor/moment' + 'moment': 'vendor/moment', + 'angular2-virtual-scroll': 'angular2-virtual-scroll' }, packages: vendorPackages }); diff --git a/src/components/virtual-table/column.component.ts b/src/components/virtual-table/column.component.ts new file mode 100644 index 00000000..41a3d77a --- /dev/null +++ b/src/components/virtual-table/column.component.ts @@ -0,0 +1,21 @@ +import { Component, Input, ContentChild, TemplateRef } from '@angular/core'; + +@Component({ + selector: 'mdl-column', + template: '' +}) +export class MdlVirtualTableColumnComponent { + @Input() label: string; + @Input() field: string; + @Input() width: string; + @Input() set sortable(v: boolean) { + this._sortable = typeof(v) === 'boolean' ? v : (v === 'true'); + } + get sortable(): boolean { + return this._sortable; + } + _sortable: boolean = false; + + sortDirection: string; + @ContentChild(TemplateRef) cellTemplate: TemplateRef; +} \ No newline at end of file diff --git a/src/components/virtual-table/index.ts b/src/components/virtual-table/index.ts new file mode 100644 index 00000000..f64962c7 --- /dev/null +++ b/src/components/virtual-table/index.ts @@ -0,0 +1,3 @@ +export * from './module'; +export * from './table.component'; +export * from './column.component'; \ No newline at end of file diff --git a/src/components/virtual-table/module.ts b/src/components/virtual-table/module.ts new file mode 100644 index 00000000..51627ee2 --- /dev/null +++ b/src/components/virtual-table/module.ts @@ -0,0 +1,29 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { VirtualScrollModule } from 'angular2-virtual-scroll'; +import { MdlVirtualTableComponent } from './table.component'; +import { MdlVirtualTableColumnComponent } from './column.component'; + + +@NgModule({ + imports: [ + CommonModule, + VirtualScrollModule + ], + exports: [ + MdlVirtualTableComponent, + MdlVirtualTableColumnComponent + ], + declarations: [ + MdlVirtualTableComponent, + MdlVirtualTableColumnComponent + ] +}) +export class MdlVirtualTableModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: MdlVirtualTableModule, + providers: [] + }; + } +} \ No newline at end of file diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts new file mode 100644 index 00000000..7f5db6c2 --- /dev/null +++ b/src/components/virtual-table/table.component.ts @@ -0,0 +1,219 @@ +import { + Component, + Input, + Output, + ChangeDetectorRef, + ChangeDetectionStrategy, + ViewChild, + ContentChildren, + EventEmitter, + OnInit, + HostBinding +} from '@angular/core'; +import {VirtualScrollComponent} from 'angular2-virtual-scroll'; +import {MdlVirtualTableColumnComponent} from './column.component'; + +@Component({ + selector: 'mdl-virtual-table', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+
+
+ {{col.label}}
+
+
+ +
+
+
+ +
+
+
+
+ {{value}} +
+ `, + styles: [` + :host.flex-height, + .flex-height virtual-scroll, + .flex-height.table-container { + display: flex; + } + + .flex-height.table-container { + flex-direction: column; + } + + virtual-scroll { + width: 100%; + display: block; + } + + .table-header { + table-layout: fixed; + border-bottom: 1px solid rgba(0,0,0,.12); + } + .table { + width: 100%; + table-layout: fixed; + } + .row { + display: table-row; + height: 30px; + width: 100%; + } + + .cell { + display: table-cell; + position: relative; + height: 48px; + border-top: 1px solid rgba(0,0,0,.12); + border-bottom: 1px solid rgba(0,0,0,.12); + padding: 12px 18px; + box-sizing: border-box; + vertical-align: middle; + width: 100%; + } + + .row:first-child .cell { + border-top: none; + } + + .table .row { + transition-duration: .28s; + transition-timing-function: cubic-bezier(.4,0,.2,1); + transition-property: background-color; + } + + .table .row:hover { + background-color: #eee; + } + + .header-cell { + position: relative; + vertical-align: bottom; + text-overflow: ellipsis; + font-weight: 700; + line-height: 24px; + letter-spacing: 0; + height: 48px; + font-size: 12px; + color: rgba(0,0,0,.54); + padding-bottom: 8px; + box-sizing: border-box; + display: table-cell; + padding: 0 18px 12px 18px; + text-align: left; + width: 100%; + } + + .header-cell.sortable { + cursor: pointer; + } + .header-cell.sortable:after { + font-family: 'Material Icons'; + font-size: 20pt; + content: 'arrow_drop_down arrow_drop_up'; + vertical-align: middle; + letter-spacing: -11px; + position: absolute; + } + + .header-cell.sortable.desc:after { + content: 'arrow_drop_down'; + } + .header-cell.sortable.asc:after { + content: 'arrow_drop_up'; + } + + `] +}) +export class MdlVirtualTableComponent implements OnInit { + values: any[]; + + @ViewChild(VirtualScrollComponent) + private virtualScroll: VirtualScrollComponent; + + @ContentChildren(MdlVirtualTableColumnComponent) + private columns: MdlVirtualTableColumnComponent; + private lastLoadedElementHash: string; + + @Input() fetchRowCount: Function; + @Input() fetchRowData: Function; + + @Input() maxHeight: string = '100vh'; + + private _isFlexHeight: boolean = false; + @HostBinding('class.flex-height') + @Input('flex-height') + get isFlexHeight() { + return this._isFlexHeight; + } + + set isFlexHeight(value) { + this._isFlexHeight = value != null && "" + value !== 'false'; + } + + private _rowCount: number; + private _visibleRowOffset: number; + + @Output() sort: EventEmitter = new EventEmitter(); + @Output() rowClick: EventEmitter = new EventEmitter(); + + constructor(private cdr: ChangeDetectorRef) { + + } + + ngOnInit() { + this.refresh(); + } + + fetch(offset: number, limit: number) { + let hash = offset + ':' + limit; + if (this.lastLoadedElementHash === hash) { + return; + } + this._visibleRowOffset = offset; + this.fetchRowData(offset, limit).subscribe((rows: any[]) => { + this.values = new Array(this._rowCount); + for (var i = offset; i < (rows.length + offset); i++) { + this.values[i] = rows[i - offset]; + } + this.cdr.markForCheck(); + this.lastLoadedElementHash = hash; + }); + } + + onListChange(event) { + this.fetch(event.start, (event.end - event.start)); + } + + onRowClick(event, row, index) { + this.rowClick.emit({event, row, index}); + } + + + toggleSortByColumn(col: MdlVirtualTableColumnComponent) { + if (!col.sortable) { + return; + } + + col.sortDirection = col.sortDirection === 'asc' ? 'desc' : 'asc'; + this.sort.emit({column: col.field, direction: col.sortDirection}); + } + + refresh() { + + this.fetchRowCount().subscribe((count) => { + this._rowCount = count; + this.values = new Array(count); + this.virtualScroll.previousStart = undefined; + this.virtualScroll.refresh(); + }); + + } +} \ No newline at end of file diff --git a/src/e2e-app/app/app.component.html b/src/e2e-app/app/app.component.html index 44ae9e3e..a6efb8c2 100644 --- a/src/e2e-app/app/app.component.html +++ b/src/e2e-app/app/app.component.html @@ -23,6 +23,7 @@ Select Expansion Panel FAB menu + Virtual Table angular2-mdl explore diff --git a/src/e2e-app/app/app.component.ts b/src/e2e-app/app/app.component.ts index 12cb78b9..fdbc36f4 100644 --- a/src/e2e-app/app/app.component.ts +++ b/src/e2e-app/app/app.component.ts @@ -10,6 +10,7 @@ import { ExpansionPanelDemo } from './expansion-panel/expansion-panel.component' import { MdlLayoutComponent } from '@angular-mdl/core'; import { DatepickerDemo } from './datepicker/datepicker.component'; import {FabMenuDemo} from './fab-menu/fab-menu-demo.component'; +import {VirtualTableDemo} from "./virtual-table/virtual-table.component"; @Component({ @@ -27,6 +28,7 @@ export const appRoutes: Routes = [ { path: 'select', component: SelectDemo, data: {title: 'Select'} }, { path: 'expansion-panel', component: ExpansionPanelDemo, data: {title: 'Expanion Panel'} }, { path: 'fab-menu', component: FabMenuDemo, data: {title: 'FAB Menu'} }, + { path: 'virtual-table', component: VirtualTableDemo, data: {title: 'Virtual Table'} }, { path: '**', redirectTo: '' } ]; diff --git a/src/e2e-app/app/app.module.ts b/src/e2e-app/app/app.module.ts index efdc699b..b26ddf06 100644 --- a/src/e2e-app/app/app.module.ts +++ b/src/e2e-app/app/app.module.ts @@ -17,6 +17,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { DatepickerDemo } from './datepicker/datepicker.component'; import { MdlDatePickerModule } from '../../components/datepicker/index'; import { FabMenuDemo } from './fab-menu/fab-menu-demo.component'; +import { MdlVirtualTableModule} from "../../components/virtual-table/index" +import {VirtualTableDemo} from "./virtual-table/virtual-table.component"; @NgModule({ imports: [ @@ -30,7 +32,8 @@ import { FabMenuDemo } from './fab-menu/fab-menu-demo.component'; MdlFabMenuModule, MdlExpansionPanelModule, BrowserAnimationsModule, - MdlDatePickerModule + MdlDatePickerModule, + MdlVirtualTableModule ], declarations: [ AppComponent, @@ -42,6 +45,7 @@ import { FabMenuDemo } from './fab-menu/fab-menu-demo.component'; MatchSorterPipe, ExpansionPanelDemo, PrismDirective, + VirtualTableDemo ], entryComponents: [AppComponent], bootstrap: [] diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.html b/src/e2e-app/app/virtual-table/virtual-table.component.html new file mode 100644 index 00000000..af1d98cf --- /dev/null +++ b/src/e2e-app/app/virtual-table/virtual-table.component.html @@ -0,0 +1,58 @@ +
+
+

THE VIRTUAL TABLE

+ +

Light weight data table with smooth virtual scrolling.

+ +
Table
+ +
+ + + + +
Title: {{value}}
+
+
+ +
+ +
+  
+    
+    
+      
Title: {value}
+
+ + ]]> +
+ +
+  
+
+
diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.ts b/src/e2e-app/app/virtual-table/virtual-table.component.ts new file mode 100644 index 00000000..3228791f --- /dev/null +++ b/src/e2e-app/app/virtual-table/virtual-table.component.ts @@ -0,0 +1,35 @@ +import { + ChangeDetectionStrategy, + Component +} from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; + +@Component({ + selector: 'virtual-table-demo', + templateUrl: 'virtual-table.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class VirtualTableDemo { + + onSort(data) { + console.log("sort data:", data); + } + + onRowClick(event) { + console.log("on row click", event); + } + + requestRowCount() { + return Observable.of(500).delay(1000);; + } + + requestRowData(offset, limit) { + let rows = []; + for(var i = offset; i < (offset + limit); i++) { + rows.push({_id: i, _label: 'Test ' + i}); + } + + return Observable.of(rows).delay(1000); + } +} From 0d8bce5f95f33debbc4fda37f518aa9bf81e56e2 Mon Sep 17 00:00:00 2001 From: KiefTimo Date: Mon, 4 Sep 2017 15:32:34 +0200 Subject: [PATCH 02/17] (fix) missing types --- src/components/virtual-table/table.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts index 7f5db6c2..e835c59a 100644 --- a/src/components/virtual-table/table.component.ts +++ b/src/components/virtual-table/table.component.ts @@ -10,7 +10,7 @@ import { OnInit, HostBinding } from '@angular/core'; -import {VirtualScrollComponent} from 'angular2-virtual-scroll'; +import {VirtualScrollComponent, ChangeEvent} from 'angular2-virtual-scroll'; import {MdlVirtualTableColumnComponent} from './column.component'; @Component({ @@ -188,11 +188,11 @@ export class MdlVirtualTableComponent implements OnInit { }); } - onListChange(event) { + onListChange(event: ChangeEvent) { this.fetch(event.start, (event.end - event.start)); } - onRowClick(event, row, index) { + onRowClick(event: MouseEvent, row: any, index: number) { this.rowClick.emit({event, row, index}); } @@ -208,7 +208,7 @@ export class MdlVirtualTableComponent implements OnInit { refresh() { - this.fetchRowCount().subscribe((count) => { + this.fetchRowCount().subscribe((count: number) => { this._rowCount = count; this.values = new Array(count); this.virtualScroll.previousStart = undefined; From 572a512233b0922693ca0d7bff3c3363b26d4f30 Mon Sep 17 00:00:00 2001 From: Michael Seemann Date: Mon, 2 Oct 2017 09:53:58 +0200 Subject: [PATCH 03/17] enable tests, added README.md and pakcage.json; made some notes about the code --- README.md | 2 +- src/components/system-config-spec.ts | 3 +- src/components/virtual-table/README.md | 19 ++++++++++++ .../virtual-table/column.component.ts | 1 + src/components/virtual-table/package.json | 28 ++++++++++++++++++ .../virtual-table/table.component.spec.ts | 29 +++++++++++++++++++ .../virtual-table/table.component.ts | 4 +++ tools/gulp/tasks/test.ts | 3 +- 8 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 src/components/virtual-table/README.md create mode 100644 src/components/virtual-table/package.json create mode 100644 src/components/virtual-table/table.component.spec.ts diff --git a/README.md b/README.md index c52eab86..b25a0167 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Additional components for @angular-mdl/core that are not part of material design | fab-menu | [leojpod](https://github.com/leojpod) | a fab menu component | [![npm version](https://badge.fury.io/js/@angular-mdl%2Ffab-menu.svg)](https://www.npmjs.com/package/@angular-mdl/fab-menu)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/fab-menu) | experimental | [demo](http://mseemann.io/angular2-mdl-ext/fab-menu) | popover | [tb](https://github.com/tb) | popover with arbitrary content | [![npm version](https://badge.fury.io/js/%40angular-mdl%2Fpopover.svg)](https://www.npmjs.com/package/@angular-mdl/popover)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/popover) | experimental | [demo](http://mseemann.io/angular2-mdl-ext/popover) | select | [tb](https://github.com/tb) | a select box | [![npm version](https://badge.fury.io/js/%40angular-mdl%2Fselect.svg)](https://www.npmjs.com/package/@angular-mdl/select)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/select) | experimental | [demo](http://mseemann.io/angular2-mdl-ext/select) -| virtual-table | [kmcs](https://github.com/kmcs) | a virtual table | [![npm version](https://badge.fury.io/js/%40angular-mdl%2Fselect.svg)](https://www.npmjs.com/package/@angular-mdl/select)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/select) | proof of concept | [demo](http://mseemann.io/angular2-mdl-ext/virtual-table) +| virtual-table | [kmcs](https://github.com/kmcs) | a virtual table | [![npm version](https://badge.fury.io/js/%40angular-mdl%2Fvirtual-table.svg)](https://www.npmjs.com/package/@angular-mdl/virtual-table)| [readme](https://github.com/mseemann/angular2-mdl-ext/tree/master/src/components/virtual-table) | proof of concept | [demo](http://mseemann.io/angular2-mdl-ext/virtual-table) Status means: diff --git a/src/components/system-config-spec.ts b/src/components/system-config-spec.ts index b8217484..2fccc30e 100644 --- a/src/components/system-config-spec.ts +++ b/src/components/system-config-spec.ts @@ -33,6 +33,7 @@ vendorPackages[`@angular-mdl/core`] = { main: `bundle/core.js` }; vendorPackages['rxjs'] = { main: 'index.js' }; vendorPackages['moment'] = { main: 'min/moment.min.js'}; +vendorPackages['angular2-virtual-scroll'] = { main: 'dist/virtual-scroll.js'}; /** Type declaration for ambient System. */ declare var System: any; @@ -44,7 +45,7 @@ System.config({ '@angular-mdl/core': 'vendor/@angular-mdl/core', 'rxjs': 'vendor/rxjs', 'moment': 'vendor/moment', - 'angular2-virtual-scroll': 'angular2-virtual-scroll' + 'angular2-virtual-scroll': 'vendor/angular2-virtual-scroll' }, packages: vendorPackages }); diff --git a/src/components/virtual-table/README.md b/src/components/virtual-table/README.md new file mode 100644 index 00000000..719a4565 --- /dev/null +++ b/src/components/virtual-table/README.md @@ -0,0 +1,19 @@ +# Virtual-Table + +### Installing + +Install the package and angular2-virtual-scroll! + + npm i --save @angular-mdl/virtual-table + npm i --save angular2-virtual-scroll + +import the MdlVirtualTableModule and add it to your app.module imports: + + import { MdlVirtualTableModule } from '@angular-mdl/virtual-table'; + + +### Usage & API + + Visit [demo] for usage examples and API summary. + [demo]: http://mseemann.io/angular2-mdl-ext/virtual-table + diff --git a/src/components/virtual-table/column.component.ts b/src/components/virtual-table/column.component.ts index 41a3d77a..d5e17211 100644 --- a/src/components/virtual-table/column.component.ts +++ b/src/components/virtual-table/column.component.ts @@ -9,6 +9,7 @@ export class MdlVirtualTableColumnComponent { @Input() field: string; @Input() width: string; @Input() set sortable(v: boolean) { + // TODO why did you compare with 'true'? if the type is boolean it musst be true or false. this._sortable = typeof(v) === 'boolean' ? v : (v === 'true'); } get sortable(): boolean { diff --git a/src/components/virtual-table/package.json b/src/components/virtual-table/package.json new file mode 100644 index 00000000..ce1711aa --- /dev/null +++ b/src/components/virtual-table/package.json @@ -0,0 +1,28 @@ +{ + "name": "@angular-mdl/virtual-table", + "version": "0.0.1", + "description": "Angular Material Design Lite - Virtual Table Component", + "main": "./index.umd.js", + "module": "./index.js", + "typings": "./index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/mseemann/angular2-mdl-ext.git" + }, + "keywords": [ + "angular2", + "angular4", + "angular", + "material design lite", + "virtual table" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/mseemann/angular2-mdl-ext/issues" + }, + "homepage": "https://github.com/mseemann/angular2-mdl-ext#readme", + "peerDependencies": { + "@angular-mdl/core": ">=4.0.0", + "angular2-virtual-scroll": "^0.2.1" + } +} diff --git a/src/components/virtual-table/table.component.spec.ts b/src/components/virtual-table/table.component.spec.ts new file mode 100644 index 00000000..8b22f401 --- /dev/null +++ b/src/components/virtual-table/table.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MdlVirtualTableComponent } from './table.component'; +import { VirtualScrollModule } from 'angular2-virtual-scroll'; + +describe('VirtualTableComponent', () => { + + + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + VirtualScrollModule + ], + declarations: [MdlVirtualTableComponent], + providers: [ + ] + }); + + TestBed.compileComponents().then(() => { + fixture = TestBed.createComponent(MdlVirtualTableComponent); + fixture.detectChanges(); + }); + })); + + it('should instantiate the component', async(() => { + expect(fixture).toBeDefined(); + })); +}); \ No newline at end of file diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts index e835c59a..01ec45b0 100644 --- a/src/components/virtual-table/table.component.ts +++ b/src/components/virtual-table/table.component.ts @@ -207,6 +207,10 @@ export class MdlVirtualTableComponent implements OnInit { } refresh() { + if(!this.fetchRowCount) { + // TODO maybe we should throw an error that tells the developer what is missing. + return; + } this.fetchRowCount().subscribe((count: number) => { this._rowCount = count; diff --git a/tools/gulp/tasks/test.ts b/tools/gulp/tasks/test.ts index a1a7311c..5f9f101b 100644 --- a/tools/gulp/tasks/test.ts +++ b/tools/gulp/tasks/test.ts @@ -15,7 +15,8 @@ gulp.task(':build:test:vendor', () => { 'rxjs', 'systemjs/dist', 'zone.js/dist', - 'moment' + 'moment', + 'angular2-virtual-scroll' ]; return gulpMerge( From 720f5115d9eb94125b54e4307cd65694b41a563a Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Tue, 3 Oct 2017 15:45:58 +0200 Subject: [PATCH 04/17] rewrite of first proof of concept. --- package.json | 2 +- src/components/system-config-spec.ts | 2 +- src/components/virtual-table/package.json | 26 ++ .../virtual-table/table.component.scss | 104 +++++++ .../virtual-table/table.component.ts | 274 +++++++++--------- .../virtual-table.component.html | 72 +++-- .../virtual-table/virtual-table.component.ts | 16 +- 7 files changed, 326 insertions(+), 170 deletions(-) create mode 100644 src/components/virtual-table/package.json create mode 100644 src/components/virtual-table/table.component.scss diff --git a/package.json b/package.json index c601ac90..e870951a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@angular/platform-browser-dynamic": "4.3.6", "@angular/router": "4.3.6", "@mseemann/prism": "0.0.1", - "angular2-virtual-scroll": "^0.2.1", + "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#b70c730", "core-js": "2.5.1", "custom-event-polyfill": "^0.3.0", "match-sorter": "1.8.1", diff --git a/src/components/system-config-spec.ts b/src/components/system-config-spec.ts index b8217484..141f65ea 100644 --- a/src/components/system-config-spec.ts +++ b/src/components/system-config-spec.ts @@ -44,7 +44,7 @@ System.config({ '@angular-mdl/core': 'vendor/@angular-mdl/core', 'rxjs': 'vendor/rxjs', 'moment': 'vendor/moment', - 'angular2-virtual-scroll': 'angular2-virtual-scroll' + 'angular2-virtual-scroll': 'vendor/angular2-virtual-scroll' }, packages: vendorPackages }); diff --git a/src/components/virtual-table/package.json b/src/components/virtual-table/package.json new file mode 100644 index 00000000..6e0fb836 --- /dev/null +++ b/src/components/virtual-table/package.json @@ -0,0 +1,26 @@ +{ + "name": "@angular-mdl/virtual-table", + "version": "0.11.1", + "description": "Angular 2 Material Design Lite - Select Component", + "main": "./index.umd.js", + "module": "./index.js", + "typings": "./index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/mseemann/angular2-mdl-ext.git" + }, + "keywords": [ + "angular2", + "material design lite", + "select" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/mseemann/angular2-mdl-ext/issues" + }, + "homepage": "https://github.com/mseemann/angular2-mdl-ext#readme", + "peerDependencies": { + "@angular-mdl/core": ">=4.0.0", + "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#b70c730" + } +} diff --git a/src/components/virtual-table/table.component.scss b/src/components/virtual-table/table.component.scss new file mode 100644 index 00000000..2f9750ac --- /dev/null +++ b/src/components/virtual-table/table.component.scss @@ -0,0 +1,104 @@ +:host.flex-height, +.flex-height virtual-scroll, +.flex-height.table-container { + display: flex; +} + +.flex-height.table-container { + flex-direction: column; +} + +.table-container { + width: 100%; +} +.scrollbar-space { + padding-right: 16px; +} + +virtual-scroll { + width: 100%; + display: block; +} + +.table-header { + table-layout: fixed; + border-bottom: 1px solid rgba(0,0,0,.12); + display: table; + width: 100%; +} +.table { + width: 100%; + table-layout: fixed; + display: table; +} +.table-row { + display: table-row; + height: 30px; + width: 100%; +} + +.table-cell { + display: table-cell; + position: relative; + height: 48px; + border-top: 1px solid rgba(0,0,0,.12); + border-bottom: 1px solid rgba(0,0,0,.12); + padding: 12px 18px; + box-sizing: border-box; + vertical-align: middle; + width: 100%; + overflow: hidden; + text-overflow:ellipsis; + white-space:nowrap +} + +.table-row:first-child .cell { + border-top: none; +} + +.table .table-row { + transition-duration: .28s; + transition-timing-function: cubic-bezier(.4,0,.2,1); + transition-property: background-color; +} + +.table .table-row:hover { + background-color: #eee; +} + +.header-cell { + position: relative; + vertical-align: bottom; + text-overflow: ellipsis; + font-weight: 700; + line-height: 24px; + letter-spacing: 0; + height: 48px; + font-size: 12px; + color: rgba(0,0,0,.54); + padding-bottom: 8px; + box-sizing: border-box; + display: table-cell; + padding: 0 18px 12px 18px; + text-align: left; + width: 100%; +} + +.header-cell.sortable { + cursor: pointer; +} +.header-cell.sortable:after { + font-family: 'Material Icons'; + font-size: 20pt; + content: 'arrow_drop_down arrow_drop_up'; + vertical-align: middle; + letter-spacing: -11px; + position: absolute; +} + +.header-cell.sortable.desc:after { + content: 'arrow_drop_down'; +} +.header-cell.sortable.asc:after { + content: 'arrow_drop_up'; +} diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts index e835c59a..29e3d079 100644 --- a/src/components/virtual-table/table.component.ts +++ b/src/components/virtual-table/table.component.ts @@ -8,27 +8,42 @@ import { ContentChildren, EventEmitter, OnInit, - HostBinding + OnChanges, + OnDestroy, + AfterViewChecked, + SimpleChanges, + HostBinding, + QueryList, + ElementRef } from '@angular/core'; import {VirtualScrollComponent, ChangeEvent} from 'angular2-virtual-scroll'; import {MdlVirtualTableColumnComponent} from './column.component'; +import {Observable, Subject} from 'rxjs'; +import 'rxjs/add/operator/withLatestFrom'; + +declare var IntersectionObserver: any; @Component({ selector: 'mdl-virtual-table', - changeDetection: ChangeDetectionStrategy.OnPush, + changeDetection: ChangeDetectionStrategy.Default, template: ` -
-
-
-
- {{col.label}}
-
+
+
+
+
+
+ {{col.label}}
+
+
- +
-
-
+
+
@@ -37,115 +52,26 @@ import {MdlVirtualTableColumnComponent} from './column.component'; {{value}}
`, - styles: [` - :host.flex-height, - .flex-height virtual-scroll, - .flex-height.table-container { - display: flex; - } - - .flex-height.table-container { - flex-direction: column; - } - - virtual-scroll { - width: 100%; - display: block; - } - - .table-header { - table-layout: fixed; - border-bottom: 1px solid rgba(0,0,0,.12); - } - .table { - width: 100%; - table-layout: fixed; - } - .row { - display: table-row; - height: 30px; - width: 100%; - } - - .cell { - display: table-cell; - position: relative; - height: 48px; - border-top: 1px solid rgba(0,0,0,.12); - border-bottom: 1px solid rgba(0,0,0,.12); - padding: 12px 18px; - box-sizing: border-box; - vertical-align: middle; - width: 100%; - } - - .row:first-child .cell { - border-top: none; - } - - .table .row { - transition-duration: .28s; - transition-timing-function: cubic-bezier(.4,0,.2,1); - transition-property: background-color; - } - - .table .row:hover { - background-color: #eee; - } - - .header-cell { - position: relative; - vertical-align: bottom; - text-overflow: ellipsis; - font-weight: 700; - line-height: 24px; - letter-spacing: 0; - height: 48px; - font-size: 12px; - color: rgba(0,0,0,.54); - padding-bottom: 8px; - box-sizing: border-box; - display: table-cell; - padding: 0 18px 12px 18px; - text-align: left; - width: 100%; - } - - .header-cell.sortable { - cursor: pointer; - } - .header-cell.sortable:after { - font-family: 'Material Icons'; - font-size: 20pt; - content: 'arrow_drop_down arrow_drop_up'; - vertical-align: middle; - letter-spacing: -11px; - position: absolute; - } - - .header-cell.sortable.desc:after { - content: 'arrow_drop_down'; - } - .header-cell.sortable.asc:after { - content: 'arrow_drop_up'; - } - - `] + styleUrls: ['table.component.scss'] }) -export class MdlVirtualTableComponent implements OnInit { +export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { values: any[]; @ViewChild(VirtualScrollComponent) private virtualScroll: VirtualScrollComponent; + + @ViewChild('tableWrapper') + private virutalScrollElement: ElementRef; @ContentChildren(MdlVirtualTableColumnComponent) - private columns: MdlVirtualTableColumnComponent; + columns: QueryList; private lastLoadedElementHash: string; - @Input() fetchRowCount: Function; - @Input() fetchRowData: Function; + @Input() rowCountStream: Observable; + @Input() rowDataStream: Observable<{rows: any[], offset: number, limit: number}>; @Input() maxHeight: string = '100vh'; + @Input() minHeight: string; private _isFlexHeight: boolean = false; @HostBinding('class.flex-height') @@ -160,36 +86,100 @@ export class MdlVirtualTableComponent implements OnInit { private _rowCount: number; private _visibleRowOffset: number; + private _nativeElementObserver: any; - @Output() sort: EventEmitter = new EventEmitter(); - @Output() rowClick: EventEmitter = new EventEmitter(); - - constructor(private cdr: ChangeDetectorRef) { + @Output() sort: EventEmitter; + @Output() rowClick: EventEmitter; + + private requestRowCountSubject: Subject; + private requestRowDataSubject: Subject; + + @Output() rowCountRequest: EventEmitter; + @Output() rowDataRequest: EventEmitter<{offset: number, limit: number}>; - } + rows: any[]; - ngOnInit() { - this.refresh(); + constructor(private cdr: ChangeDetectorRef) { + this.sort = new EventEmitter(); + this.rowClick = new EventEmitter(); + this.rowCountRequest = new EventEmitter(); + this.rowDataRequest = new EventEmitter(); + this.requestRowCountSubject = new Subject(); + this.requestRowDataSubject = new Subject(); + this.requestRowCountSubject.asObservable().subscribe(() => { + this.rowCountRequest.emit(); + }); + this.requestRowDataSubject.asObservable().distinctUntilChanged((x: any, y: any) => { + return (x.offset === y.offset && x.limit === y.limit) && !x.refresh && !y.refresh; + }).subscribe((result) => { + this.rowDataRequest.emit(result); + }); + } + + ngOnInit() { + this.refresh(true); } - - fetch(offset: number, limit: number) { - let hash = offset + ':' + limit; - if (this.lastLoadedElementHash === hash) { + + ngAfterViewChecked() { + if(!this._nativeElementObserver && this.virutalScrollElement.nativeElement) { + this._nativeElementObserver = new IntersectionObserver((entries: any[]) => { + + if(!(entries.shift().intersectionRatio > 0)) { return; - } - this._visibleRowOffset = offset; - this.fetchRowData(offset, limit).subscribe((rows: any[]) => { - this.values = new Array(this._rowCount); - for (var i = offset; i < (rows.length + offset); i++) { - this.values[i] = rows[i - offset]; + } + if(this.virtualScroll.startupLoop) { + return; + } + this.virtualScroll.startupLoop = true; + this.virtualScroll.refresh(); + + }, { + rootMargin: '0px', + threshold: 1.0 + }); + + this._nativeElementObserver.observe(this.virutalScrollElement.nativeElement); + } + } + + ngOnDestroy() { + if(this._nativeElementObserver) { + this._nativeElementObserver.disconnect(); + } + } + + ngOnChanges(changes: SimpleChanges) { + if((changes).rowDataStream && (changes).rowDataStream.currentValue) { + let lastRowDataSubscription = this.rowDataStream.subscribe((result) => { + this._visibleRowOffset = result.offset; + this._rowCount = this._rowCount || result.rows.length; + let values = new Array(this._rowCount); + for (var i = result.offset; i < (result.rows.length + result.offset); i++) { + values[i] = result.rows[i - result.offset]; } + this.values = values; + this.virtualScroll.previousStart = undefined; + this.virtualScroll.previousEnd = undefined; + this.virtualScroll.refresh(); this.cdr.markForCheck(); - this.lastLoadedElementHash = hash; - }); + }); + } + if((changes).rowCountStream && (changes).rowCountStream.currentValue) { + this.rowCountStream.subscribe((count) => { + this._rowCount = count; + this.values = new Array(count); + this.virtualScroll.previousStart = undefined; + this.virtualScroll.previousEnd = undefined; + this.virtualScroll.refresh(); + this.cdr.markForCheck(); + }); + } + } onListChange(event: ChangeEvent) { - this.fetch(event.start, (event.end - event.start)); + let limit = event.end - event.start; + this.requestRowDataSubject.next({offset: event.start, limit}); } onRowClick(event: MouseEvent, row: any, index: number) { @@ -206,14 +196,20 @@ export class MdlVirtualTableComponent implements OnInit { this.sort.emit({column: col.field, direction: col.sortDirection}); } - refresh() { - - this.fetchRowCount().subscribe((count: number) => { - this._rowCount = count; - this.values = new Array(count); - this.virtualScroll.previousStart = undefined; - this.virtualScroll.refresh(); + refresh(withRowCount: boolean = false) { + if(withRowCount) { + this.requestRowCountSubject.next(); + } + if(typeof(this.virtualScroll.previousStart) === 'number' + && typeof(this.virtualScroll.previousEnd) === 'number') { + this.requestRowDataSubject.next({ + refresh: true, + offset: this.virtualScroll.previousStart, + limit: this.virtualScroll.previousEnd - this.virtualScroll.previousStart }); - + this.virtualScroll.previousEnd = undefined; + this.virtualScroll.previousStart = undefined; + } + this.virtualScroll.refresh(); } -} \ No newline at end of file +} diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.html b/src/e2e-app/app/virtual-table/virtual-table.component.html index af1d98cf..f28cc6f0 100644 --- a/src/e2e-app/app/virtual-table/virtual-table.component.html +++ b/src/e2e-app/app/virtual-table/virtual-table.component.html @@ -8,7 +8,10 @@
Table
- +
Title: {{value}}
@@ -19,40 +22,55 @@
Table
   
+  
     
     
-      
Title: {value}
+
Title: {{value}}
-
+ ]]>
   ;
+    private rowDataStream: Observable<{rows: any[], offset: number, limit: number}>;
+  
+      onRowCountRequest() {
+          console.log("on row count request");
+          this.rowCountStream = this.requestRowCount();
+      }
+  
+      onRowDataRequest(request) {
+          console.log("on row data request");
+          this.rowDataStream = this.requestRowData(request.offset, request.limit);
+      }
+  
+      onSort(data) {
+          console.log("sort data:", data);
+      }
+  
+      onRowClick(event) {
+          console.log("on row click", event);
+      }
+  
+      requestRowCount() {
+          return Observable.of(500).delay(1000);;
+      }
+  
+      requestRowData(offset, limit) {
+          let rows = [];
+          for(var i = offset; i < (offset + limit); i++) {
+              rows.push({_id: i, _label: 'Test ' + i});
+          }
+          return Observable.of({rows, offset, limit}).delay(1000);
+      }
+  }
   ]]>
 
 
diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.ts b/src/e2e-app/app/virtual-table/virtual-table.component.ts index 3228791f..80fdb0bb 100644 --- a/src/e2e-app/app/virtual-table/virtual-table.component.ts +++ b/src/e2e-app/app/virtual-table/virtual-table.component.ts @@ -11,6 +11,19 @@ import 'rxjs/add/observable/of'; changeDetection: ChangeDetectionStrategy.OnPush }) export class VirtualTableDemo { + + private rowCountStream: Observable; + private rowDataStream: Observable<{rows: any[], offset: number, limit: number}>; + + onRowCountRequest() { + console.log("on row count request"); + this.rowCountStream = this.requestRowCount(); + } + + onRowDataRequest(request) { + console.log("on row data request"); + this.rowDataStream = this.requestRowData(request.offset, request.limit); + } onSort(data) { console.log("sort data:", data); @@ -29,7 +42,6 @@ export class VirtualTableDemo { for(var i = offset; i < (offset + limit); i++) { rows.push({_id: i, _label: 'Test ' + i}); } - - return Observable.of(rows).delay(1000); + return Observable.of({rows, offset, limit}).delay(1000); } } From 0a8f83ff04c91bc693c163de781baf67c19f161e Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Tue, 3 Oct 2017 16:18:13 +0200 Subject: [PATCH 05/17] added browser requirements hint --- src/components/virtual-table/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/virtual-table/README.md b/src/components/virtual-table/README.md index 719a4565..24d66af7 100644 --- a/src/components/virtual-table/README.md +++ b/src/components/virtual-table/README.md @@ -11,6 +11,9 @@ import the MdlVirtualTableModule and add it to your app.module imports: import { MdlVirtualTableModule } from '@angular-mdl/virtual-table'; +### Browser Requirements + +The IntersectionObserver is used for any table resize occurenc! Please be aware of using a modern browser ((see here for implementation status)[https://github.com/w3c/IntersectionObserver]) or use the official [polyfill](https://github.com/w3c/IntersectionObserver/tree/master/polyfill). ### Usage & API From 556bfd16ca6b36f9d7977e1204ad144de395e56a Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Tue, 10 Oct 2017 17:14:06 +0200 Subject: [PATCH 06/17] change single moduleId replacement --- config/webpack/webpack.common.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 5a89b408..c85b9620 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -29,9 +29,8 @@ module.exports = { test: /.ts$/, loader: 'string-replace-loader', query: { - multiple: [ - {search: new RegExp('moduleId: module.id,', 'g'), replace: ''} - ] + search: new RegExp('moduleId: module.id,', 'g'), + replace: '' } }, { From 125492eade18a63aed5b32e1dbe5ab70b146200b Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Tue, 10 Oct 2017 17:21:49 +0200 Subject: [PATCH 07/17] (fix) initial tests --- .../virtual-table/table.component.scss | 1 - .../virtual-table/table.component.spec.ts | 129 ++++++++++++++++-- .../virtual-table/table.component.ts | 23 +++- 3 files changed, 139 insertions(+), 14 deletions(-) diff --git a/src/components/virtual-table/table.component.scss b/src/components/virtual-table/table.component.scss index 2f9750ac..e1961845 100644 --- a/src/components/virtual-table/table.component.scss +++ b/src/components/virtual-table/table.component.scss @@ -33,7 +33,6 @@ virtual-scroll { } .table-row { display: table-row; - height: 30px; width: 100%; } diff --git a/src/components/virtual-table/table.component.spec.ts b/src/components/virtual-table/table.component.spec.ts index 8b22f401..94342dc4 100644 --- a/src/components/virtual-table/table.component.spec.ts +++ b/src/components/virtual-table/table.component.spec.ts @@ -1,29 +1,142 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { MdlVirtualTableComponent } from './table.component'; -import { VirtualScrollModule } from 'angular2-virtual-scroll'; +import { async, fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import {Component, DebugElement, ViewChild} from '@angular/core' +import { MdlVirtualTableComponent, MdlVirtualTableModule } from './'; +import { VirtualScrollModule, VirtualScrollComponent } from 'angular2-virtual-scroll'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/delay'; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000000; describe('VirtualTableComponent', () => { - let fixture: ComponentFixture; + let fixture: ComponentFixture; + + //generate rows + let rows:any[] = []; + for(var i = 0; i < 1000; i++) { + rows.push({_index: i, _label: 'Test ' + i}); + } beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - VirtualScrollModule + MdlVirtualTableModule.forRoot() ], - declarations: [MdlVirtualTableComponent], + declarations: [TestMdlVirtualTableComponent], providers: [ ] }); TestBed.compileComponents().then(() => { - fixture = TestBed.createComponent(MdlVirtualTableComponent); + fixture = TestBed.createComponent(TestMdlVirtualTableComponent); fixture.detectChanges(); }); })); + afterEach(() => { + fixture = null + }); + it('should instantiate the component', async(() => { expect(fixture).toBeDefined(); })); -}); \ No newline at end of file + + it('should load row count', async(() => { + let testInstance = fixture.componentInstance; + let debugComponent = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)); + + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + expect(debugComponent.componentInstance.values.length).toBe(rows.length, 'Rows length should be 1000'); + }); + + })); + + it('should load viewport rows', ((done) => { + let testInstance = fixture.componentInstance; + + let componentInstance:MdlVirtualTableComponent = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)).componentInstance; + let debugVirtualScroll = fixture.debugElement.query(By.directive(VirtualScrollComponent)); + let componentVirtualScroll: VirtualScrollComponent = debugVirtualScroll.componentInstance; + + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + //simulate scroll + var scrollElement = componentVirtualScroll.parentScroll instanceof Window ? document.body : componentVirtualScroll.parentScroll || debugVirtualScroll.nativeElement; + var fixedRowHeight = 48; //@see table.component.scss + var scrollIndex = 500; + scrollElement.scrollTop = (fixedRowHeight + 1) * scrollIndex; + componentVirtualScroll.refresh(); + setTimeout(() => { + expect(componentInstance.values[500]).toBe(rows[500], 'Row index 500 should be loaded'); + done(); + }, 20); //wait for requestAnimationFrame + }); + })); + + +}); + +@Component({ + template: ` +
+ + + +
Title: {{value}}
+
+
+
+ ` + }) + class TestMdlVirtualTableComponent { + + @ViewChild('table') table: MdlVirtualTableComponent; + + private rows: any[]; + + public rowCountStream: Observable; + public rowDataStream: Observable<{rows: any[], offset: number, limit: number}>; + + onSort(data: {column: string, direction: string}) { + console.log("sort data:", data); + } + + onRowClick(event: any) { + console.log("on row click", event); + } + + requestRowCount() { + return Observable.of(1000); + } + + onRowCountRequest() { + this.rowCountStream = this.requestRowCount(); + } + + onRowDataRequest(request: {offset: number, limit: number, refresh?:boolean}) { + this.rowDataStream = this.requestRowData(request.offset, request.limit); + } + + requestRowData(offset: number, limit: number) { + if(!this.rows) { + return Observable.of({rows: [], offset: 0, limit: 0}); + } + let items = this.rows.slice(offset, offset + limit); + return Observable.of({rows: items, offset, limit}); + } + + initData(rows: any[]) { + this.rows = rows; + this.table.refresh(true); + } + } \ No newline at end of file diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts index 1efea9c8..c43747e8 100644 --- a/src/components/virtual-table/table.component.ts +++ b/src/components/virtual-table/table.component.ts @@ -18,12 +18,15 @@ import { } from '@angular/core'; import {VirtualScrollComponent, ChangeEvent} from 'angular2-virtual-scroll'; import {MdlVirtualTableColumnComponent} from './column.component'; -import {Observable, Subject} from 'rxjs'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; import 'rxjs/add/operator/withLatestFrom'; +import 'rxjs/add/operator/distinctUntilChanged'; declare var IntersectionObserver: any; @Component({ + moduleId: module.id, selector: 'mdl-virtual-table', changeDetection: ChangeDetectionStrategy.Default, template: ` @@ -76,14 +79,22 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe private _isFlexHeight: boolean = false; @HostBinding('class.flex-height') @Input('flex-height') - get isFlexHeight() { + get isFlexHeight(): boolean { return this._isFlexHeight; } - set isFlexHeight(value) { this._isFlexHeight = value != null && "" + value !== 'false'; } + private _isInitialLoadDisabled: boolean = false; + @Input('init-refresh-disabled') + get isInitialLoadDisabled(): boolean { + return this._isInitialLoadDisabled; + } + set isInitialLoadDisabled(value) { + this._isInitialLoadDisabled = value != null && "" + value !== 'false'; + } + private _rowCount: number; private _visibleRowOffset: number; private _nativeElementObserver: any; @@ -95,7 +106,7 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe private requestRowDataSubject: Subject; @Output() rowCountRequest: EventEmitter; - @Output() rowDataRequest: EventEmitter<{offset: number, limit: number}>; + @Output() rowDataRequest: EventEmitter<{offset: number, limit: number, refresh?: boolean}>; rows: any[]; @@ -116,8 +127,10 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe }); } - ngOnInit() { + ngOnInit() { + if(!this.isInitialLoadDisabled) { this.refresh(true); + } } ngAfterViewChecked() { From dbb69c40b1c4002c5bd3a9c99efdd94720aea1e9 Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Tue, 10 Oct 2017 17:32:02 +0200 Subject: [PATCH 08/17] (fix) sortable TODO, if attribute sortable is present, sortable is now true --- src/components/virtual-table/column.component.ts | 3 +-- src/components/virtual-table/table.component.spec.ts | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/virtual-table/column.component.ts b/src/components/virtual-table/column.component.ts index d5e17211..f846421c 100644 --- a/src/components/virtual-table/column.component.ts +++ b/src/components/virtual-table/column.component.ts @@ -9,8 +9,7 @@ export class MdlVirtualTableColumnComponent { @Input() field: string; @Input() width: string; @Input() set sortable(v: boolean) { - // TODO why did you compare with 'true'? if the type is boolean it musst be true or false. - this._sortable = typeof(v) === 'boolean' ? v : (v === 'true'); + this._sortable = v !== null && "" + v !== 'false'; } get sortable(): boolean { return this._sortable; diff --git a/src/components/virtual-table/table.component.spec.ts b/src/components/virtual-table/table.component.spec.ts index 94342dc4..b19e64f8 100644 --- a/src/components/virtual-table/table.component.spec.ts +++ b/src/components/virtual-table/table.component.spec.ts @@ -78,8 +78,6 @@ describe('VirtualTableComponent', () => { }, 20); //wait for requestAnimationFrame }); })); - - }); @Component({ @@ -90,7 +88,7 @@ describe('VirtualTableComponent', () => { [rowDataStream]="rowDataStream" [rowCountStream]="rowCountStream" (rowCountRequest)="onRowCountRequest()" (rowDataRequest)="onRowDataRequest($event)" (sort)="onSort($event)" (rowClick)="onRowClick($event)"> - +
Title: {{value}}
From 1e72a45316718486319c3fd1853afffc3fffc25a Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Fri, 13 Oct 2017 19:55:13 +0200 Subject: [PATCH 09/17] add some more tests for virtual-table component --- .../virtual-table/table.component.spec.ts | 204 +++++++++++++++++- 1 file changed, 197 insertions(+), 7 deletions(-) diff --git a/src/components/virtual-table/table.component.spec.ts b/src/components/virtual-table/table.component.spec.ts index b19e64f8..83ba91d7 100644 --- a/src/components/virtual-table/table.component.spec.ts +++ b/src/components/virtual-table/table.component.spec.ts @@ -1,7 +1,7 @@ import { async, fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import {Component, DebugElement, ViewChild} from '@angular/core' -import { MdlVirtualTableComponent, MdlVirtualTableModule } from './'; +import { MdlVirtualTableComponent, MdlVirtualTableModule, MdlVirtualTableColumnComponent } from './'; import { VirtualScrollModule, VirtualScrollComponent } from 'angular2-virtual-scroll'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; @@ -25,7 +25,10 @@ describe('VirtualTableComponent', () => { imports: [ MdlVirtualTableModule.forRoot() ], - declarations: [TestMdlVirtualTableComponent], + declarations: [TestMdlVirtualTableComponent, + TestMdlVirtualTableComponentWithAutoInit, + TestMdlVirtualTableComponentWithoutRowCountRequest, + TestMdlVirtualTableComponentWithoutRowDataRequest], providers: [ ] }); @@ -78,13 +81,130 @@ describe('VirtualTableComponent', () => { }, 20); //wait for requestAnimationFrame }); })); + + it('should sort columns with sortable attribute', async(() => { + let testInstance = fixture.componentInstance; + let debugTableInstance = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)); + let componentInstance: MdlVirtualTableComponent = debugTableInstance. + componentInstance; + + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + let sortSpy = spyOn(testInstance, 'onSort').and.callThrough(); + + let columnElements = debugTableInstance.nativeElement.querySelectorAll("div.table-header div.header-cell"); + columnElements[0].click(); + expect(testInstance.onSort).toHaveBeenCalledWith({column: '_index', direction: 'asc'}); + sortSpy.calls.reset(); + columnElements[0].click(); + expect(testInstance.onSort).toHaveBeenCalledWith({column: '_index', direction: 'desc'}); + }); + })); + + it('should not sort columns without sortable attribute', async(() => { + let testInstance = fixture.componentInstance; + let debugTableInstance = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)); + let componentInstance: MdlVirtualTableComponent = debugTableInstance. + componentInstance; + + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + spyOn(testInstance, 'onSort').and.callThrough(); + + let columnElements = debugTableInstance.nativeElement.querySelectorAll("div.table-header div.header-cell"); + columnElements[1].click(); + expect(testInstance.onSort).not.toHaveBeenCalled(); + }); + })); + + it('should emit event on click on a row', async(() => { + let testInstance = fixture.componentInstance; + let debugTableInstance = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)); + let componentInstance: MdlVirtualTableComponent = debugTableInstance. + componentInstance; + + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + spyOn(testInstance, 'onRowClick').and.callThrough(); + + let visibleRows = debugTableInstance.nativeElement.querySelectorAll("div.table div.table-row"); + visibleRows[0].click(); + expect(testInstance.onRowClick).toHaveBeenCalled(); + }); + })); + + it('should throw error on missing rowCountRequest listener', async(() => { + TestBed.compileComponents().then(() => { + let fixtureWrongConfig: ComponentFixture = TestBed.createComponent(TestMdlVirtualTableComponentWithoutRowCountRequest); + fixtureWrongConfig.detectChanges(); + expect(() => fixtureWrongConfig.componentInstance.refresh()).toThrowError('mdl-virtual-table component has no rowCountRequest Listener'); + }); + + })); + + it('should throw error on missing rowDataRequest listener', async(() => { + TestBed.compileComponents().then(() => { + let fixtureTest: ComponentFixture = TestBed.createComponent(TestMdlVirtualTableComponentWithoutRowDataRequest); + fixtureTest.detectChanges(); + expect(() => fixtureTest.componentInstance.refresh()).toThrowError('mdl-virtual-table component has no rowDataRequest Listener'); + }); + })); + + + it('should load with auto initial load data', fakeAsync(() => { + let fixtureAutoInit: ComponentFixture; + TestBed.compileComponents().then(() => { + fixtureAutoInit = TestBed.createComponent(TestMdlVirtualTableComponentWithAutoInit); + + }); + tick(); + let debugComponent = fixtureAutoInit.debugElement.query(By.directive(MdlVirtualTableComponent)); + expect(fixtureAutoInit).toBeDefined(); + fixtureAutoInit.componentInstance.initData(rows); + fixtureAutoInit.autoDetectChanges(); + fixtureAutoInit.whenStable().then(() => { + expect(debugComponent.componentInstance.values.length).toBe(rows.length, 'Rows length should be 1000'); + }); + tick(); + })); + + it('should refresh data if table becomes visible', (done) => { + let testInstance = fixture.componentInstance; + + let debugComponent = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)); + let componentInstance:MdlVirtualTableComponent = debugComponent.componentInstance; + let debugVirtualScroll = fixture.debugElement.query(By.directive(VirtualScrollComponent)); + + testInstance.hideTable(); + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + spyOn(debugVirtualScroll.componentInstance, 'refresh'); + + testInstance.showTable(); + fixture.detectChanges(); + setTimeout(() => { + expect(debugVirtualScroll.componentInstance.refresh).toHaveBeenCalled(); + done(); + }, 10); // IntersectionObserver delay + }); + }); + + }); @Component({ template: `
@@ -99,9 +219,11 @@ describe('VirtualTableComponent', () => { class TestMdlVirtualTableComponent { @ViewChild('table') table: MdlVirtualTableComponent; - - private rows: any[]; - + + protected rows: any[]; + + public visible: boolean = true; + public rowCountStream: Observable; public rowDataStream: Observable<{rows: any[], offset: number, limit: number}>; @@ -137,4 +259,72 @@ describe('VirtualTableComponent', () => { this.rows = rows; this.table.refresh(true); } - } \ No newline at end of file + + hideTable() { + this.visible = false; + } + + showTable() { + this.visible = true; + } + } + + @Component({ + template: ` +
+ + + +
Title: {{value}}
+
+
+
+ ` + }) + class TestMdlVirtualTableComponentWithoutRowCountRequest extends TestMdlVirtualTableComponent { + + refresh() { + this.table.refresh(); + } + } + +@Component({ + template: ` +
+ + + +
Title: {{value}}
+
+
+
+ ` +}) +class TestMdlVirtualTableComponentWithoutRowDataRequest extends TestMdlVirtualTableComponent { + refresh() { + this.table.refresh(); + } +} + +@Component({ + template: ` +
+ + + +
Title: {{value}}
+
+
+
+` +}) + class TestMdlVirtualTableComponentWithAutoInit extends TestMdlVirtualTableComponent { + + } \ No newline at end of file From 094df91028f08c7a73cb8fdd1f64f3cb2a403f06 Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Fri, 13 Oct 2017 22:30:39 +0200 Subject: [PATCH 10/17] add directive to disable IntersectionObserver --- src/components/virtual-table/README.md | 5 ++++- src/components/virtual-table/table.component.ts | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/virtual-table/README.md b/src/components/virtual-table/README.md index 24d66af7..8874d10c 100644 --- a/src/components/virtual-table/README.md +++ b/src/components/virtual-table/README.md @@ -5,7 +5,8 @@ Install the package and angular2-virtual-scroll! npm i --save @angular-mdl/virtual-table - npm i --save angular2-virtual-scroll + +Please use the angular2-virtual-scroll from the peer dependency reference! import the MdlVirtualTableModule and add it to your app.module imports: @@ -15,6 +16,8 @@ import the MdlVirtualTableModule and add it to your app.module imports: The IntersectionObserver is used for any table resize occurenc! Please be aware of using a modern browser ((see here for implementation status)[https://github.com/w3c/IntersectionObserver]) or use the official [polyfill](https://github.com/w3c/IntersectionObserver/tree/master/polyfill). +You can disable the IntersectionObserver by adding the directive intersection-observer-disabled. Be aware if you are using e.g. the table within @angular-mdl/core tabs component and the virtual table is inital hidden! + ### Usage & API Visit [demo] for usage examples and API summary. diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts index c43747e8..46f7aeae 100644 --- a/src/components/virtual-table/table.component.ts +++ b/src/components/virtual-table/table.component.ts @@ -95,6 +95,15 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe this._isInitialLoadDisabled = value != null && "" + value !== 'false'; } + private _isIntersectionObserverDisabled: boolean = false; + @Input('intersection-observer-disabled') + get isIntersectionObserverDisabled(): boolean { + return this._isIntersectionObserverDisabled; + } + set isIntersectionObserverDisabled(value) { + this._isIntersectionObserverDisabled = value != null && "" + value !== 'false'; + } + private _rowCount: number; private _visibleRowOffset: number; private _nativeElementObserver: any; @@ -133,8 +142,9 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe } } - ngAfterViewChecked() { - if(!this._nativeElementObserver && this.virutalScrollElement.nativeElement) { + ngAfterViewChecked() { + if(!this._nativeElementObserver && this.virutalScrollElement.nativeElement + && !this.isIntersectionObserverDisabled) { this._nativeElementObserver = new IntersectionObserver((entries: any[]) => { if(!(entries.shift().intersectionRatio > 0)) { From b341b22f986135390077b8ab6815cf0bab2f6420 Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Fri, 13 Oct 2017 22:31:37 +0200 Subject: [PATCH 11/17] add tests for refresh and disabled intersection observer --- .../virtual-table/table.component.spec.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/virtual-table/table.component.spec.ts b/src/components/virtual-table/table.component.spec.ts index 83ba91d7..c34c4c55 100644 --- a/src/components/virtual-table/table.component.spec.ts +++ b/src/components/virtual-table/table.component.spec.ts @@ -7,8 +7,6 @@ import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/delay'; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000000; - describe('VirtualTableComponent', () => { @@ -195,7 +193,23 @@ describe('VirtualTableComponent', () => { done(); }, 10); // IntersectionObserver delay }); - }); + }); + + it('should refresh table on refresh call', async(() => { + let testInstance = fixture.componentInstance; + let debugTableInstance = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)); + let componentInstance: MdlVirtualTableComponent = debugTableInstance. + componentInstance; + + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + spyOn(testInstance, 'onRowDataRequest').and.callThrough(); + componentInstance.refresh(); + expect(testInstance.onRowDataRequest).toHaveBeenCalled(); + }); + })); }); @@ -316,6 +330,7 @@ class TestMdlVirtualTableComponentWithoutRowDataRequest extends TestMdlVirtualTa
From 6167f7fd9ba843ea76284d5861c13e0d1fae982b Mon Sep 17 00:00:00 2001 From: Timo Kiefer Date: Wed, 1 Nov 2017 22:02:35 +0100 Subject: [PATCH 12/17] (fix) wrong reference to virtual-table fork --- package.json | 2 +- src/components/virtual-table/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ad851bf4..c82e5492 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@angular/platform-browser-dynamic": "4.4.4", "@angular/router": "4.4.4", "@mseemann/prism": "0.0.1", - "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#b70c730", + "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#a83b922", "core-js": "2.5.1", "custom-event-polyfill": "^0.3.0", "match-sorter": "2.0.0", diff --git a/src/components/virtual-table/package.json b/src/components/virtual-table/package.json index 8f6b0bd6..200ca9c7 100644 --- a/src/components/virtual-table/package.json +++ b/src/components/virtual-table/package.json @@ -23,6 +23,6 @@ "homepage": "https://github.com/mseemann/angular2-mdl-ext#readme", "peerDependencies": { "@angular-mdl/core": ">=4.0.0", - "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#b70c730" + "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#a83b922" } } From e4e6b725ca15569bca13289689eea0dd681a09e6 Mon Sep 17 00:00:00 2001 From: tk Date: Thu, 12 Jul 2018 17:13:38 +0200 Subject: [PATCH 13/17] added karam-viewport testing module --- config/tests/karma.conf.js | 25 ++++++++++++++++++++++--- package.json | 1 + src/components/tsconfig-spec.json | 3 ++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/config/tests/karma.conf.js b/config/tests/karma.conf.js index cf1ba1bd..38b3370c 100644 --- a/config/tests/karma.conf.js +++ b/config/tests/karma.conf.js @@ -2,12 +2,13 @@ module.exports = function (config) { config.set({ basePath: '../..', - frameworks: ['jasmine'], + frameworks: ['jasmine', 'viewport'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-coverage'), - require('karma-spec-reporter') + require('karma-spec-reporter'), + require('karma-viewport') ], customLaunchers: { // chrome setup for travis CI using chromium @@ -59,6 +60,24 @@ module.exports = function (config) { logLevel: config.LOG_ERROR, autoWatch: true, browsers: ['Chrome'], - singleRun: false + singleRun: false, + viewport: { + breakpoints: [ + { + name: 'mobile', + size: { + width: 320, + height: 480 + } + }, + { + name: 'desktop', + size: { + width: 1440, + height: 900 + } + } + ] + } }); }; diff --git a/package.json b/package.json index c82e5492..cb491814 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "karma-jasmine": "1.1.0", "karma-remap-istanbul": "0.6.0", "karma-spec-reporter": "0.0.31", + "karma-viewport": "^1.0.2", "merge2": "1.2.0", "node-sass": "4.5.3", "postcss-loader": "1.3.3", diff --git a/src/components/tsconfig-spec.json b/src/components/tsconfig-spec.json index 329bf111..13732f24 100644 --- a/src/components/tsconfig-spec.json +++ b/src/components/tsconfig-spec.json @@ -17,7 +17,8 @@ "baseUrl": "", "types": [ "jasmine", - "node" + "node", + "karma-viewport" ] } } From 3f86dee10f2a05bc8700c2ca352c3cc713e8de72 Mon Sep 17 00:00:00 2001 From: tk Date: Thu, 12 Jul 2018 20:46:02 +0200 Subject: [PATCH 14/17] refactor and add row selection --- src/components/virtual-table/README.md | 5 +- .../virtual-table/column.component.ts | 7 + src/components/virtual-table/index.ts | 4 +- .../virtual-table/list.component.ts | 38 ++ src/components/virtual-table/module.ts | 9 +- .../row-data-response.interface.ts | 5 + .../virtual-table/table.component.scss | 78 ++++- .../virtual-table/table.component.spec.ts | 329 +++++++++++++----- .../virtual-table/table.component.ts | 160 ++++++--- src/components/virtual-table/table.html | 58 +++ .../virtual-table.component.html | 50 ++- .../virtual-table/virtual-table.component.ts | 13 +- 12 files changed, 590 insertions(+), 166 deletions(-) create mode 100644 src/components/virtual-table/list.component.ts create mode 100644 src/components/virtual-table/row-data-response.interface.ts create mode 100644 src/components/virtual-table/table.html diff --git a/src/components/virtual-table/README.md b/src/components/virtual-table/README.md index 8874d10c..4960be62 100644 --- a/src/components/virtual-table/README.md +++ b/src/components/virtual-table/README.md @@ -16,7 +16,10 @@ import the MdlVirtualTableModule and add it to your app.module imports: The IntersectionObserver is used for any table resize occurenc! Please be aware of using a modern browser ((see here for implementation status)[https://github.com/w3c/IntersectionObserver]) or use the official [polyfill](https://github.com/w3c/IntersectionObserver/tree/master/polyfill). -You can disable the IntersectionObserver by adding the directive intersection-observer-disabled. Be aware if you are using e.g. the table within @angular-mdl/core tabs component and the virtual table is inital hidden! +If you use the polyfill please do not forget to disable the mutuation observer! +`(window).IntersectionObserver.prototype.USE_MUTATION_OBSERVER = false;` + +You can disable the IntersectionObserver by adding the directive intersection-observer-disabled. Be aware if you are using e.g. the table within @angular-mdl/core tabs component and the virtual table is inital hidden may data is not loaded as expected. ### Usage & API diff --git a/src/components/virtual-table/column.component.ts b/src/components/virtual-table/column.component.ts index f846421c..8e9f2b11 100644 --- a/src/components/virtual-table/column.component.ts +++ b/src/components/virtual-table/column.component.ts @@ -15,6 +15,13 @@ export class MdlVirtualTableColumnComponent { return this._sortable; } _sortable: boolean = false; + @Input('row-selection-enabled') set rowSelection(v: boolean) { + this._rowSelection = v !== null && "" + v !== 'false'; + } + get rowSelection(): boolean { + return this._rowSelection; + } + _rowSelection: boolean = false; sortDirection: string; @ContentChild(TemplateRef) cellTemplate: TemplateRef; diff --git a/src/components/virtual-table/index.ts b/src/components/virtual-table/index.ts index f64962c7..c7ff92b2 100644 --- a/src/components/virtual-table/index.ts +++ b/src/components/virtual-table/index.ts @@ -1,3 +1,5 @@ export * from './module'; export * from './table.component'; -export * from './column.component'; \ No newline at end of file +export * from './column.component'; +export * from './list.component'; +export * from './row-data-response.interface'; \ No newline at end of file diff --git a/src/components/virtual-table/list.component.ts b/src/components/virtual-table/list.component.ts new file mode 100644 index 00000000..b5101bc3 --- /dev/null +++ b/src/components/virtual-table/list.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, ContentChild, TemplateRef, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'mdl-virtual-table-list', + template: '' +}) +export class MdlVirtualTableListComponent { + + + @Input('breakpoint-max-width') set breakpointMaxWidth(v: number) { + this._breakpointMaxWidth = parseInt("" + v); + } + get breakpointMaxWidth(): number { + return this._breakpointMaxWidth; + } + + @Input('item-height') set itemHeight(v: number) { + this._itemHeight = parseInt("" + v); + } + get itemHeight(): number { + return this._itemHeight; + } + + get styledItemHeight(): string { + return this._itemHeight - 11 + 'px'; + } + sortDirection: string; + + @Output('item-click') itemClick: EventEmitter; + @ContentChild(TemplateRef) itemTemplate: TemplateRef; + + private _breakpointMaxWidth: number = 480; + private _itemHeight: number = 51; + + constructor() { + this.itemClick = new EventEmitter(); + } +} \ No newline at end of file diff --git a/src/components/virtual-table/module.ts b/src/components/virtual-table/module.ts index 51627ee2..d84d7ba7 100644 --- a/src/components/virtual-table/module.ts +++ b/src/components/virtual-table/module.ts @@ -1,22 +1,27 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { MdlModule } from '@angular-mdl/core'; import { VirtualScrollModule } from 'angular2-virtual-scroll'; import { MdlVirtualTableComponent } from './table.component'; import { MdlVirtualTableColumnComponent } from './column.component'; +import { MdlVirtualTableListComponent } from './list.component'; @NgModule({ imports: [ CommonModule, + MdlModule, VirtualScrollModule ], exports: [ MdlVirtualTableComponent, - MdlVirtualTableColumnComponent + MdlVirtualTableColumnComponent, + MdlVirtualTableListComponent ], declarations: [ MdlVirtualTableComponent, - MdlVirtualTableColumnComponent + MdlVirtualTableColumnComponent, + MdlVirtualTableListComponent ] }) export class MdlVirtualTableModule { diff --git a/src/components/virtual-table/row-data-response.interface.ts b/src/components/virtual-table/row-data-response.interface.ts new file mode 100644 index 00000000..51e4faab --- /dev/null +++ b/src/components/virtual-table/row-data-response.interface.ts @@ -0,0 +1,5 @@ +export interface RowDataResponseInterface { + rows: any[]; + offset: number; + limit: number; +} \ No newline at end of file diff --git a/src/components/virtual-table/table.component.scss b/src/components/virtual-table/table.component.scss index e1961845..ab1dc830 100644 --- a/src/components/virtual-table/table.component.scss +++ b/src/components/virtual-table/table.component.scss @@ -1,14 +1,19 @@ :host.flex-height, .flex-height virtual-scroll, -.flex-height.table-container { +.flex-height.wrapper { display: flex; + width: 100%; } -.flex-height.table-container { +.flex-height.wrapper { flex-direction: column; } -.table-container { +.flex-height virtual-scroll { + flex: 1; +} + +.wrapper { width: 100%; } .scrollbar-space { @@ -18,6 +23,7 @@ virtual-scroll { width: 100%; display: block; + } .table-header { @@ -86,6 +92,21 @@ virtual-scroll { .header-cell.sortable { cursor: pointer; } +.mdl-menu__item.sortable:before { + font-family: 'Material Icons'; + font-size: 20pt; + content: ' '; + vertical-align: middle; + letter-spacing: -11px; + position: relative; + padding-right: 10px; + padding-left: 20px; +} + +.mdl-menu__item.sortable { + +} + .header-cell.sortable:after { font-family: 'Material Icons'; font-size: 20pt; @@ -95,9 +116,60 @@ virtual-scroll { position: absolute; } +.mdl-menu__item.sortable.desc:before, .header-cell.sortable.desc:after { content: 'arrow_drop_down'; + padding-left: 0; } +.mdl-menu__item.sortable.asc:before, .header-cell.sortable.asc:after { content: 'arrow_drop_up'; + padding-left: 0; +} + +.list-header { + width: 100%; + border-bottom: 1px solid #ccc; + text-align: right; +} + +.menu-item-label { + padding: 2px 10px; + color: #999; + text-align: left; +} + +.list-sort-button { + +} + + +.list { + width: 100%; +} + +.list-item { + border-bottom: 1px solid #eee; + padding: 5px 10px; + + position: relative; + cursor: pointer; + + +} + +.list-item:last-child { + border-bottom: none; +} + +.list-item:after { + font-family: 'Material Icons'; + font-size: 20pt; + content: 'keyboard_arrow_right'; + vertical-align: middle; + letter-spacing: -11px; + position: absolute; + color: #666; + right: 5px; + top: calc(50% - 10px); } diff --git a/src/components/virtual-table/table.component.spec.ts b/src/components/virtual-table/table.component.spec.ts index c34c4c55..6ca4c7d0 100644 --- a/src/components/virtual-table/table.component.spec.ts +++ b/src/components/virtual-table/table.component.spec.ts @@ -1,21 +1,23 @@ import { async, fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import {Component, DebugElement, ViewChild} from '@angular/core' -import { MdlVirtualTableComponent, MdlVirtualTableModule, MdlVirtualTableColumnComponent } from './'; +import { Component, DebugElement, ViewChild } from '@angular/core' +import { MdlVirtualTableComponent, MdlVirtualTableModule, MdlVirtualTableColumnComponent, RowDataResponseInterface } from './'; import { VirtualScrollModule, VirtualScrollComponent } from 'angular2-virtual-scroll'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/delay'; +/// + describe('VirtualTableComponent', () => { let fixture: ComponentFixture; //generate rows - let rows:any[] = []; - for(var i = 0; i < 1000; i++) { - rows.push({_index: i, _label: 'Test ' + i}); + let rows: any[] = []; + for (var i = 0; i < 1000; i++) { + rows.push({ _index: i, _label: 'Test ' + i }); } beforeEach(async(() => { @@ -26,7 +28,10 @@ describe('VirtualTableComponent', () => { declarations: [TestMdlVirtualTableComponent, TestMdlVirtualTableComponentWithAutoInit, TestMdlVirtualTableComponentWithoutRowCountRequest, - TestMdlVirtualTableComponentWithoutRowDataRequest], + TestMdlVirtualTableComponentWithoutRowDataRequest, + TestMdlVirtualTableComponentWithRowSelection, + TestMdlVirtualTableComponentWithResponsiveList + ], providers: [ ] }); @@ -54,13 +59,13 @@ describe('VirtualTableComponent', () => { fixture.whenStable().then(() => { expect(debugComponent.componentInstance.values.length).toBe(rows.length, 'Rows length should be 1000'); }); - + })); it('should load viewport rows', ((done) => { let testInstance = fixture.componentInstance; - - let componentInstance:MdlVirtualTableComponent = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)).componentInstance; + + let componentInstance: MdlVirtualTableComponent = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)).componentInstance; let debugVirtualScroll = fixture.debugElement.query(By.directive(VirtualScrollComponent)); let componentVirtualScroll: VirtualScrollComponent = debugVirtualScroll.componentInstance; @@ -68,7 +73,7 @@ describe('VirtualTableComponent', () => { fixture.autoDetectChanges(); fixture.whenStable().then(() => { //simulate scroll - var scrollElement = componentVirtualScroll.parentScroll instanceof Window ? document.body : componentVirtualScroll.parentScroll || debugVirtualScroll.nativeElement; + var scrollElement = componentVirtualScroll.parentScroll instanceof Window ? document.body : componentVirtualScroll.parentScroll || debugVirtualScroll.nativeElement; var fixedRowHeight = 48; //@see table.component.scss var scrollIndex = 500; scrollElement.scrollTop = (fixedRowHeight + 1) * scrollIndex; @@ -86,19 +91,19 @@ describe('VirtualTableComponent', () => { let componentInstance: MdlVirtualTableComponent = debugTableInstance. componentInstance; - testInstance.initData(rows); - fixture.autoDetectChanges(); - fixture.whenStable().then(() => { - - let sortSpy = spyOn(testInstance, 'onSort').and.callThrough(); - - let columnElements = debugTableInstance.nativeElement.querySelectorAll("div.table-header div.header-cell"); - columnElements[0].click(); - expect(testInstance.onSort).toHaveBeenCalledWith({column: '_index', direction: 'asc'}); - sortSpy.calls.reset(); - columnElements[0].click(); - expect(testInstance.onSort).toHaveBeenCalledWith({column: '_index', direction: 'desc'}); - }); + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + let sortSpy = spyOn(testInstance, 'onSort').and.callThrough(); + + let columnElements = debugTableInstance.nativeElement.querySelectorAll("div.table-header div.header-cell"); + columnElements[0].click(); + expect(testInstance.onSort).toHaveBeenCalledWith({ column: '_index', direction: 'asc' }); + sortSpy.calls.reset(); + columnElements[0].click(); + expect(testInstance.onSort).toHaveBeenCalledWith({ column: '_index', direction: 'desc' }); + }); })); it('should not sort columns without sortable attribute', async(() => { @@ -107,16 +112,16 @@ describe('VirtualTableComponent', () => { let componentInstance: MdlVirtualTableComponent = debugTableInstance. componentInstance; - testInstance.initData(rows); - fixture.autoDetectChanges(); - fixture.whenStable().then(() => { - - spyOn(testInstance, 'onSort').and.callThrough(); - - let columnElements = debugTableInstance.nativeElement.querySelectorAll("div.table-header div.header-cell"); - columnElements[1].click(); - expect(testInstance.onSort).not.toHaveBeenCalled(); - }); + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + spyOn(testInstance, 'onSort').and.callThrough(); + + let columnElements = debugTableInstance.nativeElement.querySelectorAll("div.table-header div.header-cell"); + columnElements[1].click(); + expect(testInstance.onSort).not.toHaveBeenCalled(); + }); })); it('should emit event on click on a row', async(() => { @@ -125,16 +130,16 @@ describe('VirtualTableComponent', () => { let componentInstance: MdlVirtualTableComponent = debugTableInstance. componentInstance; - testInstance.initData(rows); - fixture.autoDetectChanges(); - fixture.whenStable().then(() => { - - spyOn(testInstance, 'onRowClick').and.callThrough(); - - let visibleRows = debugTableInstance.nativeElement.querySelectorAll("div.table div.table-row"); - visibleRows[0].click(); - expect(testInstance.onRowClick).toHaveBeenCalled(); - }); + testInstance.initData(rows); + fixture.autoDetectChanges(); + fixture.whenStable().then(() => { + + spyOn(testInstance, 'onRowClick').and.callThrough(); + + let visibleRows = debugTableInstance.nativeElement.querySelectorAll("div.table div.table-row"); + visibleRows[0].click(); + expect(testInstance.onRowClick).toHaveBeenCalled(); + }); })); it('should throw error on missing rowCountRequest listener', async(() => { @@ -143,13 +148,13 @@ describe('VirtualTableComponent', () => { fixtureWrongConfig.detectChanges(); expect(() => fixtureWrongConfig.componentInstance.refresh()).toThrowError('mdl-virtual-table component has no rowCountRequest Listener'); }); - + })); it('should throw error on missing rowDataRequest listener', async(() => { TestBed.compileComponents().then(() => { let fixtureTest: ComponentFixture = TestBed.createComponent(TestMdlVirtualTableComponentWithoutRowDataRequest); - fixtureTest.detectChanges(); + fixtureTest.detectChanges(); expect(() => fixtureTest.componentInstance.refresh()).toThrowError('mdl-virtual-table component has no rowDataRequest Listener'); }); })); @@ -176,7 +181,7 @@ describe('VirtualTableComponent', () => { let testInstance = fixture.componentInstance; let debugComponent = fixture.debugElement.query(By.directive(MdlVirtualTableComponent)); - let componentInstance:MdlVirtualTableComponent = debugComponent.componentInstance; + let componentInstance: MdlVirtualTableComponent = debugComponent.componentInstance; let debugVirtualScroll = fixture.debugElement.query(By.directive(VirtualScrollComponent)); testInstance.hideTable(); @@ -204,24 +209,124 @@ describe('VirtualTableComponent', () => { testInstance.initData(rows); fixture.autoDetectChanges(); fixture.whenStable().then(() => { - + spyOn(testInstance, 'onRowDataRequest').and.callThrough(); componentInstance.refresh(); expect(testInstance.onRowDataRequest).toHaveBeenCalled(); }); })); - - + + + it('should emit select rows', ((done) => { + fixture.destroy(); + let fixtureSelection: ComponentFixture; + TestBed.compileComponents().then(() => { + fixtureSelection = TestBed.createComponent(TestMdlVirtualTableComponentWithRowSelection); + fixtureSelection.detectChanges(); + + expect(fixtureSelection).toBeDefined(); + + let testInstance = fixtureSelection.componentInstance; + let debugTableInstance = fixtureSelection.debugElement.query(By.directive(MdlVirtualTableComponent)); + let componentInstance: MdlVirtualTableComponent = debugTableInstance. + componentInstance; + + let debugVirtualScroll = fixtureSelection.debugElement.query(By.directive(VirtualScrollComponent)); + let componentVirtualScroll: VirtualScrollComponent = debugVirtualScroll.componentInstance; + + testInstance.initData(rows.slice(0, 100)); + fixtureSelection.autoDetectChanges(); + fixtureSelection.whenStable().then(() => { + + let spyOnRowSelection = spyOn(testInstance, 'onRowSelection').and.callThrough(); + let selectionCheckbox = debugTableInstance.nativeElement.querySelector("div.table div.table-row:first-child + .table-row .mdl-checkbox"); + selectionCheckbox.click(); + + expect(testInstance.onRowSelection).toHaveBeenCalled(); + expect(componentInstance.selection).toEqual([1]); + + selectionCheckbox = debugTableInstance.nativeElement.querySelector("div.table div.table-row:first-child + .table-row .mdl-checkbox"); + selectionCheckbox.click(); + expect(spyOnRowSelection.calls.count()).toBe(2); + expect(componentInstance.selection).toEqual([]); + + spyOnRowSelection.calls.reset(); + componentInstance.onChangeRowSelection(undefined, 1); + expect(testInstance.onRowSelection).not.toHaveBeenCalled(); + + let selectAllCheckbox = debugTableInstance.nativeElement.querySelector("div.table-header div.table-row .header-cell .mdl-checkbox"); + selectAllCheckbox.click(); + + var scrollElement = componentVirtualScroll.parentScroll instanceof Window ? document.body : componentVirtualScroll.parentScroll || debugVirtualScroll.nativeElement; + var fixedRowHeight = 48; //@see table.component.scss + var scrollIndex = 30; + scrollElement.scrollTop = (fixedRowHeight + 1) * scrollIndex; + componentVirtualScroll.refresh(); + + setTimeout(() => { + //scroll down + scrollIndex += 30; + scrollElement.scrollTop = (fixedRowHeight + 1) * scrollIndex; + componentVirtualScroll.refresh(); + + setTimeout(() => { + scrollIndex += 30; + scrollElement.scrollTop = (fixedRowHeight + 1) * scrollIndex; + componentVirtualScroll.refresh(); + setTimeout(() => { + expect(componentInstance.selection.length).toBe(componentInstance.values.length, "Could not select all visible rows!"); + + selectAllCheckbox.click(); + expect(componentInstance.selection.length).toBe(0, "Could not unselect all visible rows!"); + done(); + }, 120); + + }, 120); //wait for requestAnimationFrame + }, 120); //wait for requestAnimationFrame + + }); + + + }); + + })); + + it('should show list instead of table if window size reaches the breakpoint', async(() => { + viewport.set("mobile"); + let fixtureList: ComponentFixture; + TestBed.compileComponents().then(() => { + fixtureList = TestBed.createComponent(TestMdlVirtualTableComponentWithResponsiveList); + let debugComponent = fixtureList.debugElement.query(By.directive(MdlVirtualTableComponent)); + expect(fixtureList).toBeDefined(); + fixtureList.componentInstance.initData(rows); + + let testInstance = fixtureList.componentInstance; + let debugTableInstance = fixtureList.debugElement.query(By.directive(MdlVirtualTableComponent)); + let componentInstance: MdlVirtualTableComponent = debugTableInstance. + componentInstance; + + testInstance.initData(rows); + fixtureList.autoDetectChanges(); + + fixtureList.whenStable().then(() => { + expect(debugTableInstance.nativeElement.querySelector(".list")).not.toBe(null); + let listItemClickSpy = spyOn(testInstance, 'onListItemClick').and.callThrough(); + let listElements = debugTableInstance.nativeElement.querySelectorAll("div.list div.list-item"); + listElements[1].click(); + expect(testInstance.onListItemClick).toHaveBeenCalledWith(jasmine.objectContaining({ row: rows[1], index: 1 })); + }); + }); + })); }); @Component({ template: `
- + [row-data-stream]="rowDataStream" [row-count-stream]="rowCountStream" + (row-count-request)="onRowCountRequest()" (row-data-request)="onRowDataRequest($event)" + (sort)="onSort($event)" (row-click)="onRowClick($event)">
Title: {{value}}
@@ -229,44 +334,42 @@ describe('VirtualTableComponent', () => {
` - }) - class TestMdlVirtualTableComponent { +}) +class TestMdlVirtualTableComponent { @ViewChild('table') table: MdlVirtualTableComponent; - + protected rows: any[]; - - public visible: boolean = true; - + + public visible: boolean = true; + public rowCountStream: Observable; - public rowDataStream: Observable<{rows: any[], offset: number, limit: number}>; + public rowDataStream: Observable<{ rows: any[], offset: number, limit: number }>; - onSort(data: {column: string, direction: string}) { - console.log("sort data:", data); + onSort(data: { column: string, direction: string }) { } onRowClick(event: any) { - console.log("on row click", event); } requestRowCount() { - return Observable.of(1000); + return Observable.of(this.rows.length || 1000); } onRowCountRequest() { this.rowCountStream = this.requestRowCount(); } - onRowDataRequest(request: {offset: number, limit: number, refresh?:boolean}) { + onRowDataRequest(request: { offset: number, limit: number, refresh?: boolean }) { this.rowDataStream = this.requestRowData(request.offset, request.limit); } - requestRowData(offset: number, limit: number) { - if(!this.rows) { - return Observable.of({rows: [], offset: 0, limit: 0}); + requestRowData(offset: number, limit: number): Observable { + if (!this.rows) { + return Observable.of({ rows: [], offset: 0, limit: 0 }); } let items = this.rows.slice(offset, offset + limit); - return Observable.of({rows: items, offset, limit}); + return Observable.of({ rows: items, offset, limit }); } initData(rows: any[]) { @@ -280,15 +383,15 @@ describe('VirtualTableComponent', () => { showTable() { this.visible = true; - } - } + } +} - @Component({ +@Component({ template: `
- +
Title: {{value}}
@@ -296,21 +399,21 @@ describe('VirtualTableComponent', () => {
` - }) - class TestMdlVirtualTableComponentWithoutRowCountRequest extends TestMdlVirtualTableComponent { +}) +class TestMdlVirtualTableComponentWithoutRowCountRequest extends TestMdlVirtualTableComponent { refresh() { this.table.refresh(); } - } +} @Component({ template: `
- +
Title: {{value}}
@@ -328,18 +431,70 @@ class TestMdlVirtualTableComponentWithoutRowDataRequest extends TestMdlVirtualTa @Component({ template: `
- + (row-count-request)="onRowCountRequest()" (row-data-request)="onRowDataRequest($event)"> + + +
Title: {{value}}
+
+
+
+` +}) +class TestMdlVirtualTableComponentWithAutoInit extends TestMdlVirtualTableComponent { + +} + + +@Component({ + template: ` +
+
Title: {{value}}
+ + + Index: {{row?._index}}
+ My Label: {{row?._label}} +
+
` }) - class TestMdlVirtualTableComponentWithAutoInit extends TestMdlVirtualTableComponent { +class TestMdlVirtualTableComponentWithResponsiveList extends TestMdlVirtualTableComponent { + + onListItemClick(data: { event: MouseEvent, row: any, index: number }) { + + } +} - } \ No newline at end of file +@Component({ + template: ` +
+ + + + +
Title: {{value}}
+
+
+
+` +}) +class TestMdlVirtualTableComponentWithRowSelection extends TestMdlVirtualTableComponent { + onRowSelection(event: number[]) { + + } +} \ No newline at end of file diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts index 46f7aeae..553d04df 100644 --- a/src/components/virtual-table/table.component.ts +++ b/src/components/virtual-table/table.component.ts @@ -6,7 +6,9 @@ import { ChangeDetectionStrategy, ViewChild, ContentChildren, + ContentChild, EventEmitter, + HostListener, OnInit, OnChanges, OnDestroy, @@ -22,6 +24,9 @@ import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import 'rxjs/add/operator/withLatestFrom'; import 'rxjs/add/operator/distinctUntilChanged'; +import { MdlVirtualTableListComponent } from './list.component'; +import { RowDataResponseInterface } from './index'; +import { MdlCheckboxComponent } from '@angular-mdl/core'; declare var IntersectionObserver: any; @@ -29,55 +34,36 @@ declare var IntersectionObserver: any; moduleId: module.id, selector: 'mdl-virtual-table', changeDetection: ChangeDetectionStrategy.Default, - template: ` -
-
-
-
-
- {{col.label}}
-
-
-
- -
-
-
- -
-
-
-
- {{value}} -
- `, + templateUrl: 'table.html', styleUrls: ['table.component.scss'] }) + export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy { values: any[]; + @HostBinding('class.flex-height') + @ViewChild(VirtualScrollComponent) private virtualScroll: VirtualScrollComponent; @ViewChild('tableWrapper') private virutalScrollElement: ElementRef; + @ViewChild('selectAllCheckbox') + private selectAllCheckbox: MdlCheckboxComponent; + @ContentChildren(MdlVirtualTableColumnComponent) columns: QueryList; - private lastLoadedElementHash: string; - - @Input() rowCountStream: Observable; - @Input() rowDataStream: Observable<{rows: any[], offset: number, limit: number}>; - @Input() maxHeight: string = '100vh'; - @Input() minHeight: string; + @ContentChild(MdlVirtualTableListComponent) + responsiveList: MdlVirtualTableListComponent; - private _isFlexHeight: boolean = false; - @HostBinding('class.flex-height') + @Input('row-count-stream') rowCountStream: Observable; + @Input('row-data-stream') rowDataStream: Observable; + @Input('row-height') rowHeight: number = 48; + @Input('max-height') maxHeight: string = '100vh'; + @Input('min-height') minHeight: string; + @Input('flex-height') get isFlexHeight(): boolean { return this._isFlexHeight; @@ -85,8 +71,7 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe set isFlexHeight(value) { this._isFlexHeight = value != null && "" + value !== 'false'; } - - private _isInitialLoadDisabled: boolean = false; + @Input('init-refresh-disabled') get isInitialLoadDisabled(): boolean { return this._isInitialLoadDisabled; @@ -95,7 +80,6 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe this._isInitialLoadDisabled = value != null && "" + value !== 'false'; } - private _isIntersectionObserverDisabled: boolean = false; @Input('intersection-observer-disabled') get isIntersectionObserverDisabled(): boolean { return this._isIntersectionObserverDisabled; @@ -104,32 +88,42 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe this._isIntersectionObserverDisabled = value != null && "" + value !== 'false'; } - private _rowCount: number; - private _visibleRowOffset: number; - private _nativeElementObserver: any; + get selection():number[] { + return Object.keys(this._rowSelectionByIndex).map((index) => parseInt(index)).sort(); + } @Output() sort: EventEmitter; - @Output() rowClick: EventEmitter; - - private requestRowCountSubject: Subject; - private requestRowDataSubject: Subject; - - @Output() rowCountRequest: EventEmitter; - @Output() rowDataRequest: EventEmitter<{offset: number, limit: number, refresh?: boolean}>; + @Output('row-click') rowClick: EventEmitter; + @Output('row-selection-change') rowSelection: EventEmitter; + @Output('row-count-request') rowCountRequest: EventEmitter; + @Output('row-data-request') rowDataRequest: EventEmitter<{offset: number, limit: number, refresh?: boolean}>; - rows: any[]; + public rows: any[]; + + private _rowSelectionByIndex: {[index: number]: boolean} = {}; + private _isInitialLoadDisabled: boolean = false; + private _lastLoadedElementHash: string; + private _useList: boolean = false; + private _isIntersectionObserverDisabled: boolean = false; + private _isFlexHeight: boolean = false; + private _rowCount: number; + private _visibleRowOffset: number; + private _nativeElementObserver: any; + private _requestRowCountSubject: Subject; + private _requestRowDataSubject: Subject; constructor(private cdr: ChangeDetectorRef) { this.sort = new EventEmitter(); this.rowClick = new EventEmitter(); + this.rowSelection = new EventEmitter(); this.rowCountRequest = new EventEmitter(); this.rowDataRequest = new EventEmitter(); - this.requestRowCountSubject = new Subject(); - this.requestRowDataSubject = new Subject(); - this.requestRowCountSubject.asObservable().subscribe(() => { + this._requestRowCountSubject = new Subject(); + this._requestRowDataSubject = new Subject(); + this._requestRowCountSubject.asObservable().subscribe(() => { this.rowCountRequest.emit(); }); - this.requestRowDataSubject.asObservable().distinctUntilChanged((x: any, y: any) => { + this._requestRowDataSubject.asObservable().distinctUntilChanged((x: any, y: any) => { return (x.offset === y.offset && x.limit === y.limit) && !x.refresh && !y.refresh; }).subscribe((result) => { this.rowDataRequest.emit(result); @@ -140,10 +134,16 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe if(!this.isInitialLoadDisabled) { this.refresh(true); } + this.checkUseList(); + } + + checkUseList() { + this._useList = this.responsiveList && window.innerWidth <= this.responsiveList.breakpointMaxWidth; + this.cdr.detectChanges(); } ngAfterViewChecked() { - if(!this._nativeElementObserver && this.virutalScrollElement.nativeElement + if(!this._nativeElementObserver && this.virutalScrollElement && this.virutalScrollElement.nativeElement && !this.isIntersectionObserverDisabled) { this._nativeElementObserver = new IntersectionObserver((entries: any[]) => { @@ -173,7 +173,7 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe ngOnChanges(changes: SimpleChanges) { if((changes).rowDataStream && (changes).rowDataStream.currentValue) { - let lastRowDataSubscription = this.rowDataStream.subscribe((result) => { + let lastRowDataSubscription = this.rowDataStream.subscribe((result: RowDataResponseInterface) => { this._visibleRowOffset = result.offset; this._rowCount = this._rowCount || result.rows.length; let values = new Array(this._rowCount); @@ -188,7 +188,7 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe }); } if((changes).rowCountStream && (changes).rowCountStream.currentValue) { - this.rowCountStream.subscribe((count) => { + this.rowCountStream.subscribe((count: number) => { this._rowCount = count; this.values = new Array(count); this.virtualScroll.previousStart = undefined; @@ -200,15 +200,51 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe } + onSelectAllRows(selected: boolean) { + if(selected) { + for(var i = 0; i < this._rowCount; i++) { + this._rowSelectionByIndex[i] = true; + } + } else { + this._rowSelectionByIndex = {}; + } + this.rowSelection.emit(Object.keys(this._rowSelectionByIndex).map((index) => parseInt(index)).sort()); + } + + onChangeRowSelection(selected: boolean, index: number) { + if (typeof(selected) === 'undefined') { + return; + } + if (this._rowSelectionByIndex[index] === selected) { + return; + } + if (selected === false) { + delete this._rowSelectionByIndex[index]; + } else { + this._rowSelectionByIndex[index] = selected; + } + let selection = Object.keys(this._rowSelectionByIndex).map((index) => parseInt(index)).sort(); + this.selectAllCheckbox.writeValue(selection.length === this._rowCount); + this.rowSelection.emit(selection); + } + onListChange(event: ChangeEvent) { let limit = event.end - event.start; - this.requestRowDataSubject.next({offset: event.start, limit}); + this._requestRowDataSubject.next({offset: event.start, limit}); + } + + onListItemClick(event: MouseEvent, row: any, index: number) { + this.responsiveList.itemClick.emit({event, row, index}); } onRowClick(event: MouseEvent, row: any, index: number) { this.rowClick.emit({event, row, index}); } + @HostListener('window:resize', []) + onWindowResize() { + this.checkUseList(); + } toggleSortByColumn(col: MdlVirtualTableColumnComponent) { if (!col.sortable) { @@ -229,19 +265,27 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe } if(withRowCount) { - this.requestRowCountSubject.next(); + this._requestRowCountSubject.next(); + this.cdr.detectChanges(); } + /* istanbul ignore if */ + if (!this.virtualScroll) { + return; + } + if(typeof(this.virtualScroll.previousStart) === 'number' && typeof(this.virtualScroll.previousEnd) === 'number') { - this.requestRowDataSubject.next({ + this._requestRowDataSubject.next({ refresh: true, offset: this.virtualScroll.previousStart, limit: this.virtualScroll.previousEnd - this.virtualScroll.previousStart }); this.virtualScroll.previousEnd = undefined; this.virtualScroll.previousStart = undefined; + } this.virtualScroll.refresh(); + } } diff --git a/src/components/virtual-table/table.html b/src/components/virtual-table/table.html new file mode 100644 index 00000000..e30901af --- /dev/null +++ b/src/components/virtual-table/table.html @@ -0,0 +1,58 @@ +
+
+ + + +
+ + {{col.label}} + +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + {{col.label}} + + {{col.label}} +
+
+
+
+ +
+
+
+ +
+
+
+
+ + {{value}} + + + + +
\ No newline at end of file diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.html b/src/e2e-app/app/virtual-table/virtual-table.component.html index f28cc6f0..dbeab26b 100644 --- a/src/e2e-app/app/virtual-table/virtual-table.component.html +++ b/src/e2e-app/app/virtual-table/virtual-table.component.html @@ -9,32 +9,50 @@
Table
+ [row-data-stream]="rowDataStream" [row-count-stream]="rowCountStream" + (row-count-request)="onRowCountRequest()" (row-data-request)="onRowDataRequest($event)" + (sort)="onSort($event)" (row-click)="onRowClick($event)" (row-selection-change)="onRowSelectionChange($event)"> + - -
Title: {{value}}
+ + +
Title: {{value}}
+
+ + + {{row?._id}}
+ {{row?._label}} +
+
-
+
   
+  [row-data-stream]="rowDataStream" [row-count-stream]="rowCountStream" 
+  (row-count-request)="onRowCountRequest()" (row-data-request)="onRowDataRequest($event)"
+  (sort)="onSort($event)" (row-click)="onRowClick($event)" (row-selection-change)="onRowSelectionChange($event)">
+    
     
-    
-      
Title: {{value}}
+ + +
Title: {{value}}
+
+ + + {{row?._id}}
+ {{row?._label}} +
+
]]>
-
+
   Table
       onRowClick(event) {
           console.log("on row click", event);
       }
+
+      onListItemClick(event) {
+          console.log("on list item click", event);
+      }
+
+      onRowSelectionChange(event) {
+          console.log("change row selection", event);
+      }
   
       requestRowCount() {
           return Observable.of(500).delay(1000);;
diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.ts b/src/e2e-app/app/virtual-table/virtual-table.component.ts
index 80fdb0bb..1e1dccb7 100644
--- a/src/e2e-app/app/virtual-table/virtual-table.component.ts
+++ b/src/e2e-app/app/virtual-table/virtual-table.component.ts
@@ -4,6 +4,7 @@ import {
 } from '@angular/core';
 import { Observable } from 'rxjs/Observable';
 import 'rxjs/add/observable/of';
+import { RowDataResponseInterface } from '../../../components/virtual-table/index';
 
 @Component({
   selector: 'virtual-table-demo',
@@ -33,11 +34,19 @@ export class VirtualTableDemo {
         console.log("on row click", event);
     }
 
-    requestRowCount() {
+    onListItemClick(event) {
+        console.log("on list item click", event);
+    }
+
+    onRowSelectionChange(event) {
+        console.log("change row selection", event);
+    }
+
+    requestRowCount(): Observable {
         return Observable.of(500).delay(1000);;
     }
 
-    requestRowData(offset, limit) {
+    requestRowData(offset, limit): Observable {
         let rows = [];
         for(var i = offset; i < (offset + limit); i++) {
             rows.push({_id: i, _label: 'Test ' + i});

From 8dc806ed7ad88f39ef3866749de3de1a882fbb9c Mon Sep 17 00:00:00 2001
From: tk 
Date: Sat, 14 Jul 2018 16:29:19 +0200
Subject: [PATCH 15/17] change webpack config for webpack 4.x support

---
 config/webpack/webpack.common.js          | 23 +++++++++++++++++------
 config/webpack/webpack.dev.js             |  2 +-
 config/webpack/webpack.prod.js            | 17 ++++++++++++++++-
 package.json                              | 16 +++++++++-------
 src/components/system-config-spec.ts      |  2 ++
 src/components/virtual-table/package.json |  3 ++-
 tools/gulp/tasks/test.ts                  |  3 ++-
 7 files changed, 49 insertions(+), 17 deletions(-)

diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js
index e586cdaf..4c0315ca 100644
--- a/config/webpack/webpack.common.js
+++ b/config/webpack/webpack.common.js
@@ -65,7 +65,14 @@ module.exports = {
 				test: /\.scss$/,
 				exclude: [util.root('src', 'e2e-app', 'app'), util.root('src', 'components')],
 				use: ExtractTextPlugin.extract({
-					use: ["css-loader", "postcss-loader", "sass-loader"],
+					use: ["css-loader",  {
+						loader: 'postcss-loader',
+						options: {
+						  ident: 'embedded',
+						  plugins: function() { return []; },
+						  sourceMap: true
+						}
+					  }, "sass-loader"],
 					// use style-loader in development
 					fallback: "style-loader"
 				})
@@ -73,7 +80,14 @@ module.exports = {
 			{
 				test: /\.scss$/,
 				include: [util.root('src', 'e2e-app', 'app'), util.root('src', 'components')],
-				loaders: ['raw-loader', 'postcss-loader', 'sass-loader']
+				loaders: ['raw-loader', {
+					loader: 'postcss-loader',
+					options: {
+					  ident: 'embedded',
+					  plugins: function() { return []; },
+					  sourceMap: true
+					}
+				  }, 'sass-loader']
 			},
 			{
 				test: /\.hbs$/,
@@ -89,9 +103,6 @@ module.exports = {
 			/@angular(\\|\/)core(\\|\/)esm5/,
 			util.root('src') // location of your src
 		),
-		new webpack.optimize.CommonsChunkPlugin({
-			name: ['app', 'vendor', 'polyfills']
-		}),
 		new webpack.LoaderOptionsPlugin({
 			options: {
 				postcss: function () {
@@ -105,4 +116,4 @@ module.exports = {
 			production: process.env.NODE_ENV == 'production' ? true : false
 		})
 	]
-};
+};
\ No newline at end of file
diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js
index fa0054d1..013cad4c 100644
--- a/config/webpack/webpack.dev.js
+++ b/config/webpack/webpack.dev.js
@@ -5,7 +5,7 @@ var util = require('./util');
 
 module.exports = webpackMerge(commonConfig, {
 	devtool: 'cheap-module-eval-source-map',
-
+	mode: 'development',
 	output: {
 		path: util.root('dist'),
 		filename: '[name].js',
diff --git a/config/webpack/webpack.prod.js b/config/webpack/webpack.prod.js
index 86c0a388..cf854e56 100644
--- a/config/webpack/webpack.prod.js
+++ b/config/webpack/webpack.prod.js
@@ -2,6 +2,7 @@ var webpack = require('webpack');
 var webpackMerge = require('webpack-merge');
 var ExtractTextPlugin = require('extract-text-webpack-plugin');
 var CopyWebpackPlugin = require('copy-webpack-plugin');
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 var commonConfig = require('./webpack.common.js');
 var util = require('./util');
 
@@ -16,11 +17,26 @@ module.exports = webpackMerge(commonConfig, {
 		filename: '[name].[hash].js',
 		chunkFilename: '[id].[hash].chunk.js'
 	},
+	
 	//
 	// htmlLoader: {
 	// 	minimize: false // workaround for ng2
 	// },
 
+	optimization: {
+		minimizer: [
+			new UglifyJsPlugin({
+				cache: true,
+				parallel: true,
+				uglifyOptions: {
+				  compress: true,
+				  ecma: 5,
+				  mangle: true
+				}
+			})
+		]
+	},
+
 	plugins: [
 		new CopyWebpackPlugin([{ from: util.root('src', 'e2e-app', '404.html') }], {copyUnmodified: true}),
 		new webpack.NoEmitOnErrorsPlugin(),
@@ -28,7 +44,6 @@ module.exports = webpackMerge(commonConfig, {
 			minimize: false,
 			debug: false
 		}),
-		new webpack.optimize.UglifyJsPlugin(),
 		new ExtractTextPlugin('[name].[hash].css'),
 		new webpack.DefinePlugin({
 			'process.env': {
diff --git a/package.json b/package.json
index 04ac6363..83d2e6f2 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
     "@angular/platform-browser-dynamic": "5.2.9",
     "@angular/router": "5.2.9",
     "@mseemann/prism": "0.0.1",
+    "@tweenjs/tween.js":"17.2.0",
     "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#a83b922",
     "core-js": "2.5.7",
     "custom-event-polyfill": "^0.3.0",
@@ -51,11 +52,11 @@
     "angular2-template-loader": "^0.6.2",
     "awesome-typescript-loader": "5.1.1",
     "codeclimate-test-reporter": "0.5.0",
-    "copy-webpack-plugin": "4.2.1",
+    "copy-webpack-plugin": "4.5.2",
     "coveralls": "3.0.1",
     "cross-env": "5.2.0",
     "css-loader": "1.0.0",
-    "extract-text-webpack-plugin": "3.0.1",
+    "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "glob": "7.1.2",
     "gulp": "3.9.1",
     "gulp-autoprefixer": "4.0.0",
@@ -68,7 +69,7 @@
     "gulp-typescript": "^4.0.2",
     "handlebars-loader": "1.6.0",
     "html-loader": "0.5.5",
-    "html-webpack-plugin": "2.30.1",
+    "html-webpack-plugin": "3.1.0",
     "jasmine-core": "2.8.0",
     "jasmine-spec-reporter": "4.2.1",
     "karma": "2.0.4",
@@ -82,7 +83,7 @@
     "karma-viewport": "^1.0.2",
     "merge2": "1.2.1",
     "node-sass": "4.9.2",
-    "postcss-loader": "1.3.3",
+    "postcss-loader": "^2.1.5",
     "raw-loader": "0.5.1",
     "remap-istanbul": "0.11.1",
     "rimraf": "2.6.2",
@@ -93,9 +94,10 @@
     "ts-helpers": "^1.1.2",
     "ts-node": "5.0.1",
     "tslint": "5.8.0",
-    "typescript": "2.6.2",
-    "webpack": "3.8.0",
-    "webpack-dev-server": "2.9.4",
+    "typescript": "2.7.2",
+    "webpack": "4.16.0",
+    "webpack-cli": "^3.0.8",
+    "webpack-dev-server": "3.1.4",
     "webpack-merge": "4.1.2"
   },
   "keywords": [
diff --git a/src/components/system-config-spec.ts b/src/components/system-config-spec.ts
index 2fccc30e..2ca1a1de 100644
--- a/src/components/system-config-spec.ts
+++ b/src/components/system-config-spec.ts
@@ -33,6 +33,7 @@ vendorPackages[`@angular-mdl/core`] = { main: `bundle/core.js` };
 
 vendorPackages['rxjs'] = { main: 'index.js' };
 vendorPackages['moment'] = { main: 'min/moment.min.js'};
+vendorPackages['@tweenjs/tween.js'] = { main: 'src/Tween.js'};
 vendorPackages['angular2-virtual-scroll'] = { main: 'dist/virtual-scroll.js'};
 
 /** Type declaration for ambient System. */
@@ -45,6 +46,7 @@ System.config({
         '@angular-mdl/core': 'vendor/@angular-mdl/core',
         'rxjs': 'vendor/rxjs',
         'moment': 'vendor/moment',
+        '@tweenjs/tween.js': 'vendor/@tweenjs/tween.js',
         'angular2-virtual-scroll': 'vendor/angular2-virtual-scroll'
     },
     packages: vendorPackages
diff --git a/src/components/virtual-table/package.json b/src/components/virtual-table/package.json
index 200ca9c7..a6405983 100644
--- a/src/components/virtual-table/package.json
+++ b/src/components/virtual-table/package.json
@@ -23,6 +23,7 @@
   "homepage": "https://github.com/mseemann/angular2-mdl-ext#readme",
   "peerDependencies": {
     "@angular-mdl/core": ">=4.0.0",
-    "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#a83b922"
+    "angular2-virtual-scroll": "github:kmcs/angular2-virtual-scroll#a83b922",
+    "@tweenjs/tween.js": "17.2.0"
   }
 }
diff --git a/tools/gulp/tasks/test.ts b/tools/gulp/tasks/test.ts
index 5f9f101b..b492d594 100644
--- a/tools/gulp/tasks/test.ts
+++ b/tools/gulp/tasks/test.ts
@@ -16,7 +16,8 @@ gulp.task(':build:test:vendor', () => {
         'systemjs/dist',
         'zone.js/dist',
         'moment',
-        'angular2-virtual-scroll'
+        'angular2-virtual-scroll',
+        "@tweenjs/tween.js"
     ];
 
     return gulpMerge(

From 6198e842398ef8918d25f6efae2c3da90fd5b5ef Mon Sep 17 00:00:00 2001
From: tk 
Date: Sat, 14 Jul 2018 16:31:22 +0200
Subject: [PATCH 16/17] fix missing import of delay operator

---
 src/e2e-app/app/virtual-table/virtual-table.component.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.ts b/src/e2e-app/app/virtual-table/virtual-table.component.ts
index 1e1dccb7..47aa09b6 100644
--- a/src/e2e-app/app/virtual-table/virtual-table.component.ts
+++ b/src/e2e-app/app/virtual-table/virtual-table.component.ts
@@ -4,6 +4,7 @@ import {
 } from '@angular/core';
 import { Observable } from 'rxjs/Observable';
 import 'rxjs/add/observable/of';
+import 'rxjs/add/operator/delay';
 import { RowDataResponseInterface } from '../../../components/virtual-table/index';
 
 @Component({

From abdf28243aa1eaf60ccda0f6e8c67032b9945fb2 Mon Sep 17 00:00:00 2001
From: tk 
Date: Wed, 18 Jul 2018 14:52:09 +0200
Subject: [PATCH 17/17] [TASK] change row selection from selection by index to
 a custom row property

---
 .../virtual-table/column.component.ts         | 15 +++-
 .../virtual-table/table.component.spec.ts     | 84 +++++++++++++++++--
 .../virtual-table/table.component.ts          | 75 ++++++++++++-----
 src/components/virtual-table/table.html       | 15 ++--
 .../virtual-table.component.html              | 20 ++++-
 5 files changed, 168 insertions(+), 41 deletions(-)

diff --git a/src/components/virtual-table/column.component.ts b/src/components/virtual-table/column.component.ts
index 8e9f2b11..33c177d0 100644
--- a/src/components/virtual-table/column.component.ts
+++ b/src/components/virtual-table/column.component.ts
@@ -14,14 +14,23 @@ export class MdlVirtualTableColumnComponent {
     get sortable(): boolean {
         return this._sortable;
     }
-    _sortable: boolean = false;
+    
     @Input('row-selection-enabled') set rowSelection(v: boolean) {
         this._rowSelection = v !== null && "" + v !== 'false';
     }
-    get rowSelection(): boolean {
+    get rowSelectionEnabled(): boolean {
         return this._rowSelection;
     }
-    _rowSelection: boolean = false;
+    @Input('select-all-enabled') set selectAllEnabled(v: boolean) {
+        this._selectAllEnabled = v !== null && "" + v !== 'false';
+    }
+    get selectAllEnabled(): boolean {
+        return this._selectAllEnabled;
+    }
+
+    private _sortable: boolean = false;
+    private _rowSelection: boolean = false;
+    private _selectAllEnabled: boolean = false;
 
     sortDirection: string;
     @ContentChild(TemplateRef) cellTemplate: TemplateRef;
diff --git a/src/components/virtual-table/table.component.spec.ts b/src/components/virtual-table/table.component.spec.ts
index 6ca4c7d0..be7e0dfb 100644
--- a/src/components/virtual-table/table.component.spec.ts
+++ b/src/components/virtual-table/table.component.spec.ts
@@ -30,6 +30,7 @@ describe('VirtualTableComponent', () => {
                 TestMdlVirtualTableComponentWithoutRowCountRequest,
                 TestMdlVirtualTableComponentWithoutRowDataRequest,
                 TestMdlVirtualTableComponentWithRowSelection,
+                TestMdlVirtualTableComponentWithRowSelectionAll,    
                 TestMdlVirtualTableComponentWithResponsiveList
             ],
             providers: [
@@ -81,7 +82,7 @@ describe('VirtualTableComponent', () => {
             setTimeout(() => {
                 expect(componentInstance.values[500]).toBe(rows[500], 'Row index 500 should be loaded');
                 done();
-            }, 20); //wait for requestAnimationFrame
+            }, 120); //wait for requestAnimationFrame
         });
     }));
 
@@ -159,7 +160,6 @@ describe('VirtualTableComponent', () => {
         });
     }));
 
-
     it('should load with auto initial load data', fakeAsync(() => {
         let fixtureAutoInit: ComponentFixture;
         TestBed.compileComponents().then(() => {
@@ -217,11 +217,11 @@ describe('VirtualTableComponent', () => {
     }));
 
 
-    it('should emit select rows', ((done) => {
+    it('should be able to select single row and all rows', ((done) => {
         fixture.destroy();
-        let fixtureSelection: ComponentFixture;
+        let fixtureSelection: ComponentFixture;
         TestBed.compileComponents().then(() => {
-            fixtureSelection = TestBed.createComponent(TestMdlVirtualTableComponentWithRowSelection);
+            fixtureSelection = TestBed.createComponent(TestMdlVirtualTableComponentWithRowSelectionAll);
             fixtureSelection.detectChanges();
 
             expect(fixtureSelection).toBeDefined();
@@ -243,7 +243,7 @@ describe('VirtualTableComponent', () => {
                 selectionCheckbox.click();
                 
                 expect(testInstance.onRowSelection).toHaveBeenCalled();
-                expect(componentInstance.selection).toEqual([1]);
+                expect(componentInstance.selection).toEqual(['1']);
                 
                 selectionCheckbox = debugTableInstance.nativeElement.querySelector("div.table div.table-row:first-child + .table-row .mdl-checkbox");
                 selectionCheckbox.click();
@@ -251,7 +251,7 @@ describe('VirtualTableComponent', () => {
                 expect(componentInstance.selection).toEqual([]);
 
                 spyOnRowSelection.calls.reset();
-                componentInstance.onChangeRowSelection(undefined, 1);
+                componentInstance.onChangeRowSelection(undefined, '1');
                 expect(testInstance.onRowSelection).not.toHaveBeenCalled();
                 
                 let selectAllCheckbox = debugTableInstance.nativeElement.querySelector("div.table-header div.table-row .header-cell .mdl-checkbox");
@@ -274,9 +274,25 @@ describe('VirtualTableComponent', () => {
                         scrollElement.scrollTop = (fixedRowHeight + 1) * scrollIndex;
                         componentVirtualScroll.refresh();
                         setTimeout(() => {
-                            expect(componentInstance.selection.length).toBe(componentInstance.values.length, "Could not select all visible rows!");
+                            // selection should contain all row ids + __all__ element
+                            expect(componentInstance.selection.length).toBe(componentInstance.values.length+1, "Could not select all visible rows!");
+
+                            selectionCheckbox = debugTableInstance.nativeElement.querySelector("div.table div.table-row:first-child + .table-row .mdl-checkbox");
+                            selectionCheckbox.click();
+
+                            expect(componentInstance.selection).toContain('__all__');
+                            expect(componentInstance.rejection).toContain('80');
+
+                            selectionCheckbox = debugTableInstance.nativeElement.querySelector("div.table div.table-row:first-child + .table-row + .table-row .mdl-checkbox");
+                            selectionCheckbox.click();
+
+                            expect(componentInstance.rejection.length).toBe(2);
 
-                            selectAllCheckbox.click();
+                            selectionCheckbox = debugTableInstance.nativeElement.querySelector("div.table div.table-row:first-child + .table-row .mdl-checkbox");
+                            selectionCheckbox.click();
+
+                            selectAllCheckbox.click(); //select all
+                            selectAllCheckbox.click(); //unselect all
                             expect(componentInstance.selection.length).toBe(0, "Could not unselect all visible rows!");
                             done();
                         }, 120);
@@ -291,6 +307,33 @@ describe('VirtualTableComponent', () => {
 
     }));
 
+    it('should select single row without all option', ((done) => {
+        let fixtureSelection: ComponentFixture;
+        TestBed.compileComponents().then(() => {
+            fixtureSelection = TestBed.createComponent(TestMdlVirtualTableComponentWithRowSelection);
+            fixtureSelection.detectChanges();
+
+            expect(fixtureSelection).toBeDefined();
+
+            let testInstance = fixtureSelection.componentInstance;
+            let debugTableInstance = fixtureSelection.debugElement.query(By.directive(MdlVirtualTableComponent));
+            let componentInstance: MdlVirtualTableComponent = debugTableInstance.
+                componentInstance;
+
+            testInstance.initData(rows.slice(0, 100));
+            fixtureSelection.autoDetectChanges();
+            fixtureSelection.whenStable().then(() => {
+                let spyOnRowSelection = spyOn(testInstance, 'onRowSelection').and.callThrough();
+                let selectionCheckbox = debugTableInstance.nativeElement.querySelector("div.table div.table-row:first-child + .table-row .mdl-checkbox");
+                selectionCheckbox.click();
+                
+                expect(testInstance.onRowSelection).toHaveBeenCalled();
+                expect(componentInstance.selection).toEqual(['1']);
+                done();
+            });
+        });
+    }));
+
     it('should show list instead of table if window size reaches the breakpoint', async(() => {
         viewport.set("mobile");
         let fixtureList: ComponentFixture;
@@ -476,6 +519,29 @@ class TestMdlVirtualTableComponentWithResponsiveList extends TestMdlVirtualTable
     }
 }
 
+@Component({
+    template: `
+    
+ + + + +
Title: {{value}}
+
+
+
+` +}) +class TestMdlVirtualTableComponentWithRowSelectionAll extends TestMdlVirtualTableComponent { + onRowSelection(event: number[]) { + + } +} + @Component({ template: `
diff --git a/src/components/virtual-table/table.component.ts b/src/components/virtual-table/table.component.ts index 553d04df..ee6f6645 100644 --- a/src/components/virtual-table/table.component.ts +++ b/src/components/virtual-table/table.component.ts @@ -88,19 +88,28 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe this._isIntersectionObserverDisabled = value != null && "" + value !== 'false'; } - get selection():number[] { - return Object.keys(this._rowSelectionByIndex).map((index) => parseInt(index)).sort(); + get selection():string[] { + return Object.keys(this._rowSelectionByKey) + .filter((key) => this._rowSelectionByKey[key] === true && key !== '' + || key === '__all__' && this._rowSelectionByKey[key] !== false) + .sort(); + } + + get rejection(): string[] { + return Object.keys(this._rowSelectionByKey) + .filter((key) => this._rowSelectionByKey[key] === false && key !== '__all__') + .sort(); } @Output() sort: EventEmitter; @Output('row-click') rowClick: EventEmitter; - @Output('row-selection-change') rowSelection: EventEmitter; + @Output('row-selection-change') rowSelection: EventEmitter<{selection: string[], rejection: string[]}>; @Output('row-count-request') rowCountRequest: EventEmitter; @Output('row-data-request') rowDataRequest: EventEmitter<{offset: number, limit: number, refresh?: boolean}>; public rows: any[]; - private _rowSelectionByIndex: {[index: number]: boolean} = {}; + private _rowSelectionByKey: {[key: string]: boolean|string} = {}; private _isInitialLoadDisabled: boolean = false; private _lastLoadedElementHash: string; private _useList: boolean = false; @@ -170,17 +179,22 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe this._nativeElementObserver.disconnect(); } } - + ngOnChanges(changes: SimpleChanges) { if((changes).rowDataStream && (changes).rowDataStream.currentValue) { + let selectionColumn = this.columns.find((col: MdlVirtualTableColumnComponent) => col.rowSelectionEnabled); let lastRowDataSubscription = this.rowDataStream.subscribe((result: RowDataResponseInterface) => { this._visibleRowOffset = result.offset; this._rowCount = this._rowCount || result.rows.length; let values = new Array(this._rowCount); for (var i = result.offset; i < (result.rows.length + result.offset); i++) { values[i] = result.rows[i - result.offset]; + if (this._rowSelectionByKey['__all__'] && this._rowSelectionByKey[values[i][selectionColumn.field]] !== false) { + this._rowSelectionByKey[values[i][selectionColumn.field]] = true; + } } this.values = values; + this.virtualScroll.previousStart = undefined; this.virtualScroll.previousEnd = undefined; this.virtualScroll.refresh(); @@ -196,36 +210,57 @@ export class MdlVirtualTableComponent implements OnInit, OnChanges, AfterViewChe this.virtualScroll.refresh(); this.cdr.markForCheck(); }); - } - + } } onSelectAllRows(selected: boolean) { - if(selected) { - for(var i = 0; i < this._rowCount; i++) { - this._rowSelectionByIndex[i] = true; + for( let key in this._rowSelectionByKey) { + if(selected) { + this._rowSelectionByKey[key] = true; + } else { + delete this._rowSelectionByKey[key]; } - } else { - this._rowSelectionByIndex = {}; } - this.rowSelection.emit(Object.keys(this._rowSelectionByIndex).map((index) => parseInt(index)).sort()); + if (selected) { + let selectionColumn = this.columns.find((col: MdlVirtualTableColumnComponent) => col.rowSelectionEnabled); + this.values.forEach((row) => { + this._rowSelectionByKey[row[selectionColumn.field]] = true; + }); + } + this._rowSelectionByKey['__all__'] = selected; + this.rowSelection.emit({selection: this.selection, rejection: this.rejection}); + this.cdr.detectChanges(); + } - onChangeRowSelection(selected: boolean, index: number) { + onChangeRowSelection(selected: boolean, key: string) { if (typeof(selected) === 'undefined') { return; } - if (this._rowSelectionByIndex[index] === selected) { + if (this._rowSelectionByKey[key] === selected) { return; } + if (selected === false) { - delete this._rowSelectionByIndex[index]; + if (this._rowSelectionByKey['__all__'] === true) { + this._rowSelectionByKey['__all__'] = 'auto'; + } + this._rowSelectionByKey[key] = false; } else { - this._rowSelectionByIndex[index] = selected; + this._rowSelectionByKey[key] = selected; + } + + + let rawSelection = Object.keys(this._rowSelectionByKey); + let selection = rawSelection.filter((key => this._rowSelectionByKey[key])).sort(); + if (selected && this._rowSelectionByKey['__all__'] === 'auto') { + this._rowSelectionByKey['__all__'] = rawSelection.length === selection.length || 'auto'; + } + + if (this.selectAllCheckbox) { + this.selectAllCheckbox.writeValue(this._rowSelectionByKey['__all__'] === true); } - let selection = Object.keys(this._rowSelectionByIndex).map((index) => parseInt(index)).sort(); - this.selectAllCheckbox.writeValue(selection.length === this._rowCount); - this.rowSelection.emit(selection); + this.rowSelection.emit({selection: this.selection, rejection: this.rejection}); } onListChange(event: ChangeEvent) { diff --git a/src/components/virtual-table/table.html b/src/components/virtual-table/table.html index e30901af..dd3ab367 100644 --- a/src/components/virtual-table/table.html +++ b/src/components/virtual-table/table.html @@ -30,10 +30,10 @@
- + {{col.label}} - {{col.label}} + {{col.label}}
@@ -42,9 +42,10 @@ [style.minHeight]="minHeight" [items]="values" (change)="onListChange($event)" (start)="onListChange($event)" (end)="onListChange($event)">
-
- +
+ +
@@ -52,7 +53,7 @@ {{value}} - - + +
\ No newline at end of file diff --git a/src/e2e-app/app/virtual-table/virtual-table.component.html b/src/e2e-app/app/virtual-table/virtual-table.component.html index dbeab26b..3aa1f9da 100644 --- a/src/e2e-app/app/virtual-table/virtual-table.component.html +++ b/src/e2e-app/app/virtual-table/virtual-table.component.html @@ -12,7 +12,7 @@
Table
[row-data-stream]="rowDataStream" [row-count-stream]="rowCountStream" (row-count-request)="onRowCountRequest()" (row-data-request)="onRowDataRequest($event)" (sort)="onSort($event)" (row-click)="onRowClick($event)" (row-selection-change)="onRowSelectionChange($event)"> - + @@ -35,7 +35,7 @@
Table
[row-data-stream]="rowDataStream" [row-count-stream]="rowCountStream" (row-count-request)="onRowCountRequest()" (row-data-request)="onRowDataRequest($event)" (sort)="onSort($event)" (row-click)="onRowClick($event)" (row-selection-change)="onRowSelectionChange($event)"> - + @@ -100,3 +100,19 @@
Table
]]>
+ + +
Selection
+ +

Just add the option row-selection-enabled to your column. Optional you can +also add select-all-enabled if you want a select all checkbox on the column header. +

+ +

+You receive each change on the row-selection-change-Event. +The data you receive is selection and rejection with list of selected rows and also the rows +that are not selected. If the selection list contains a element __all__ the user has selected +all rows, expect the list from rejection!
This is because normal you could only select rows that +are visible and already rendered, but in case of the select all option you don't want do +load your complete table to know the selected row items. +

\ No newline at end of file