diff --git a/README.md b/README.md index 07998fdf..13aa10d9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,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%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) These components support AOT and TreeShaking! 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/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 8fcb03e0..4c0315ca 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -28,10 +28,9 @@ module.exports = { enforce: 'pre', test: /.ts$/, loader: 'string-replace-loader', - options: { - search: 'moduleId: module.id,', - replace: '', - flags: 'g' + query: { + search: new RegExp('moduleId: module.id,', 'g'), + replace: '' } }, { @@ -66,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" }) @@ -74,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$/, @@ -90,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 () { @@ -106,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 239456f8..83d2e6f2 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "@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", "match-sorter": "2.2.1", @@ -50,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", @@ -67,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", @@ -78,9 +80,10 @@ "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.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", @@ -91,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 6c017b58..2ca1a1de 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 = [ @@ -32,6 +33,8 @@ 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. */ declare var System: any; @@ -42,7 +45,9 @@ System.config({ '@angular': 'vendor/@angular', '@angular-mdl/core': 'vendor/@angular-mdl/core', 'rxjs': 'vendor/rxjs', - 'moment': 'vendor/moment' + 'moment': 'vendor/moment', + '@tweenjs/tween.js': 'vendor/@tweenjs/tween.js', + 'angular2-virtual-scroll': 'vendor/angular2-virtual-scroll' }, packages: vendorPackages }); 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" ] } } diff --git a/src/components/virtual-table/README.md b/src/components/virtual-table/README.md new file mode 100644 index 00000000..4960be62 --- /dev/null +++ b/src/components/virtual-table/README.md @@ -0,0 +1,28 @@ +# Virtual-Table + +### Installing + +Install the package and angular2-virtual-scroll! + + npm i --save @angular-mdl/virtual-table + +Please use the angular2-virtual-scroll from the peer dependency reference! + +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). + +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 + + 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 new file mode 100644 index 00000000..33c177d0 --- /dev/null +++ b/src/components/virtual-table/column.component.ts @@ -0,0 +1,37 @@ +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 = v !== null && "" + v !== 'false'; + } + get sortable(): boolean { + return this._sortable; + } + + @Input('row-selection-enabled') set rowSelection(v: boolean) { + this._rowSelection = v !== null && "" + v !== 'false'; + } + get rowSelectionEnabled(): boolean { + return this._rowSelection; + } + @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; +} \ 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..c7ff92b2 --- /dev/null +++ b/src/components/virtual-table/index.ts @@ -0,0 +1,5 @@ +export * from './module'; +export * from './table.component'; +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 new file mode 100644 index 00000000..d84d7ba7 --- /dev/null +++ b/src/components/virtual-table/module.ts @@ -0,0 +1,34 @@ +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, + MdlVirtualTableListComponent + ], + declarations: [ + MdlVirtualTableComponent, + MdlVirtualTableColumnComponent, + MdlVirtualTableListComponent + ] +}) +export class MdlVirtualTableModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: MdlVirtualTableModule, + providers: [] + }; + } +} \ No newline at end of file diff --git a/src/components/virtual-table/package.json b/src/components/virtual-table/package.json new file mode 100644 index 00000000..a6405983 --- /dev/null +++ b/src/components/virtual-table/package.json @@ -0,0 +1,29 @@ +{ + "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": "github:kmcs/angular2-virtual-scroll#a83b922", + "@tweenjs/tween.js": "17.2.0" + } +} 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 new file mode 100644 index 00000000..ab1dc830 --- /dev/null +++ b/src/components/virtual-table/table.component.scss @@ -0,0 +1,175 @@ +:host.flex-height, +.flex-height virtual-scroll, +.flex-height.wrapper { + display: flex; + width: 100%; +} + +.flex-height.wrapper { + flex-direction: column; +} + +.flex-height virtual-scroll { + flex: 1; +} + +.wrapper { + 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; + 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; +} +.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; + content: 'arrow_drop_down arrow_drop_up'; + vertical-align: middle; + letter-spacing: -11px; + 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 new file mode 100644 index 00000000..be7e0dfb --- /dev/null +++ b/src/components/virtual-table/table.component.spec.ts @@ -0,0 +1,566 @@ +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, 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 }); + } + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MdlVirtualTableModule.forRoot() + ], + declarations: [TestMdlVirtualTableComponent, + TestMdlVirtualTableComponentWithAutoInit, + TestMdlVirtualTableComponentWithoutRowCountRequest, + TestMdlVirtualTableComponentWithoutRowDataRequest, + TestMdlVirtualTableComponentWithRowSelection, + TestMdlVirtualTableComponentWithRowSelectionAll, + TestMdlVirtualTableComponentWithResponsiveList + ], + providers: [ + ] + }); + + TestBed.compileComponents().then(() => { + fixture = TestBed.createComponent(TestMdlVirtualTableComponent); + fixture.detectChanges(); + }); + })); + + afterEach(() => { + fixture = null + }); + + it('should instantiate the component', async(() => { + expect(fixture).toBeDefined(); + })); + + 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(); + }, 120); //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 + }); + }); + + 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(); + }); + })); + + + it('should be able to select single row and all rows', ((done) => { + fixture.destroy(); + let fixtureSelection: ComponentFixture; + TestBed.compileComponents().then(() => { + fixtureSelection = TestBed.createComponent(TestMdlVirtualTableComponentWithRowSelectionAll); + 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(() => { + // 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); + + 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); + + }, 120); //wait for requestAnimationFrame + }, 120); //wait for requestAnimationFrame + + }); + + + }); + + })); + + 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; + 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: ` +
+ + + +
Title: {{value}}
+
+
+
+ ` +}) +class TestMdlVirtualTableComponent { + + @ViewChild('table') table: MdlVirtualTableComponent; + + protected rows: any[]; + + public visible: boolean = true; + + public rowCountStream: Observable; + public rowDataStream: Observable<{ rows: any[], offset: number, limit: number }>; + + onSort(data: { column: string, direction: string }) { + } + + onRowClick(event: any) { + } + + requestRowCount() { + return Observable.of(this.rows.length || 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): 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 }); + } + + initData(rows: any[]) { + this.rows = rows; + this.table.refresh(true); + } + + 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 { + +} + + +@Component({ + template: ` +
+ + + +
Title: {{value}}
+
+ + + Index: {{row?._index}}
+ My Label: {{row?._label}} +
+
+
+
+` +}) +class TestMdlVirtualTableComponentWithResponsiveList extends TestMdlVirtualTableComponent { + + onListItemClick(data: { event: MouseEvent, row: any, index: number }) { + + } +} + +@Component({ + template: ` +
+ + + + +
Title: {{value}}
+
+
+
+` +}) +class TestMdlVirtualTableComponentWithRowSelectionAll extends TestMdlVirtualTableComponent { + onRowSelection(event: number[]) { + + } +} + +@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 new file mode 100644 index 00000000..ee6f6645 --- /dev/null +++ b/src/components/virtual-table/table.component.ts @@ -0,0 +1,326 @@ +import { + Component, + Input, + Output, + ChangeDetectorRef, + ChangeDetectionStrategy, + ViewChild, + ContentChildren, + ContentChild, + EventEmitter, + HostListener, + OnInit, + OnChanges, + OnDestroy, + AfterViewChecked, + SimpleChanges, + HostBinding, + QueryList, + ElementRef +} from '@angular/core'; +import {VirtualScrollComponent, ChangeEvent} from 'angular2-virtual-scroll'; +import {MdlVirtualTableColumnComponent} from './column.component'; +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; + +@Component({ + moduleId: module.id, + selector: 'mdl-virtual-table', + changeDetection: ChangeDetectionStrategy.Default, + 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; + + @ContentChild(MdlVirtualTableListComponent) + responsiveList: MdlVirtualTableListComponent; + + @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; + } + set isFlexHeight(value) { + this._isFlexHeight = value != null && "" + value !== 'false'; + } + + @Input('init-refresh-disabled') + get isInitialLoadDisabled(): boolean { + return this._isInitialLoadDisabled; + } + set isInitialLoadDisabled(value) { + this._isInitialLoadDisabled = value != null && "" + value !== 'false'; + } + + @Input('intersection-observer-disabled') + get isIntersectionObserverDisabled(): boolean { + return this._isIntersectionObserverDisabled; + } + set isIntersectionObserverDisabled(value) { + this._isIntersectionObserverDisabled = value != null && "" + value !== 'false'; + } + + 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<{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 _rowSelectionByKey: {[key: string]: boolean|string} = {}; + 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.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() { + 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 && this.virutalScrollElement.nativeElement + && !this.isIntersectionObserverDisabled) { + this._nativeElementObserver = new IntersectionObserver((entries: any[]) => { + + if(!(entries.shift().intersectionRatio > 0)) { + return; + } + 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 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(); + this.cdr.markForCheck(); + }); + } + if((changes).rowCountStream && (changes).rowCountStream.currentValue) { + this.rowCountStream.subscribe((count: number) => { + this._rowCount = count; + this.values = new Array(count); + this.virtualScroll.previousStart = undefined; + this.virtualScroll.previousEnd = undefined; + this.virtualScroll.refresh(); + this.cdr.markForCheck(); + }); + } + } + + onSelectAllRows(selected: boolean) { + for( let key in this._rowSelectionByKey) { + if(selected) { + this._rowSelectionByKey[key] = true; + } else { + delete this._rowSelectionByKey[key]; + } + } + 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, key: string) { + if (typeof(selected) === 'undefined') { + return; + } + if (this._rowSelectionByKey[key] === selected) { + return; + } + + if (selected === false) { + if (this._rowSelectionByKey['__all__'] === true) { + this._rowSelectionByKey['__all__'] = 'auto'; + } + this._rowSelectionByKey[key] = false; + } else { + 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); + } + this.rowSelection.emit({selection: this.selection, rejection: this.rejection}); + } + + onListChange(event: ChangeEvent) { + let limit = event.end - event.start; + 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) { + return; + } + + col.sortDirection = col.sortDirection === 'asc' ? 'desc' : 'asc'; + this.sort.emit({column: col.field, direction: col.sortDirection}); + } + + refresh(withRowCount: boolean = false) { + + if(this.rowCountRequest.observers.length === 0) { + throw new Error("mdl-virtual-table component has no rowCountRequest Listener"); + } + if(this.rowDataRequest.observers.length === 0) { + throw new Error("mdl-virtual-table component has no rowDataRequest Listener"); + } + + if(withRowCount) { + 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({ + 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..dd3ab367 --- /dev/null +++ b/src/components/virtual-table/table.html @@ -0,0 +1,59 @@ +
+
+ + + +
+ + {{col.label}} + +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + {{col.label}} + + {{col.label}} +
+
+
+
+ +
+
+
+ + +
+
+
+
+ + {{value}} + + + + +
\ 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 07b7be2a..b00c5e5b 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..3aa1f9da --- /dev/null +++ b/src/e2e-app/app/virtual-table/virtual-table.component.html @@ -0,0 +1,118 @@ +
+
+

THE VIRTUAL TABLE

+ +

Light weight data table with smooth virtual scrolling.

+ +
Table
+ +
+ + + + + + +
Title: {{value}}
+
+
+ + + {{row?._id}}
+ {{row?._label}} +
+
+
+ +
+ +
+  
+    
+    
+    
+      
+        
Title: {{value}}
+
+
+ + + {{row?._id}}
+ {{row?._label}} +
+
+ + ]]> +
+ +
+  ;
+    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);
+      }
+
+      onListItemClick(event) {
+          console.log("on list item click", event);
+      }
+
+      onRowSelectionChange(event) {
+          console.log("change row selection", 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);
+      }
+  }
+  ]]>
+
+
+ + +
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 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..47aa09b6 --- /dev/null +++ b/src/e2e-app/app/virtual-table/virtual-table.component.ts @@ -0,0 +1,57 @@ +import { + ChangeDetectionStrategy, + Component +} 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({ + selector: 'virtual-table-demo', + templateUrl: 'virtual-table.component.html', + 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); + } + + 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(): Observable { + return Observable.of(500).delay(1000);; + } + + requestRowData(offset, limit): Observable { + 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/tools/gulp/tasks/test.ts b/tools/gulp/tasks/test.ts index a1a7311c..b492d594 100644 --- a/tools/gulp/tasks/test.ts +++ b/tools/gulp/tasks/test.ts @@ -15,7 +15,9 @@ gulp.task(':build:test:vendor', () => { 'rxjs', 'systemjs/dist', 'zone.js/dist', - 'moment' + 'moment', + 'angular2-virtual-scroll', + "@tweenjs/tween.js" ]; return gulpMerge(