From 33790243daf55b265061b175bd092ec0063fc315 Mon Sep 17 00:00:00 2001 From: Ed Morales Date: Mon, 2 Oct 2017 15:43:28 -0700 Subject: [PATCH] feat(data-table): allow dynamic row heights in data table even when using its virtual scroll (#898) * fix(data-table): remove cell 48 height css rule so its dynamic * feat(data-table): allow dynamic row heights in data table virtual scroll now we will cache the height of every row and recalculate the height as we keep scrolling * chore(data-table): update demo and docs with better examples and load data from service * chore(): add comments to explain code in PR --- .../data-table/data-table.component.html | 318 +++--------------- .../data-table/data-table.component.ts | 168 ++------- src/app/services/internal-docs.service.ts | 18 + .../data-table-cell.component.scss | 1 - .../data-table-row.component.ts | 8 + .../core/data-table/data-table.component.html | 2 +- .../core/data-table/data-table.component.ts | 180 ++++++---- 7 files changed, 223 insertions(+), 472 deletions(-) diff --git a/src/app/components/components/data-table/data-table.component.html b/src/app/components/components/data-table/data-table.component.html index e17ce8c8a8..35dc040949 100644 --- a/src/app/components/components/data-table/data-table.component.html +++ b/src/app/components/components/data-table/data-table.component.html @@ -13,25 +13,25 @@

with custom headings, columns, and inline editing

- + - +
- comment - Comments - {{column.label}} + comment + Comments +
- - {{column.format ? column.format(row[column.name]) : row[column.name]}} + +
@@ -45,25 +45,25 @@

with custom headings, columns, and inline editing

- + - +
- comment - Comments - { {column.label}} + comment + Comments +
- - { {column.format ? column.format(row[column.name]) : row[column.name]}} + +
@@ -75,69 +75,19 @@

with custom headings, columns, and inline editing

import { ITdDataTableColumn } from '@covalent/core'; import { TdDialogService } from '@covalent/core'; ... - const NUMBER_FORMAT: (v: any) => any = (v: number) => v; const DECIMAL_FORMAT: (v: any) => any = (v: number) => v.toFixed(2); ... }) export class Demo { columns: ITdDataTableColumn[] = [ - { name: 'name', label: 'Dessert (100g serving)' }, - { name: 'type', label: 'Type', filter: true }, - { name: 'calories', label: 'Calories', numeric: true, format: NUMBER_FORMAT }, - { name: 'fat', label: 'Fat (g)', numeric: true, format: DECIMAL_FORMAT }, - { name: 'carbs', label: 'Carbs (g)', numeric: true, format: NUMBER_FORMAT }, - { name: 'protein', label: 'Protein (g)', numeric: true, format: DECIMAL_FORMAT }, - { name: 'sodium', label: 'Sodium (mg)', numeric: true, format: NUMBER_FORMAT }, - { name: 'calcium', label: 'Calcium (%)', numeric: true, format: NUMBER_FORMAT }, - { name: 'iron', label: 'Iron (%)', numeric: true, format: NUMBER_FORMAT }, + { name: 'first_name', label: 'First Name' }, + { name: 'last_name', label: 'Last Name' }, + { name: 'gender', label: 'Gender' }, + { name: 'email', label: 'Email' }, + { name: 'balance', label: 'Balance', numeric: true, format: DECIMAL_FORMAT }, ]; - data: any[] = { - 'id': 1, - 'name': 'Frozen yogurt', - 'type': 'Ice cream', - 'calories': 159.0, - 'fat': 6.0, - 'carbs': 24.0, - 'protein': 4.0, - 'sodium': 87.0, - 'calcium': 14.0, - 'iron': 1.0, - 'comments': 'I love froyo!', - }, { - 'id': 2, - 'name': 'Ice cream sandwich', - 'type': 'Ice cream', - 'calories': 237.0, - 'fat': 9.0, - 'carbs': 37.0, - 'protein': 4.3, - 'sodium': 129.0, - 'calcium': 8.0, - 'iron': 1.0, - }, { - 'id': 3, - 'name': 'Eclair', - 'type': 'Pastry', - 'calories': 262.0, - 'fat': 16.0, - 'carbs': 24.0, - 'protein': 6.0, - 'sodium': 337.0, - 'calcium': 6.0, - 'iron': 7.0, - }, { - 'id': 4, - 'name': 'Cupcake', - 'type': 'Pastry', - 'calories': 305.0, - 'fat': 3.7, - 'carbs': 67.0, - 'protein': 4.3, - 'sodium': 413.0, - 'calcium': 3.0, - 'iron': 8.0, - }]; + basicData: any[] = [...]; // see data json constructor(private _dialogService: TdDialogService) {} @@ -154,6 +104,9 @@

