src/table/table.component.ts
Build your table with this component by extending things that differ from default.
demo: https://angular.carbondesignsystem.com/?selectedKind=Table
Instead of the usual write-your-own-html approach you had with <table>,
carbon table uses model-view-controller approach.
Here, you create a view (with built-in controller) and provide it a model.
Changes you make to the model are reflected in the view. Provide same model you use
in the table to the <ibm-pagination> components.
They provide a different view over the same data.
<ibm-table [model]="model"></ibm-table>public model = new TableModel();
this.model.data = [
[new TableItem({data: "asdf"}), new TableItem({data: "qwer"})],
[new TableItem({data: "csdf"}), new TableItem({data: "zwer"})],
[new TableItem({data: "bsdf"}), new TableItem({data: "swer"})],
[new TableItem({data: "csdf"}), new TableItem({data: "twer"})]
];If you have custom data in your table, you need a way to display it. You can do that
by providing a template to TableItem.
<ng-template #customTableItemTemplate let-data="data">
<a [routerLink]="data.link">{{data.name}} {{data.surname}}</a>
</ng-template>customTableItemTemplate: TemplateRef<any>;
this.customModel.data = [
[new TableItem({data: "asdf"}), new TableItem({data: {name: "Lessy", link: "/table"}, template: this.customTableItemTemplate})],
[new TableItem({data: "csdf"}), new TableItem({data: "swer"})],
[new TableItem({data: "bsdf"}), new TableItem({data: {name: "Alice", surname: "Bob"}, template: this.customTableItemTemplate})],
[new TableItem({data: "csdf"}), new TableItem({data: "twer"})],
];In case you need custom sorting and/or filtering you should subclass TableHeaderItem
and override needed functions.
class FilterableHeaderItem extends TableHeaderItem {
// custom filter function
filter(item: TableItem): boolean {
if (typeof item.data === "string" && item.data.toLowerCase().indexOf(this.filterData.data.toLowerCase()) >= 0 ||
item.data.name && item.data.name.toLowerCase().indexOf(this.filterData.data.toLowerCase()) >= 0 ||
item.data.surname && item.data.surname.toLowerCase().indexOf(this.filterData.data.toLowerCase()) >= 0) {
return false;
}
return true;
}
set filterCount(n) {}
get filterCount() {
return (this.filterData && this.filterData.data && this.filterData.data.length > 0) ? 1 : 0;
}
// used for custom sorting
compare(one: TableItem, two: TableItem) {
const stringOne = (one.data.name || one.data.surname || one.data).toLowerCase();
const stringTwo = (two.data.name || two.data.surname || two.data).toLowerCase();
if (stringOne > stringTwo) {
return 1;
} else if (stringOne < stringTwo) {
return -1;
} else {
return 0;
}
}
}See TableHeaderItem class for more information.
When table has no data to show, it can show a message you provide it instead.
<ibm-table [model]="model">No data.</ibm-table>... will show No data. message, but you can get creative and provide any template you want
to replace table's default tbody.
<ibm-pagination [model]="model" (selectPage)="selectPage($event)"></ibm-pagination>selectPage() function should fetch the data from backend, create new data, apply it to model.data,
and update model.currentPage.
If the data your server returns is a two dimensional array of objects, it would look something like this:
selectPage(page) {
this.getPage(page).then((data: Array<Array<any>>) => {
// set the data and update page
this.model.data = this.prepareData(data);
this.model.currentPage = page;
});
}
private prepareData(data: Array<Array<any>>) {
// create new data from the service data
let newData = [];
data.forEach(dataRow => {
let row = [];
dataRow.forEach(dataElement => {
row.push(new TableItem({
data: dataElement,
template: typeof dataElement === "string" ? undefined : this.paginationTableItemTemplate
// your template can handle all the data types so you don't have to conditionally set it
// you can also set different templates for different columns based on index
}));
});
newData.push(row);
});
return newData;
}
| selector | ibm-table |
Properties |
|
Methods |
Inputs |
Outputs |
Accessors |
constructor(applicationRef: ApplicationRef, i18n: I18n)
|
||||||||||||
|
Defined in src/table/table.component.ts:552
|
||||||||||||
|
Creates an instance of Table.
Parameters :
|
columnsDraggable
|
Set to Changing the column order in table changes table model. Be aware of it when you add additional data to the model.
Default value: |
|
Defined in src/table/table.component.ts:460
|
|
columnsResizable
|
Set to Works for columns with width set in pixels.
Default value: |
|
Defined in src/table/table.component.ts:450
|
|
enableRowSelect
|
Controls whether to show the selection checkboxes column or not.
Type: |
|
Defined in src/table/table.component.ts:410
|
|
enableSingleSelect
|
Controls whether to enable multiple or single row selection.
Default value: |
|
Defined in src/table/table.component.ts:432
|
|
expandButtonAriaLabel
|
Default value: |
|
Defined in src/table/table.component.ts:462
|
|
model
|
|
|
Defined in src/table/table.component.ts:387
|
|
scrollLoadDistance
|
Distance (in px) from the bottom that view has to reach before
Default value: |
|
Defined in src/table/table.component.ts:441
|
|
showSelectionColumn
|
Controls whether to show the selection checkboxes column or not.
Default value: |
|
Defined in src/table/table.component.ts:424
|
|
size
|
Size of the table rows.
Type:
Default value: |
|
Defined in src/table/table.component.ts:373
|
|
sortAscendingLabel
|
Default value: |
|
Defined in src/table/table.component.ts:464
|
|
sortDescendingLabel
|
Default value: |
|
Defined in src/table/table.component.ts:463
|
|
striped
|
Set to
Default value: |
|
Defined in src/table/table.component.ts:488
|
|
translations
|
Object of all the strings table needs.
Defaults to the |
|
Defined in src/table/table.component.ts:378
|
|
deselectAll
|
Emits if all rows are deselected. $event type: EventEmitter
|
|
Defined in src/table/table.component.ts:511
|
|
deselectRow
|
Emits if a single row is deselected. $event type: EventEmitter
|
|
Defined in src/table/table.component.ts:527
|
|
scrollLoad
|
Emits when table requires more data to be loaded. $event type: EventEmitter
|
|
Defined in src/table/table.component.ts:535
|
|
selectAll
|
Emits if all rows are selected. $event type: EventEmitter
|
|
Defined in src/table/table.component.ts:503
|
|
selectRow
|
Emits if a single row is selected. $event type: EventEmitter
|
|
Defined in src/table/table.component.ts:519
|
|
sort
|
Emits an index of the column that wants to be sorted. $event type: EventEmitter
|
|
Defined in src/table/table.component.ts:495
|
|
| columnDragEnd | ||||||||||||
columnDragEnd(event: , columnIndex: )
|
||||||||||||
|
Defined in src/table/table.component.ts:688
|
||||||||||||
|
Parameters :
Returns :
void
|
| columnDragEnter | ||||||||||||||||
columnDragEnter(event: , position: , columnIndex: )
|
||||||||||||||||
|
Defined in src/table/table.component.ts:693
|
||||||||||||||||
|
Parameters :
Returns :
void
|
| columnDragLeave | ||||||||||||||||
columnDragLeave(event: , position: , columnIndex: )
|
||||||||||||||||
|
Defined in src/table/table.component.ts:698
|
||||||||||||||||
|
Parameters :
Returns :
void
|
| columnDragover | ||||||||||||||||
columnDragover(event: , position: , columnIndex: )
|
||||||||||||||||
|
Defined in src/table/table.component.ts:702
|
||||||||||||||||
|
Parameters :
Returns :
void
|
| columnDragStart | ||||||||||||
columnDragStart(event: , columnIndex: )
|
||||||||||||
|
Defined in src/table/table.component.ts:682
|
||||||||||||
|
Parameters :
Returns :
void
|
| columnDrop | ||||||||||||||||
columnDrop(event: , position: , columnIndex: )
|
||||||||||||||||
|
Defined in src/table/table.component.ts:710
|
||||||||||||||||
|
Parameters :
Returns :
void
|
| columnResizeEnd | ||||||||||||
columnResizeEnd(event: , column: )
|
||||||||||||
|
Defined in src/table/table.component.ts:580
|
||||||||||||
|
Parameters :
Returns :
void
|
| columnResizeProgress | ||||||||||||
columnResizeProgress(event: , column: )
|
||||||||||||
|
Defined in src/table/table.component.ts:575
|
||||||||||||
|
Parameters :
Returns :
void
|
| columnResizeStart | ||||||||||||
columnResizeStart(event: , column: )
|
||||||||||||
|
Defined in src/table/table.component.ts:562
|
||||||||||||
|
Parameters :
Returns :
void
|
| firstExpandedDataInRow | ||||||||
firstExpandedDataInRow(row: )
|
||||||||
|
Defined in src/table/table.component.ts:725
|
||||||||
|
Parameters :
Returns :
any
|
| firstExpandedTemplateInRow | ||||||||
firstExpandedTemplateInRow(row: )
|
||||||||
|
Defined in src/table/table.component.ts:733
|
||||||||
|
Parameters :
Returns :
any
|
| onRowCheckboxChange | ||||||||
onRowCheckboxChange(index: number)
|
||||||||
|
Defined in src/table/table.component.ts:641
|
||||||||
|
Triggered when a single row is clicked.
Updates the header checkbox state.
Emits the
Parameters :
Returns :
void
|
| onRowSelect | ||||||||
onRowSelect(index: number)
|
||||||||
|
Defined in src/table/table.component.ts:585
|
||||||||
|
Parameters :
Returns :
void
|
| onScroll | ||||||||
onScroll(event: )
|
||||||||
|
Defined in src/table/table.component.ts:672
|
||||||||
|
Triggered when the user scrolls on the
Parameters :
Returns :
void
|
| onSelectAllCheckboxChange |
onSelectAllCheckboxChange()
|
|
Defined in src/table/table.component.ts:613
|
|
Triggered whenever the header checkbox is clicked.
Updates all the checkboxes in the table view.
Emits the
Returns :
void
|
| scrollToTop | ||||||||
scrollToTop(event: )
|
||||||||
|
Defined in src/table/table.component.ts:747
|
||||||||
|
Triggered when the user scrolls on the
Parameters :
Returns :
void
|
| updateSelectAllCheckbox |
updateSelectAllCheckbox()
|
|
Defined in src/table/table.component.ts:594
|
|
Returns :
void
|
| Private _model |
_model:
|
Type : TableModel
|
|
Defined in src/table/table.component.ts:543
|
| Private columnDraggedHoverIndex |
columnDraggedHoverIndex:
|
|
Defined in src/table/table.component.ts:551
|
| Private columnDraggedPosition |
columnDraggedPosition:
|
|
Defined in src/table/table.component.ts:552
|
| Private columnResizeMouseX |
columnResizeMouseX:
|
Type : number
|
|
Defined in src/table/table.component.ts:546
|
| Private columnResizeWidth |
columnResizeWidth:
|
Type : number
|
|
Defined in src/table/table.component.ts:545
|
| Private isColumnDragging |
isColumnDragging:
|
Default value : false
|
|
Defined in src/table/table.component.ts:550
|
| Private mouseMoveSubscription |
mouseMoveSubscription:
|
Type : Subscription
|
|
Defined in src/table/table.component.ts:547
|
| Private mouseUpSubscription |
mouseUpSubscription:
|
Type : Subscription
|
|
Defined in src/table/table.component.ts:548
|
| selectAllCheckbox |
selectAllCheckbox:
|
Default value : false
|
|
Defined in src/table/table.component.ts:472
|
|
Controls if all checkboxes are viewed as selected. |
| selectAllCheckboxSomeSelected |
selectAllCheckboxSomeSelected:
|
Default value : false
|
|
Defined in src/table/table.component.ts:480
|
|
Controls the indeterminate state of the header checkbox. |
| model | ||||||||
getmodel()
|
||||||||
|
Defined in src/table/table.component.ts:398
|
||||||||
|
Returns :
TableModel
|
||||||||
setmodel(m: )
|
||||||||
|
Defined in src/table/table.component.ts:387
|
||||||||
|
Parameters :
Returns :
void
|
| enableRowSelect | ||||||||
getenableRowSelect()
|
||||||||
|
Defined in src/table/table.component.ts:414
|
||||||||
setenableRowSelect(value: boolean)
|
||||||||
|
Defined in src/table/table.component.ts:410
|
||||||||
|
Controls whether to show the selection checkboxes column or not.
Parameters :
Returns :
void
|
| noData |
getnoData()
|
|
Defined in src/table/table.component.ts:537
|
| scrollbarWidth |
getscrollbarWidth()
|
|
Defined in src/table/table.component.ts:721
|
import {
Component,
ApplicationRef,
Input,
Output,
EventEmitter
} from "@angular/core";
import { Subscription, fromEvent } from "rxjs";
import { TableModel } from "./table.module";
import { getScrollbarWidth } from "../common/utils";
import { I18n } from "./../i18n/i18n.module";
/**
* Build your table with this component by extending things that differ from default.
*
* demo: [https://angular.carbondesignsystem.com/?selectedKind=Table](https://angular.carbondesignsystem.com/?selectedKind=Table)
*
* Instead of the usual write-your-own-html approach you had with `<table>`,
* carbon table uses model-view-controller approach.
*
* Here, you create a view (with built-in controller) and provide it a model.
* Changes you make to the model are reflected in the view. Provide same model you use
* in the table to the `<ibm-pagination>` components.
* They provide a different view over the same data.
*
* ## Basic usage
*
* ```html
* <ibm-table [model]="model"></ibm-table>
* ```
*
* ```typescript
* public model = new TableModel();
*
* this.model.data = [
* [new TableItem({data: "asdf"}), new TableItem({data: "qwer"})],
* [new TableItem({data: "csdf"}), new TableItem({data: "zwer"})],
* [new TableItem({data: "bsdf"}), new TableItem({data: "swer"})],
* [new TableItem({data: "csdf"}), new TableItem({data: "twer"})]
* ];
* ```
*
* ## Customization
*
* If you have custom data in your table, you need a way to display it. You can do that
* by providing a template to `TableItem`.
*
* ```html
* <ng-template #customTableItemTemplate let-data="data">
* <a [routerLink]="data.link">{{data.name}} {{data.surname}}</a>
* </ng-template>
* ```
*
* ```typescript
* customTableItemTemplate: TemplateRef<any>;
*
* this.customModel.data = [
* [new TableItem({data: "asdf"}), new TableItem({data: {name: "Lessy", link: "/table"}, template: this.customTableItemTemplate})],
* [new TableItem({data: "csdf"}), new TableItem({data: "swer"})],
* [new TableItem({data: "bsdf"}), new TableItem({data: {name: "Alice", surname: "Bob"}, template: this.customTableItemTemplate})],
* [new TableItem({data: "csdf"}), new TableItem({data: "twer"})],
* ];
* ```
*
* ### Sorting and filtering
*
* In case you need custom sorting and/or filtering you should subclass `TableHeaderItem`
* and override needed functions.
*
* ```typescript
* class FilterableHeaderItem extends TableHeaderItem {
* // custom filter function
* filter(item: TableItem): boolean {
* if (typeof item.data === "string" && item.data.toLowerCase().indexOf(this.filterData.data.toLowerCase()) >= 0 ||
* item.data.name && item.data.name.toLowerCase().indexOf(this.filterData.data.toLowerCase()) >= 0 ||
* item.data.surname && item.data.surname.toLowerCase().indexOf(this.filterData.data.toLowerCase()) >= 0) {
* return false;
* }
* return true;
* }
*
* set filterCount(n) {}
* get filterCount() {
* return (this.filterData && this.filterData.data && this.filterData.data.length > 0) ? 1 : 0;
* }
*
* // used for custom sorting
* compare(one: TableItem, two: TableItem) {
* const stringOne = (one.data.name || one.data.surname || one.data).toLowerCase();
* const stringTwo = (two.data.name || two.data.surname || two.data).toLowerCase();
*
* if (stringOne > stringTwo) {
* return 1;
* } else if (stringOne < stringTwo) {
* return -1;
* } else {
* return 0;
* }
* }
* }
* ```
*
* See `TableHeaderItem` class for more information.
*
* ## No data template
*
* When table has no data to show, it can show a message you provide it instead.
*
* ```html
* <ibm-table [model]="model">No data.</ibm-table>
* ```
*
* ... will show `No data.` message, but you can get creative and provide any template you want
* to replace table's default `tbody`.
*
* ## Use pagination as table footer
*
* ```html
* <ibm-pagination [model]="model" (selectPage)="selectPage($event)"></ibm-pagination>
* ```
*
* `selectPage()` function should fetch the data from backend, create new `data`, apply it to `model.data`,
* and update `model.currentPage`.
*
* If the data your server returns is a two dimensional array of objects, it would look something like this:
*
* ```typescript
* selectPage(page) {
* this.getPage(page).then((data: Array<Array<any>>) => {
* // set the data and update page
* this.model.data = this.prepareData(data);
* this.model.currentPage = page;
* });
* }
*
* private prepareData(data: Array<Array<any>>) {
* // create new data from the service data
* let newData = [];
* data.forEach(dataRow => {
* let row = [];
* dataRow.forEach(dataElement => {
* row.push(new TableItem({
* data: dataElement,
* template: typeof dataElement === "string" ? undefined : this.paginationTableItemTemplate
* // your template can handle all the data types so you don't have to conditionally set it
* // you can also set different templates for different columns based on index
* }));
* });
* newData.push(row);
* });
* return newData;
* }
* ```
*
* @export
* @class Table
* @implements {AfterContentChecked}
*/
@Component({
selector: "ibm-table",
template: `
<table
class="bx--data-table-v2"
[ngClass]="{
'bx--data-table-v2--compact': size === 'sm',
'bx--data-table-v2--tall': size === 'lg',
'bx--data-table-v2--zebra': striped
}">
<thead>
<tr>
<th *ngIf="model.hasExpandableRows()"></th>
<th *ngIf="showSelectionColumn">
<ibm-checkbox
[size]="size !== ('lg' ? 'sm' : 'md')"
[(ngModel)]="selectAllCheckbox"
[indeterminate]="selectAllCheckboxSomeSelected"
aria-label="Select all rows"
(change)="onSelectAllCheckboxChange()">
</ibm-checkbox>
</th>
<ng-container *ngFor="let column of model.header; let i = index">
<th [ngClass]='{"thead_action": column.filterTemplate || this.sort.observers.length > 0}'
*ngIf="column.visible"
[class]="column.className"
[ngStyle]="column.style"
[draggable]="columnsDraggable"
(dragstart)="columnDragStart($event, i)"
(dragend)="columnDragEnd($event, i)">
<div
*ngIf="columnsResizable"
class="column-resize-handle"
(mousedown)="columnResizeStart($event, column)">
</div>
<button
class="bx--table-sort-v2"
*ngIf="this.sort.observers.length > 0 && column.sortable"
[ngClass]="{
'bx--table-sort-v2--active': column.sorted,
'bx--table-sort-v2--ascending': column.ascending
}"
(click)="sort.emit(i)">
<span *ngIf="!column.template" [title]="column.data">{{column.data}}</span>
<ng-template
[ngTemplateOutlet]="column.template" [ngTemplateOutletContext]="{data: column.data}">
</ng-template>
<svg
class="bx--table-sort-v2__icon"
width="10" height="5" viewBox="0 0 10 5"
[attr.aria-label]="(column.sorted && column.ascending ? sortDescendingLabel : sortAscendingLabel)"
[attr.alt]="(column.sorted && column.ascending ? sortDescendingLabel : sortAscendingLabel)">
<title>{{(column.sorted && column.ascending ? sortDescendingLabel : sortAscendingLabel)}}</title>
<path d="M0 0l5 4.998L10 0z" fill-rule="evenodd" />
</svg>
</button>
<span
class="bx--table-header-label"
*ngIf="this.sort.observers.length === 0 || (this.sort.observers.length > 0 && !column.sortable)">
<span *ngIf="!column.template" [title]="column.data">{{column.data}}</span>
<ng-template
[ngTemplateOutlet]="column.template" [ngTemplateOutletContext]="{data: column.data}">
</ng-template>
</span>
<button
[ngClass]="{'active': column.filterCount > 0}"
*ngIf="column.filterTemplate"
type="button"
aria-expanded="false"
aria-haspopup="true"
[ibmTooltip]="column.filterTemplate"
trigger="click"
[title]="translations.FILTER"
placement="bottom,top"
[data]="column.filterData">
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon--sm"
width="16"
height="16"
viewBox="0 0 16 16">
<path d="M0 0v3l6 8v5h4v-5l6-8V0H0zm9 10.7V15H7v-4.3L1.3 3h13.5L9 10.7z"/>
</svg>
<span *ngIf="column.filterCount > 0">
{{column.filterCount}}
</span>
</button>
<div
*ngIf="columnsDraggable && isColumnDragging"
class="drop-area">
<div
*ngIf="columnDraggedHoverIndex == i && columnDraggedPosition == 'left'"
class="drop-indicator-left"></div>
<div
class="drop-area-left"
(dragenter)="columnDragEnter($event, 'left', i)"
(dragleave)="columnDragLeave($event, 'left', i)"
(dragover)="columnDragover($event, 'left', i)"
(drop)="columnDrop($event, 'left', i)">
</div>
<div
class="drop-area-right"
(dragenter)="columnDragEnter($event, 'right', i)"
(dragleave)="columnDragLeave($event, 'right', i)"
(dragover)="columnDragover($event, 'right', i)"
(drop)="columnDrop($event, 'right', i)">
</div>
<div
*ngIf="columnDraggedHoverIndex == i && columnDraggedPosition == 'right'"
class="drop-indicator-right"></div>
</div>
</th>
</ng-container>
<th [ngStyle]="{'width': scrollbarWidth + 'px', 'padding': 0, 'border': 0}">
<!--
Scrollbar pushes body to the left so this header column is added to push
the title bar the same amount and keep the header and body columns aligned.
-->
</th>
</tr>
</thead>
<tbody
*ngIf="!noData; else noDataTemplate"
[ngStyle]="{'overflow-y': 'scroll'}"
(scroll)="onScroll($event)">
<ng-container *ngFor="let row of model.data; let i = index">
<tr *ngIf="!model.isRowFiltered(i)"
(click)="onRowSelect(i)"
[attr.data-parent-row]="(model.isRowExpandable(i) ? 'true' : null)"
[ngClass]="{
'bx--data-table-v2--selected': model.rowsSelected[i],
'bx--parent-row-v2': model.isRowExpandable(i),
'bx--expandable-row-v2': model.rowsExpanded[i],
'tbody_row--selectable': enableSingleSelect,
'tbody_row--success': !model.rowsSelected[i] && model.rowsContext[i] === 'success',
'tbody_row--warning': !model.rowsSelected[i] && model.rowsContext[i] === 'warning',
'tbody_row--info': !model.rowsSelected[i] && model.rowsContext[i] === 'info',
'tbody_row--error': !model.rowsSelected[i] && model.rowsContext[i] === 'error'
}">
<td
*ngIf="model.hasExpandableRows()"
class="bx--table-expand-v2"
[attr.data-previous-value]="(model.rowsExpanded[i] ? 'collapsed' : null)">
<button
*ngIf="model.isRowExpandable(i)"
(click)="model.expandRow(i, !model.rowsExpanded[i])"
[attr.aria-label]="expandButtonAriaLabel"
class="bx--table-expand-v2__button">
<svg class="bx--table-expand-v2__svg" width="7" height="12" viewBox="0 0 7 12">
<path fill-rule="nonzero" d="M5.569 5.994L0 .726.687 0l6.336 5.994-6.335 6.002L0 11.27z" />
</svg>
</button>
</td>
<td *ngIf="showSelectionColumn">
<ibm-checkbox
aria-label="Select row"
[size]="size !== ('lg' ? 'sm' : 'md')"
[(ngModel)]="model.rowsSelected[i]"
(change)="onRowCheckboxChange(i)">
</ibm-checkbox>
</td>
<ng-container *ngFor="let item of row; let i = index">
<td *ngIf="model.header[i].visible"
[class]="model.header[i].className"
[ngStyle]="model.header[i].style">
<ng-container *ngIf="!item.template">{{item.data}}</ng-container>
<ng-template
[ngTemplateOutlet]="item.template" [ngTemplateOutletContext]="{data: item.data}">
</ng-template>
</td>
</ng-container>
</tr>
<tr
*ngIf="model.rowsExpanded[i]"
class="bx--expandable-row-v2"
[attr.data-child-row]="(model.rowsExpanded[i] ? 'true' : null)">
<td [attr.colspan]="model.data.length + 2">
<ng-container *ngIf="!firstExpandedTemplateInRow(row)">{{firstExpandedDataInRow(row)}}</ng-container>
<ng-template
[ngTemplateOutlet]="firstExpandedTemplateInRow(row)"
[ngTemplateOutletContext]="{data: firstExpandedDataInRow(row)}">
</ng-template>
</td>
</tr>
</ng-container>
</tbody>
<ng-template #noDataTemplate><ng-content></ng-content></ng-template>
<tfoot>
<tr *ngIf="this.model.isLoading">
<td class="table_loading-indicator">
<ibm-static-icon icon="loading_rows" size="lg"></ibm-static-icon>
</td>
</tr>
<tr *ngIf="this.model.isEnd">
<td class="table_end-indicator">
<h5>{{translations.END_OF_DATA}}</h5>
<button (click)="scrollToTop($event)" class="btn--secondary-sm">
{{translations.SCROLL_TOP}}
</button>
</td>
</tr>
</tfoot>
</table>
`
})
export class Table {
/**
* Size of the table rows.
*
* @type {("sm" | "md" | "lg")}
* @memberof Table
*/
@Input() size: "sm" | "md" | "lg" = "md";
/**
* Object of all the strings table needs.
* Defaults to the `TABLE` value from the i18n service.
*/
@Input() translations = this.i18n.get().TABLE;
/**
* `TableModel` with data the table is to display.
*
* @type {TableModel}
* @memberof Table
*/
@Input()
set model(m: TableModel) {
if (this._model) {
this._model.dataChange.unsubscribe();
this._model.rowsSelectedChange.unsubscribe();
}
this._model = m;
this._model.rowsSelectedChange.subscribe(() => this.updateSelectAllCheckbox());
this._model.dataChange.subscribe(() => this.updateSelectAllCheckbox());
}
get model(): TableModel {
return this._model;
}
/**
* Controls whether to show the selection checkboxes column or not.
*
* @deprecated in the next major carbon-components-angular version in favour of
* `showSelectionColumn` because of new attribute `enableSingleSelect`
* please use `showSelectionColumn` instead
*/
@Input()
set enableRowSelect(value: boolean) {
this.showSelectionColumn = value;
}
get enableRowSelect () {
return this.showSelectionColumn;
}
/**
* Controls whether to show the selection checkboxes column or not.
*
* @type {boolean}
* @memberof Table
*/
@Input() showSelectionColumn = true;
/**
* Controls whether to enable multiple or single row selection.
*
* @type {boolean}
* @memberof Table
*/
@Input() enableSingleSelect = false;
/**
* Distance (in px) from the bottom that view has to reach before
* `scrollLoad` event is emitted.
*
* @type {number}
* @memberof Table
*/
@Input() scrollLoadDistance = 0;
/**
* Set to `true` to enable users to resize columns.
*
* Works for columns with width set in pixels.
*
* @memberof Table
*/
@Input() columnsResizable = false;
/**
* Set to `true` to enable users to drag and drop columns.
*
* Changing the column order in table changes table model. Be aware of it when you add additional data
* to the model.
*
* @memberof Table
*/
@Input() columnsDraggable = false;
@Input() expandButtonAriaLabel = "Expand row";
@Input() sortDescendingLabel = "Sort rows by this header in descending order";
@Input() sortAscendingLabel = "Sort rows by this header in ascending order";
/**
* Controls if all checkboxes are viewed as selected.
*
* @type {boolean}
* @memberof Table
*/
selectAllCheckbox = false;
/**
* Controls the indeterminate state of the header checkbox.
*
* @type {boolean}
* @memberof Table
*/
selectAllCheckboxSomeSelected = false;
/**
* Set to `false` to remove table rows (zebra) stripes.
*
* @type {boolean}
* @memberof Table
*/
@Input() striped = true;
/**
* Emits an index of the column that wants to be sorted.
*
* @memberof Table
*/
@Output() sort = new EventEmitter<number>();
/**
* Emits if all rows are selected.
*
* @param {TableModel} model
* @memberof Table
*/
@Output() selectAll = new EventEmitter<Object>();
/**
* Emits if all rows are deselected.
*
* @param {TableModel} model
* @memberof Table
*/
@Output() deselectAll = new EventEmitter<Object>();
/**
* Emits if a single row is selected.
*
* @param {Object} ({model: this.model, selectedRowIndex: index})
* @memberof Table
*/
@Output() selectRow = new EventEmitter<Object>();
/**
* Emits if a single row is deselected.
*
* @param {Object} ({model: this.model, deselectedRowIndex: index})
* @memberof Table
*/
@Output() deselectRow = new EventEmitter<Object>();
/**
* Emits when table requires more data to be loaded.
*
* @param {TableModel} model
* @memberof Table
*/
@Output() scrollLoad = new EventEmitter<TableModel>();
get noData() {
return !this.model.data ||
this.model.data.length === 0 ||
this.model.data.length === 1 && this.model.data[0].length === 0;
}
private _model: TableModel;
private columnResizeWidth: number;
private columnResizeMouseX: number;
private mouseMoveSubscription: Subscription;
private mouseUpSubscription: Subscription;
private isColumnDragging = false;
private columnDraggedHoverIndex = -1;
private columnDraggedPosition = "";
/**
* Creates an instance of Table.
*
* @param {ApplicationRef} applicationRef
* @memberof Table
*/
constructor(protected applicationRef: ApplicationRef, protected i18n: I18n) {}
columnResizeStart(event, column) {
this.columnResizeWidth = parseInt(column.style.width, 10);
this.columnResizeMouseX = event.clientX;
event.preventDefault();
this.mouseMoveSubscription = fromEvent(document.body, "mousemove").subscribe(event => {
this.columnResizeProgress(event, column);
});
this.mouseUpSubscription = fromEvent(document.body, "mouseup").subscribe(event => {
this.columnResizeEnd(event, column);
});
}
columnResizeProgress(event, column) {
const move = event.clientX - this.columnResizeMouseX;
column.style.width = `${this.columnResizeWidth + move}px`;
}
columnResizeEnd(event, column) {
this.mouseMoveSubscription.unsubscribe();
this.mouseUpSubscription.unsubscribe();
}
onRowSelect(index: number) {
if (!this.showSelectionColumn && this.enableSingleSelect) {
this.model.rowsSelected.forEach((element, index) => {
this.model.selectRow(index, false);
});
this.model.selectRow(index, !this.model.rowsSelected[index]);
}
}
updateSelectAllCheckbox() {
const selectedRowsCount = this.model.selectedRowsCount();
if (selectedRowsCount <= 0) {
// reset select all checkbox if nothing selected
this.selectAllCheckbox = false;
this.selectAllCheckboxSomeSelected = false;
} else if (selectedRowsCount < this.model.data.length) {
this.selectAllCheckboxSomeSelected = true;
}
}
/**
* Triggered whenever the header checkbox is clicked.
* Updates all the checkboxes in the table view.
* Emits the `selectAll` or `deselectAll` event.
*
* @memberof Table
*/
onSelectAllCheckboxChange() {
this.applicationRef.tick(); // give app time to process the click if needed
if (this.selectAllCheckboxSomeSelected) {
this.selectAllCheckbox = false; // clear all boxes
this.deselectAll.emit(this.model);
} else if (this.selectAllCheckbox) {
this.selectAll.emit(this.model);
} else {
this.deselectAll.emit(this.model);
}
this.selectAllCheckboxSomeSelected = false;
for (let i = 0; i < this.model.rowsSelected.length; i++) {
this.model.rowsSelected[i] = this.selectAllCheckbox;
}
}
/**
* Triggered when a single row is clicked.
* Updates the header checkbox state.
* Emits the `selectRow` or `deselectRow` event.
*
* @param {number} index
* @returns
* @memberof Table
*/
onRowCheckboxChange(index: number) {
let startValue = this.model.rowsSelected[0];
if (this.model.rowsSelected[index]) {
this.selectRow.emit({model: this.model, selectedRowIndex: index});
} else {
this.deselectRow.emit({model: this.model, deselectedRowIndex: index});
}
for (let i = 1; i < this.model.rowsSelected.length; i++) {
let one = this.model.rowsSelected[i];
if (!!one !== !!startValue) { // !! essentially converts to boolean and we want undefined to be false
// set indeterminate
this.selectAllCheckbox = false;
this.selectAllCheckboxSomeSelected = true;
return;
}
}
this.selectAllCheckboxSomeSelected = false;
this.selectAllCheckbox = startValue;
}
/**
* Triggered when the user scrolls on the `<tbody>` element.
* Emits the `scrollLoad` event.
*
* @param {any} event
* @memberof Table
*/
onScroll(event) {
const distanceFromBottom = event.target.scrollHeight - event.target.clientHeight - event.target.scrollTop;
if (distanceFromBottom <= this.scrollLoadDistance) {
this.scrollLoad.emit(this.model);
} else {
this.model.isEnd = false;
}
}
columnDragStart(event, columnIndex) {
this.isColumnDragging = true;
this.columnDraggedHoverIndex = columnIndex;
event.dataTransfer.setData("columnIndex", JSON.stringify(columnIndex));
}
columnDragEnd(event, columnIndex) {
this.isColumnDragging = false;
this.columnDraggedHoverIndex = -1;
}
columnDragEnter(event, position, columnIndex) {
this.columnDraggedPosition = position;
this.columnDraggedHoverIndex = columnIndex;
}
columnDragLeave(event, position, columnIndex) {
this.columnDraggedPosition = "";
}
columnDragover(event, position, columnIndex) {
this.columnDraggedHoverIndex = columnIndex;
this.columnDraggedPosition = position;
// needed to tell browser to allow dropping
event.preventDefault();
}
columnDrop(event, position, columnIndex) {
this.isColumnDragging = false;
this.columnDraggedHoverIndex = -1;
this.columnDraggedPosition = "";
this.model.moveColumn(
parseInt(event.dataTransfer.getData("columnIndex"), 10),
columnIndex + (position === "right" ? 1 : 0)
);
}
get scrollbarWidth() {
return getScrollbarWidth();
}
firstExpandedDataInRow(row) {
const found = row.find(d => d.expandedData);
if (found) {
return found.expandedData;
}
return found;
}
firstExpandedTemplateInRow(row) {
const found = row.find(d => d.expandedTemplate);
if (found) {
return found.expandedTemplate;
}
return found;
}
/**
* Triggered when the user scrolls on the `<tbody>` element.
* Emits the `scrollLoad` event.
*
* @param {any} event
* @memberof Table
*/
scrollToTop(event) {
event.target.parentElement.parentElement.parentElement.parentElement.children[1].scrollTop = 0;
this.model.isEnd = false;
}
}