with custom headings, columns, and inline editing

} ]]> +

Data:

+ + @@ -161,7 +114,7 @@

with custom headings, columns, and inline editing

Basic Data Table

-

with nested data, formatting, configurable width columns and templates

+

with formatting, configurable width columns and templates

@@ -169,12 +122,8 @@

with nested data, formatting, configurable width columns - -
- {{value}} - - star -
+ +
@@ -186,12 +135,8 @@

with nested data, formatting, configurable width columns - -
- { {value}} // or { {row[column]}} - - star -
+ +
]]> @@ -201,56 +146,25 @@

with nested data, formatting, configurable width columns any = (v: number) => v; const DECIMAL_FORMAT: (v: any) => any = (v: number) => v.toFixed(2); ... }) - export class Demo { + export class Demo implements OnInit { configWidthColumns: ITdDataTableColumn[] = [ - { name: 'name', label: 'Dessert (100g serving)', width: 300 }, - { name: 'type', label: 'Type', width: { min: 150, max: 250 } }, - { name: 'calories', label: 'Calories', numeric: true, format: NUMBER_FORMAT}, - { name: 'fat', label: 'Fat (g)', numeric: true, format: DECIMAL_FORMAT}, + { name: 'first_name', label: 'First name', width: 150 }, + { name: 'last_name', label: 'Last name', width: { min: 150, max: 250 } }, + { name: 'gender', label: 'Gender'}, + { name: 'email', label: 'Email', width: 250}, + { name: 'img', label: 'Avatar', width: 50}, ]; - basicData: any[] = [ - { - 'id': 1, - 'food': { - 'name': 'Frozen yogurt', - 'type': 'Ice cream', - }, - 'calories': 159.0, - 'fat': 6.0, - }, { - 'id': 2, - 'food': { - 'name': 'Ice cream sandwich', - 'type': 'Ice cream', - }, - 'calories': 237.0, - 'fat': 9.0, - }, { - 'id': 3, - 'food': { - 'name': 'Eclair', - 'type': 'Pastry', - }, - 'calories': 262.0, - 'fat': 16.0, - }, { - 'id': 4, - 'food': { - 'name': 'Cupcake', - 'type': 'Pastry', - }, - 'calories': 305.0, - 'fat': 3.7, - } - ]; + basicData: any[] = [...]; // see data json } ]]> +

Data:

+ + @@ -296,7 +210,7 @@

No results to display.

Rows per page: - + {{size}} @@ -340,7 +254,7 @@

No results to display.

Rows per page: - + { {size} } @@ -354,148 +268,19 @@

No results to display.

import { TdDataTableService, TdDataTableSortingOrder, ITdDataTableSortChangeEvent, ITdDataTableColumn } from '@covalent/core'; import { IPageChangeEvent } from '@covalent/core'; ... - const NUMBER_FORMAT: (v: any) => any = (v: number) => v; const DECIMAL_FORMAT: (v: any) => any = (v: number) => v.toFixed(2); ... }) export class Demo { columns: ITdDataTableColumn[] = [ - { name: 'name', label: 'Dessert (100g serving)', sortable: true, width: 200 }, - { name: 'type', label: 'Type', filter: true }, - { name: 'calories', label: 'Calories', numeric: true, format: NUMBER_FORMAT, sortable: true, hidden: false }, - { name: 'fat', label: 'Fat (g)', numeric: true, format: DECIMAL_FORMAT, sortable: true }, - { name: 'carbs', label: 'Carbs (g)', numeric: true, format: NUMBER_FORMAT }, - { name: 'protein', label: 'Protein (g)', numeric: true, format: DECIMAL_FORMAT }, - { name: 'sodium', label: 'Sodium (mg)', numeric: true, format: NUMBER_FORMAT }, - { name: 'calcium', label: 'Calcium (%)', numeric: true, format: NUMBER_FORMAT }, - { name: 'iron', label: 'Iron (%)', numeric: true, format: NUMBER_FORMAT }, + { name: 'first_name', label: 'First Name', sortable: true, width: 150 }, + { name: 'last_name', label: 'Last Name', filter: true }, + { name: 'gender', label: 'Gender', hidden: false }, + { name: 'email', label: 'Email', sortable: true, width: 250 }, + { name: 'balance', label: 'Balance', numeric: true, format: DECIMAL_FORMAT }, ]; - data: any[] = [ - { - 'id': 1, - 'name': 'Frozen yogurt', - 'type': 'Ice cream', - 'calories': 159.0, - 'fat': 6.0, - 'carbs': 24.0, - 'protein': 4.0, - 'sodium': 87.0, - 'calcium': 14.0, - 'iron': 1.0, - 'comments': 'I love froyo!', - }, { - 'id': 2, - 'name': 'Ice cream sandwich', - 'type': 'Ice cream', - 'calories': 237.0, - 'fat': 9.0, - 'carbs': 37.0, - 'protein': 4.3, - 'sodium': 129.0, - 'calcium': 8.0, - 'iron': 1.0, - }, { - 'id': 3, - 'name': 'Eclair', - 'type': 'Pastry', - 'calories': 262.0, - 'fat': 16.0, - 'carbs': 24.0, - 'protein': 6.0, - 'sodium': 337.0, - 'calcium': 6.0, - 'iron': 7.0, - }, { - 'id': 4, - 'name': 'Cupcake', - 'type': 'Pastry', - 'calories': 305.0, - 'fat': 3.7, - 'carbs': 67.0, - 'protein': 4.3, - 'sodium': 413.0, - 'calcium': 3.0, - 'iron': 8.0, - }, { - 'id': 5, - 'name': 'Jelly bean', - 'type': 'Candy', - 'calories': 375.0, - 'fat': 0.0, - 'carbs': 94.0, - 'protein': 0.0, - 'sodium': 50.0, - 'calcium': 0.0, - 'iron': 0.0, - }, { - 'id': 6, - 'name': 'Lollipop', - 'type': 'Candy', - 'calories': 392.0, - 'fat': 0.2, - 'carbs': 98.0, - 'protein': 0.0, - 'sodium': 38.0, - 'calcium': 0.0, - 'iron': 2.0, - }, { - 'id': 7, - 'name': 'Honeycomb', - 'type': 'Other', - 'calories': 408.0, - 'fat': 3.2, - 'carbs': 87.0, - 'protein': 6.5, - 'sodium': 562.0, - 'calcium': 0.0, - 'iron': 45.0, - }, { - 'id': 8, - 'name': 'Donut', - 'type': 'Pastry', - 'calories': 452.0, - 'fat': 25.0, - 'carbs': 51.0, - 'protein': 4.9, - 'sodium': 326.0, - 'calcium': 2.0, - 'iron': 22.0, - }, { - 'id': 9, - 'name': 'KitKat', - 'type': 'Candy', - 'calories': 518.0, - 'fat': 26.0, - 'carbs': 65.0, - 'protein': 7.0, - 'sodium': 54.0, - 'calcium': 12.0, - 'iron': 6.0, - }, { - 'id': 10, - 'name': 'Chocolate', - 'type': 'Candy', - 'calories': 518.0, - 'fat': 26.0, - 'carbs': 65.0, - 'protein': 7.0, - 'sodium': 54.0, - 'calcium': 12.0, - 'iron': 6.0, - }, { - 'id': 11, - 'name': 'Chamoy', - 'type': 'Candy', - 'calories': 518.0, - 'fat': 26.0, - 'carbs': 65.0, - 'protein': 7.0, - 'sodium': 54.0, - 'calcium': 12.0, - 'iron': 6.0, - }, - ]; + data: any[] = [...]; // see json data filteredData: any[] = this.data; filteredTotal: number = this.data.length; @@ -503,8 +288,8 @@

No results to display.

searchTerm: string = ''; fromRow: number = 1; currentPage: number = 1; - pageSize: number = 10; - sortBy: string = 'name'; + pageSize: number = 50; + sortBy: string = 'first_name'; selectedRows: any[] = []; sortOrder: TdDataTableSortingOrder = TdDataTableSortingOrder.Descending; @@ -550,6 +335,9 @@

No results to display.

} ]]> +

Data:

+ +
@@ -575,10 +363,10 @@

No results to display.

- Hide calories + Hide gender - Type column is searchable (search for candy) + Type column is searchable (search for lifsey)
diff --git a/src/app/components/components/data-table/data-table.component.ts b/src/app/components/components/data-table/data-table.component.ts index 474c89d5bf..02575fdc22 100644 --- a/src/app/components/components/data-table/data-table.component.ts +++ b/src/app/components/components/data-table/data-table.component.ts @@ -7,6 +7,10 @@ import { TdDataTableSortingOrder, TdDataTableService, TdDataTableComponent, import { IPageChangeEvent } from '../../../../platform/core'; import { TdDialogService } from '../../../../platform/core'; +import { InternalDocsService } from '../../../services'; + +import { toPromise } from 'rxjs/operator/toPromise'; + const NUMBER_FORMAT: (v: any) => any = (v: number) => v; const DECIMAL_FORMAT: (v: any) => any = (v: number) => v.toFixed(2); @@ -78,167 +82,41 @@ export class DataTableDemoComponent implements OnInit { }]; configWidthColumns: ITdDataTableColumn[] = [ - { name: 'name', label: 'Dessert (100g serving)', width: 300 }, - { name: 'type', label: 'Type', width: { min: 150, max: 250 } }, - { name: 'calories', label: 'Calories', numeric: true, format: NUMBER_FORMAT}, - { name: 'fat', label: 'Fat (g)', numeric: true, format: DECIMAL_FORMAT}, + { name: 'first_name', label: 'First name', width: 150 }, + { name: 'last_name', label: 'Last name', width: { min: 150, max: 250 } }, + { name: 'gender', label: 'Gender'}, + { name: 'email', label: 'Email', width: 250}, + { name: 'img', label: 'Avatar', width: 50}, ]; columns: ITdDataTableColumn[] = [ - { name: 'name', label: 'Dessert (100g serving)', sortable: true, width: 200 }, - { name: 'type', label: 'Type', filter: true }, - { name: 'calories', label: 'Calories', numeric: true, format: NUMBER_FORMAT, sortable: true, hidden: false }, - { name: 'fat', label: 'Fat (g)', numeric: true, format: DECIMAL_FORMAT, sortable: true }, - { name: 'carbs', label: 'Carbs (g)', numeric: true, format: NUMBER_FORMAT }, - { name: 'protein', label: 'Protein (g)', numeric: true, format: DECIMAL_FORMAT }, - { name: 'sodium', label: 'Sodium (mg)', numeric: true, format: NUMBER_FORMAT }, - { name: 'calcium', label: 'Calcium (%)', numeric: true, format: NUMBER_FORMAT }, - { name: 'iron', label: 'Iron (%)', numeric: true, format: NUMBER_FORMAT }, + { name: 'first_name', label: 'First Name', sortable: true, width: 150 }, + { name: 'last_name', label: 'Last Name', filter: true }, + { name: 'gender', label: 'Gender', hidden: false }, + { name: 'email', label: 'Email', sortable: true, width: 250 }, + { name: 'balance', label: 'Balance', numeric: true, format: DECIMAL_FORMAT }, ]; - data: any[] = [ - { - 'id': 1, - 'name': 'Frozen yogurt', - 'type': 'Ice cream', - 'calories': 159.0, - 'fat': 6.0, - 'carbs': 24.0, - 'protein': 4.0, - 'sodium': 87.0, - 'calcium': 14.0, - 'iron': 1.0, - 'comments': 'I love froyo!', - }, { - 'id': 2, - 'name': 'Ice cream sandwich', - 'type': 'Ice cream', - 'calories': 237.0, - 'fat': 9.0, - 'carbs': 37.0, - 'protein': 4.3, - 'sodium': 129.0, - 'calcium': 8.0, - 'iron': 1.0, - }, { - 'id': 3, - 'name': 'Eclair', - 'type': 'Pastry', - 'calories': 262.0, - 'fat': 16.0, - 'carbs': 24.0, - 'protein': 6.0, - 'sodium': 337.0, - 'calcium': 6.0, - 'iron': 7.0, - }, { - 'id': 4, - 'name': 'Cupcake', - 'type': 'Pastry', - 'calories': 305.0, - 'fat': 3.7, - 'carbs': 67.0, - 'protein': 4.3, - 'sodium': 413.0, - 'calcium': 3.0, - 'iron': 8.0, - }, { - 'id': 5, - 'name': 'Jelly bean', - 'type': 'Candy', - 'calories': 375.0, - 'fat': 0.0, - 'carbs': 94.0, - 'protein': 0.0, - 'sodium': 50.0, - 'calcium': 0.0, - 'iron': 0.0, - }, { - 'id': 6, - 'name': 'Lollipop', - 'type': 'Candy', - 'calories': 392.0, - 'fat': 0.2, - 'carbs': 98.0, - 'protein': 0.0, - 'sodium': 38.0, - 'calcium': 0.0, - 'iron': 2.0, - }, { - 'id': 7, - 'name': 'Honeycomb', - 'type': 'Other', - 'calories': 408.0, - 'fat': 3.2, - 'carbs': 87.0, - 'protein': 6.5, - 'sodium': 562.0, - 'calcium': 0.0, - 'iron': 45.0, - }, { - 'id': 8, - 'name': 'Donut', - 'type': 'Pastry', - 'calories': 452.0, - 'fat': 25.0, - 'carbs': 51.0, - 'protein': 4.9, - 'sodium': 326.0, - 'calcium': 2.0, - 'iron': 22.0, - }, { - 'id': 9, - 'name': 'KitKat', - 'type': 'Candy', - 'calories': 518.0, - 'fat': 26.0, - 'carbs': 65.0, - 'protein': 7.0, - 'sodium': 54.0, - 'calcium': 12.0, - 'iron': 6.0, - }, { - 'id': 10, - 'name': 'Chocolate', - 'type': 'Candy', - 'calories': 518.0, - 'fat': 26.0, - 'carbs': 65.0, - 'protein': 7.0, - 'sodium': 54.0, - 'calcium': 12.0, - 'iron': 6.0, - }, { - 'id': 11, - 'name': 'Chamoy', - 'type': 'Candy', - 'calories': 518.0, - 'fat': 26.0, - 'carbs': 65.0, - 'protein': 7.0, - 'sodium': 54.0, - 'calcium': 12.0, - 'iron': 6.0, - }, - ]; - basicData: any[] = this.data.slice(0, 4); + data: any[]; + basicData: any[]; selectable: boolean = true; clickable: boolean = false; multiple: boolean = true; filterColumn: boolean = true; - filteredData: any[] = this.data; - filteredTotal: number = this.data.length; + filteredData: any[]; + filteredTotal: number ; selectedRows: any[] = []; searchTerm: string = ''; fromRow: number = 1; currentPage: number = 1; - pageSize: number = 10; - sortBy: string = 'name'; + pageSize: number = 50; + sortBy: string = 'first_name'; sortOrder: TdDataTableSortingOrder = TdDataTableSortingOrder.Descending; constructor(private _dataTableService: TdDataTableService, + private _internalDocsService: InternalDocsService, private _dialogService: TdDialogService) {} openPrompt(row: any, name: string): void { @@ -252,7 +130,9 @@ export class DataTableDemoComponent implements OnInit { }); } - ngOnInit(): void { + async ngOnInit(): Promise { + this.data = await toPromise.call(this._internalDocsService.queryData()); + this.basicData = this.data.slice(0, 10); this.filter(); } diff --git a/src/app/services/internal-docs.service.ts b/src/app/services/internal-docs.service.ts index 3fe4c1c0a5..9443462348 100644 --- a/src/app/services/internal-docs.service.ts +++ b/src/app/services/internal-docs.service.ts @@ -41,4 +41,22 @@ export class InternalDocsService { }); } + queryData(): Observable { + return new Observable((subscriber: Subscriber) => { + this._http.get(INTERNAL_DOCS_URL + '/data.json').subscribe((response: Response) => { + let data: ITemplate[]; + try { + data = response.json(); + } catch (e) { + data = []; + subscriber.error(); + } + subscriber.next(data); + subscriber.complete(); + }, (error: any) => { + subscriber.error(); + }); + }); + } + } diff --git a/src/platform/core/data-table/data-table-cell/data-table-cell.component.scss b/src/platform/core/data-table/data-table-cell/data-table-cell.component.scss index f3f809197b..c283623b12 100644 --- a/src/platform/core/data-table/data-table-cell/data-table-cell.component.scss +++ b/src/platform/core/data-table/data-table-cell/data-table-cell.component.scss @@ -6,7 +6,6 @@ padding: 0; > .td-data-table-cell-content-wrapper { padding: 0 28px 0 28px; - height: 48px; } &:first-child > .td-data-table-cell-content-wrapper { diff --git a/src/platform/core/data-table/data-table-row/data-table-row.component.ts b/src/platform/core/data-table/data-table-row/data-table-row.component.ts index 31f5efea99..6d3b65455f 100644 --- a/src/platform/core/data-table/data-table-row/data-table-row.component.ts +++ b/src/platform/core/data-table/data-table-row/data-table-row.component.ts @@ -40,6 +40,14 @@ export class TdDataTableRowComponent { return this._selected; } + get height(): number { + let height: number = 48; + if (this._elementRef.nativeElement) { + height = (this._elementRef.nativeElement).getBoundingClientRect().height; + } + return height; + } + constructor(private _elementRef: ElementRef, private _renderer: Renderer2) { this._renderer.addClass(this._elementRef.nativeElement, 'td-data-table-row'); } diff --git a/src/platform/core/data-table/data-table.component.html b/src/platform/core/data-table/data-table.component.html index 6faef52a02..511ed8bac3 100644 --- a/src/platform/core/data-table/data-table.component.html +++ b/src/platform/core/data-table/data-table.component.html @@ -43,7 +43,7 @@ #dtRow [tabIndex]="selectable ? 0 : -1" [selected]="(clickable || selectable) && isRowSelected(row)" - *ngFor="let row of data | slice:fromRow:toRow; let rowIndex = index" + *ngFor="let row of virtualData; let rowIndex = index" (click)="handleRowClick(row, $event)" (keyup)="selectable && _rowKeyup($event, row, rowIndex)" (keydown.space)="blockEvent($event)" diff --git a/src/platform/core/data-table/data-table.component.ts b/src/platform/core/data-table/data-table.component.ts index eb77101ef4..f8b431f1f3 100644 --- a/src/platform/core/data-table/data-table.component.ts +++ b/src/platform/core/data-table/data-table.component.ts @@ -73,6 +73,16 @@ export interface IInternalColumnWidth { max?: boolean; } +/** + * Constant to set the rows offset before and after the viewport + */ +const TD_VIRTUAL_OFFSET: number = 2; + +/** + * Constant to set default row height if none is provided + */ +const TD_VIRTUAL_DEFAULT_ROW_HEIGHT: number = 48; + @Component({ providers: [ TD_DATA_TABLE_CONTROL_VALUE_ACCESSOR ], selector: 'td-data-table', @@ -103,74 +113,52 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After private _verticalScrollSubs: Subscription; private _horizontalScrollSubs: Subscription; private _scrollHorizontalOffset: number = 0; - private _scrollVerticalOffset: number = 0; - private _hostHeight: number = 0; private _onHorizontalScroll: Subject = new Subject(); private _onVerticalScroll: Subject = new Subject(); - /** - * Returns the height of the row - * For now we assume thats 49px. - */ - get rowHeight(): number { - return 49; - } + // Array of cached row heights to allow dynamic row heights + private _rowHeightCache: number[] = []; + // Total pseudo height of all the elements + private _totalHeight: number = 0; + // Total host height for the viewport + private _hostHeight: number = 0; + // Scrolled vertical pixels + private _scrollVerticalOffset: number = 0; + // Style to move the content a certain offset depending on scrolled offset + private _offsetTransform: SafeStyle; - /** - * Returns the number of rows that are rendered outside the viewport. - */ - get offsetRows(): number { - return 2; - } + // Variables that set from and to which rows will be rendered + private _fromRow: number = 0; + private _toRow: number = 0; /** * Returns the offset style with a proper calculation on how much it should move * over the y axis of the total height */ get offsetTransform(): SafeStyle { - let offset: number = 0; - if (this._scrollVerticalOffset > (this.offsetRows * this.rowHeight)) { - offset = this.fromRow * this.rowHeight; - } - return this._domSanitizer.bypassSecurityTrustStyle('translateY(' + (offset - this.totalHeight) + 'px)'); + return this._offsetTransform; } /** * Returns the assumed total height of the rows */ get totalHeight(): number { - if (this._data) { - return this._data.length * this.rowHeight; - } - return 0; + return this._totalHeight; } /** * Returns the initial row to render in the viewport */ get fromRow(): number { - if (this._data) { - // we calculate how many rows would have been scrolled minus an offset - let fromRow: number = Math.floor((this._scrollVerticalOffset / this.rowHeight)) - this.offsetRows; - return fromRow > 0 ? fromRow : 0; - } - return 0; + return this._fromRow; } /** * Returns the last row to render in the viewport */ get toRow(): number { - if (this._data) { - // we calculate how many rows would fit in the viewport and add an offset - let toRow: number = Math.floor((this._hostHeight / this.rowHeight)) + this.fromRow + (this.offsetRows * 2); - if (toRow > this._data.length) { - toRow = this._data.length; - } - return toRow; - } - return 0; + return this._toRow; } /** @@ -182,6 +170,8 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After /** internal attributes */ private _data: any[]; + // data virtually iterated by component + private _virtualData: any[]; private _columns: ITdDataTableColumn[]; private _selectable: boolean = false; private _clickable: boolean = false; @@ -226,7 +216,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After /** * Returns true if all values are not deselected - * and atleast one is. + * and at least one is. */ get indeterminate(): boolean { return this._indeterminate; @@ -251,6 +241,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After @Input('data') set data(data: any[]) { this._data = data; + this._rowHeightCache = []; Promise.resolve().then(() => { this.refresh(); // scroll back to the top if the data has changed @@ -261,6 +252,10 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After return this._data; } + get virtualData(): any[] { + return this._virtualData; + } + /** * columns?: ITdDataTableColumn[] * Sets additional column configuration. [ITdDataTableColumn.name] has to exist in [data] as key. @@ -434,8 +429,14 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After */ ngOnInit(): void { // initialize observable for resize calculations - this._resizeSubs = debounceTime.call(this._onResize.asObservable(), 10).subscribe(() => { + this._resizeSubs = this._onResize.asObservable().subscribe(() => { + if (this._rows) { + this._rows.toArray().forEach((row: TdDataTableRowComponent, index: number) => { + this._rowHeightCache[this.fromRow + index] = row.height + 1; + }); + } this._calculateWidths(); + this._calculateVirtualRows(); }); // initialize observable for scroll column header reposition this._horizontalScrollSubs = this._onHorizontalScroll.asObservable() @@ -447,6 +448,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After this._verticalScrollSubs = this._onVerticalScroll.asObservable() .subscribe((verticalScroll: number) => { this._scrollVerticalOffset = verticalScroll; + this._calculateVirtualRows(); this._changeDetectorRef.markForCheck(); }); } @@ -480,6 +482,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After // if the height of the viewport has changed, then we mark for check if (this._hostHeight !== newHostHeight) { this._hostHeight = newHostHeight; + this._calculateVirtualRows(); this._changeDetectorRef.markForCheck(); } } @@ -493,6 +496,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After this._rowsChangedSubs = debounceTime.call(this._rows.changes, 0).subscribe(() => { this._onResize.next(); }); + this._calculateVirtualRows(); } /** @@ -566,6 +570,7 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After * Refreshes data table and rerenders [data] and [columns] */ refresh(): void { + this._calculateVirtualRows(); this._calculateWidths(); this._calculateCheckboxState(); this._changeDetectorRef.markForCheck(); @@ -745,17 +750,9 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After case UP_ARROW: /** * if users presses the up arrow, we focus the prev row - * unless its the first row, then we move to the last row + * unless its the first row */ - if (index === 0) { - if (!event.shiftKey) { - this._scrollableDiv.nativeElement.scrollTop = this.totalHeight; - let subs: Subscription = this._rows.changes.subscribe(() => { - subs.unsubscribe(); - this._rows.toArray()[this._rows.toArray().length - 1].focus(); - }); - } - } else { + if (index > 0) { this._rows.toArray()[index - 1].focus(); } this.blockEvent(event); @@ -766,17 +763,9 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After case DOWN_ARROW: /** * if users presses the down arrow, we focus the next row - * unless its the last row, then we move to the first row + * unless its the last row */ - if (index === (this._rows.toArray().length - 1)) { - if (!event.shiftKey) { - this._scrollableDiv.nativeElement.scrollTop = 0; - let subs: Subscription = this._rows.changes.subscribe(() => { - subs.unsubscribe(); - this._rows.toArray()[0].focus(); - }); - } - } else { + if (index < (this._rows.toArray().length - 1)) { this._rows.toArray()[index + 1].focus(); } this.blockEvent(event); @@ -978,4 +967,73 @@ export class TdDataTableComponent implements ControlValueAccessor, OnInit, After let renderedColumns: ITdDataTableColumn[] = this.columns.filter((col: ITdDataTableColumn) => !col.hidden); return Math.floor(this.hostWidth / renderedColumns.length); } + + /** + * Method to calculate the rows to be rendered in the viewport + */ + private _calculateVirtualRows(): void { + let scrolledRows: number = 0; + if (this._data) { + this._totalHeight = 0; + let rowHeightSum: number = 0; + // loop through all rows to see if we have their height cached + // and sum them all to calculate the total height + this._data.forEach((d: any, index: number) => { + // iterate through all rows at first and assume all + // rows are the same height as the first one + if (!this._rowHeightCache[index]) { + this._rowHeightCache[index] = this._rowHeightCache[0] || TD_VIRTUAL_DEFAULT_ROW_HEIGHT; + } + rowHeightSum += this._rowHeightCache[index]; + // check how many rows have been scrolled + if (this._scrollVerticalOffset - rowHeightSum > 0) { + scrolledRows++; + } + }); + this._totalHeight = rowHeightSum; + // set the initial row to be rendered taking into account the row offset + let fromRow: number = scrolledRows - TD_VIRTUAL_OFFSET; + this._fromRow = fromRow > 0 ? fromRow : 0; + + let hostHeight: number = this._hostHeight; + let index: number = 0; + // calculate how many rows can fit in the viewport + while (hostHeight > 0) { + hostHeight -= this._rowHeightCache[this.fromRow + index]; + index++; + } + // set the last row to be rendered taking into account the row offset + let range: number = (index - 1) + (TD_VIRTUAL_OFFSET * 2); + let toRow: number = range + this.fromRow; + // if last row is greater than the total length, then we use the total length + if (isFinite(toRow) && toRow > this._data.length) { + toRow = this._data.length; + } else if (!isFinite(toRow)) { + toRow = TD_VIRTUAL_OFFSET; + } + this._toRow = toRow; + } else { + this._totalHeight = 0; + this._fromRow = 0; + this._toRow = 0; + } + + let offset: number = 0; + // calculate the proper offset depending on how many rows have been scrolled + if (scrolledRows > TD_VIRTUAL_OFFSET) { + for (let index: number = 0; index < this.fromRow; index++) { + offset += this._rowHeightCache[index]; + } + } + + this._offsetTransform = this._domSanitizer.bypassSecurityTrustStyle('translateY(' + (offset - this.totalHeight) + 'px)'); + if (this._data) { + this._virtualData = this.data.slice(this.fromRow, this.toRow); + } + // mark for check at the end of the queue so we are sure + // that the changes will be marked + Promise.resolve().then(() => { + this._changeDetectorRef.markForCheck(); + }); + } }