4.0.2 • Published 2 years ago

@terminus/ui-table v4.0.2

Weekly downloads
32
License
MIT
Repository
github
Last release
2 years ago

CI/CD Status Codecov MIT License
NPM version Github release Library size

Table of Contents

Usage

Basic

1. Define the columns HTML

Define the table's columns. Each column definition should be given a unique name and contain the content for its header and row cells.

Here's a simple column definition with the name userName. The header cell contains the text Username and the row cell will render the name property of each row's data.

<ng-container tsColumnDef="username">
  <th ts-header-cell *tsHeaderCellDef>
    Username
  </th>

  <td ts-cell *tsCellDef="let item">
    <!-- Place any HTML content here -->
    {{ item.username }}
  </td>
</ng-container>

NOTE: ts-header-cell & ts-cell can both be used as a component selector or an attribute selector. We highly encourage the attribute usage as it is more accessible and has better support for items such as multiple sticky headers etc.

2. Define the displayed columns

The table expects an array of TsColumn definitions to manage the displayed columns and the initial column width.

import { TsColumn } from '@terminus/ui-table'; 

const columns: TsColumn = [
  {name: 'title', width: 200},
  {name: 'body', width: 400},
];

3. Define the table's rows

After defining your columns, provide the header and data row templates that will be rendered out by the table. Each row needs to be given a list of the columns that it should contain. The order of the names will define the order of the cells rendered.

NOTE: It is not required to provide a list of all the defined column names, but only the ones that you want to have rendered.

<tr ts-header-row *tsHeaderRowDef="['userName', 'age']"></tr>

<!-- `let row;` is  exposing the row data to the template as the variable `row` -->
<tr ts-row *tsRowDef="let row; columns: ['userName', 'age']"></tr>

NOTE: ts-header-row & ts-row can both be used as a component selector or an attribute selector. Attribute usage is highly encouraged
as it is more accessible and has better support for items such as multiple sticky headers etc.

The table component provides an array of column names built from the array of TsColumn definitions passed to the table. You can use this
reference for the rows rather than defining two separate arrays:

<table
  ts-table
  [dataSource]="dataSource"
  [columns]="resizableColumns"
  #myTable="tsTable"
>

  <tr ts-header-row *tsHeaderRowDef="myTable.columnNames"></tr>

  <!-- 
    `myTable` is the template reference for the table component.
    `columnNames` is a getter that returns an array of column names.
  -->
  <tr ts-row *tsRowDef="let row; columns: myTable.columnNames"></tr>
</table>

NOTE: ts-table can be used as a component selector or an attribute selector. Attribute usage is highly encouraged as it is more
accessible and has better support for items such as multiple sticky headers etc.

Mapping the array of names manually is also fairly simple:

import { TsColumn } from '@terminus/ui-table'; 

const columns: TsColumn = [
  {name: 'title', width: 200},
  {name: 'body', width: 400},
];
const columnName = this.columns.map(c => c.name);

4. Provide data

The column and row definitions capture how data will render - all that's left is to provide the data itself.

Create an instance of TsTableDataSource and set the items to be displayed to the data property.

import { TsTableDataSource } from '@terminus/ui-table'; 

this.myDataSource = new TsTableDataSource();
this.myDataSource.data = dataToRender;
<table ts-table [dataSource]=”myDataSource”>
  ...
</table>

The DataSource can be seeded with initial data:

import { TsTableDataSource } from '@terminus/ui-table'; 

this.myDataSource = new TsTableDataSource(INITIAL_DATA);

An interface for your table data can be passed to TsTableDataSource for stricter typing:

import { TsTableDataSource } from '@terminus/ui-table'; 

export interface MyTableItem {
  name: string;
  id: number;
}

this.myDataSource = new TsTableDataSource<MyTableItem>(INITIAL_DATA);

Full HTML example

<!-- Pass in the dataSource -->
<table ts-table [dataSource]="myDataSource" [columns]="myColumns" #myTable="tsTable">
  <!-- Define the header cell and body cell for the `username` Column -->
  <ng-container tsColumnDef="username">
    <th ts-header-cell *tsHeaderCellDef>
      Username
    </th>

    <td ts-cell *tsCellDef="let item">
      {{ item.username }}
    </td>
  </ng-container>

  <!-- Define the header cell and body cell for the `age` Column -->
  <ng-container tsColumnDef="age">
    <th ts-header-cell *tsHeaderCellDef>
      Age
    </th>

    <td ts-cell *tsCellDef="let item">
      {{ item.age }}
    </td>
  </ng-container>

  <!-- Define the table's header row -->
  <tr ts-header-row *tsHeaderRowDef="myTable.columnNames"></tr>

  <!-- Define the table's body row(s) -->
  <tr ts-row *tsRowDef="let row; columns: myTable.columnNames;"></tr>
</table>

Dynamically update table data

Your data source was created during the bootstraping of your component:

this.myDataSource = new TsTableDataSource<MyDataType>();

Simply assign the new data to myDataSource.data. The table will flush the old data and display the new data:

this.myDataSource.data = dataToRender;

Dynamic columns

Columns can be dynamically added and removed with any control. The selected control should affect the array of columns passed to the table (myColumns in the example below).

<!-- As the `myColumns` array is changed, columns will be shown/hidden in real time -->
<table ts-table [dataSource]="myDataSource" [columns]="myColumns" #myTable="tsTable">
  <!-- Custom column definitions excluded for brevity -->

  <tr ts-header-row *tsHeaderRowDef="myTable.columnNames"></tr>
  <tr ts-row *tsRowDef="let row; columns: myTable.columnNamesr;"></tr>
</table>

Sorting by column

To add sorting behavior to the table, add the tsSort directive to the <ts-table> and add ts-sort-header to each column header cell that should trigger sorting. Provide the TsSortDirective directive to the TsTableDataSource and it will automatically listen for sorting changes and change the order of data rendered by the table.

By default, the TsTableDataSource sorts with the assumption that the sorted column's name matches the data property name that the column displays. For example, the following column definition is named position, which matches the name of the property displayed in the row cell.

<!-- Add the `tsSort` directive to the table -->
<table ts-table [dataSource]="myDataSource" [columns]="myColumns" tsSort>
  <ng-container tsColumnDef="position">
    <!-- Add the `ts-sort-header` directive to the column -->
    <th ts-header-cell *tsHeaderCellDef ts-sort-header>
      Position
    </th>

    <td ts-cell *tsCellDef="let element">
      {{ element.position }}
    </td>
  </ng-container>
</table>

In your class, get a reference to the TsSortDirective using @ViewChild:

import { AfterViewInit, ViewChild } from '@angular/core';
import { TsSortDirective } from '@terminus/ui-sort';

export class TableComponent implements AfterViewInit {
  // Get a reference to the TsSortDirective instance
  @ViewChild(TsSortDirective, {static: false})
  sort: TsSortDirective;

  public ngAfterViewInit(): void {
    // Subscribe to the sortChange event to reset pagination, fetch new data, etc
    this.sort.sortChange.subscribe(sortState => {
      // Table was sorted - go get new data!
      console.log('Table sort changed! ', sortState);
    });
  }
}

Row selection

This can be implemented at the consumer level by adding a column that contains a checkbox.

Content wrapping

By default, cell contents do not wrap. This can be overridden by adding container around your content with white-space: normal.

Cell alignment

Defining an alignment style for a cell will set the horizontal alignment of text inside the cell. Set the directive alignment and pass it any valid TsTableColumnAlignment value (left, center or right).

<!-- Set alignment on the column -->
<ng-container tsColumnDef="created" alignment="right">
  <th ts-header-cell *tsHeaderCellDef ts-sort-header>
    Created
  </th>
  <td ts-cell *tsCellDef="let item">
    {{ item.created_at | date:shortDate }}
  </td>
</ng-container>

Sticky header

Defining the header as sticky will ensure it is always visible as the table scrolls. Set sticky: true in the ts-header-row's tsHeaderRowDef directive.

<ng-container tsColumnDef="example">
  <th ts-header-cell *tsHeaderCellDef>Created</th>
  <td ts-cell *tsCellDef="let item">{{ item.created_at }}</td>
</ng-container>

<tr ts-header-row *tsHeaderRowDef="displayedColumns; sticky: true"></tr>

NOTE: Multiple sticky headers can be defined.

Footer

A footer row can be defined to be output at the bottom of the table. It supports the same 'sticky' setting that the header row does.

<ng-container tsColumnDef="example">
  <th ts-header-cell *tsHeaderCellDef>Created</th>
  <td ts-cell *tsCellDef="let item">{{ item.created_at }}</td>
  <td ts-footer-cell *tsFooterCellDef>Custom footer cell content</td>
</ng-container>
<tr ts-footer-row *tsFooterRowDef="displayedColumns; sticky: true"></tr>

Sticky column

Defining a sticky column will pin it to the left side as the table scrolls horizontally. Add the sticky data attribute to the column definition. This can be applied to more than one column.

<ng-container tsColumnDef="updated" sticky>
  <ts-header-cell *tsHeaderCellDef>
    Updated
  </ts-header-cell>
  <ts-cell *tsCellDef="let item">
    {{ item.updated_at | date:"shortDate" }}
  </ts-cell>
</ng-container>

NOTE: Multiple sticky columns can be defined.

Sticky column at end

Adding the data attribute stickyEnd will pin the column to the end of the table as it scrolls horizontally. This can be applied to more than one column.

<ng-container tsColumnDef="updated" stickyEnd>
  <ts-header-cell *tsHeaderCellDef>
    Updated
  </ts-header-cell>
  <ts-cell *tsCellDef="let item">
    {{ item.updated_at | date:"shortDate" }}
  </ts-cell>
</ng-container>

NOTE: Multiple stickyEnd columns can be defined.

Re-orderable columns

Column reordering is not built into the table itself, but it is supported with the help of the Angular CDK.

The example below shows how to allow users to adjust column visibility and column order via a menu.

NOTE: Use the custom icon table_large_plus to indicate a table settings menu.

<!-- Set up a TsMenuComponent to control which columns are visible and their order -->
<ts-menu [menuItemsTemplate]="columns" theme="accent">
  <ts-icon svgIcon="table_large_plus"></ts-icon>
  Edit Columns
</ts-menu>

<!-- Here we are defining the list of controls that will be shown in the menu -->
<ng-template #columns>
  <!-- Here we wire up a two CDK drop list directives and one CDK emitter -->
  <form
    [formGroup]="columnsForm"
    cdkDropList
    cdkDropListLockAxis="y"
    (cdkDropListDropped)="columnDropped($event)"
  >
    <!-- Pass an array of all columns that can be made visible. This array's order will be update when column order is changed. -->
    <ng-container *ngFor="let column of allPossibleColumns">
      <!-- The menu normally closes after each interaction, so we need to stop propagation here to
      support multiple selections without closing using the `(click)` event. -->
      <ts-checkbox
        [formControl]="column.control"
        (click)="$event.stopPropagation()"
        theme="accent"
        cdkDrag
      >
        <span>
          {{ column.display }}
        </span>

        <!-- Here we specify the drag handle with another CDK directive (optional). -->    
        <!-- We also need to stop drag interactions from affecting the checkbox status. -->
        <ts-icon
          cdkDragHandle
          (click)="$event.preventDefault() && $event.stopPropagation()"
        >drag_indicator</ts-icon>

        <!-- Define a placeholder that is seen in the slot the current item will fill when released (optional) -->
        <div *cdkDragPlaceholder></div>
      </ts-checkbox>
    </ng-container>
  </form>
</ng-template>

<!-- The table (column & row definitions omitted for brevity) -->
<table
  ts-table
  [dataSource]="dataSource"
  [columns]="visibleColumns"
  (columnsChange)="columnsChange($event)"
  #myTable="tsTable"
>
  <!-- ... -->
</table>
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { TsColumn } from '@terminus/ui-table';

/**
 * Extend the TsColumn interface with properties our component needs.
 *
 * This allows us to add a control we can use to determine which columns should be shown
 */
export interface CustomColumn extends TsColumn {
  // The UI text for the column dropdown
  display: string;
  // The associated FormControl
  control: FormControl;
}

@Component({...})
export class TableComponent {
  // Define the original column source with all columns defaulted to visible
  private readonly columnsSource: CustomColumn[] = [
    {
      display: 'Title',
      name: 'title',
      width: 300,
      control: new FormControl(true),
    },
    {
      display: 'Number',
      name: 'number',
      control: new FormControl(true),
      width: 100,
    },
    // ...etc
  ];

  // Define the mutable version of your columns (must be mutable as we will be changing the order)
  public allPossibleColumns: CustomColumn[] = this.columnsSource.slice();

  // Define a getter that filters our columns whose control is currently `false`.
  // This is what will get passed to the table's `columns` input.
  public get visibleColumns(): CustomColumn[] {
    return this.allPossibleColumns.filter(c => c.control && c.control.value);
  }

  constructor(private changeDetectorRef: ChangeDetectorRef) {}

  // Define a method to handle the drop event. When an item is dropped in the UI, the underlying data has not been changed yet.
  public columnDropped(event: CdkDragDrop<string[]>): void {
    // `moveItemInArray` mutates the original array, so first we clone the array
    const columns = this.allPossibleColumns.slice();
    // Then update the positions (this function is provided by the CDK)
    moveItemInArray(columns, event.previousIndex, event.currentIndex);
    // Update the original columns array
    this.allPossibleColumns = columns;
    // Fire the change detector so that the table will update all columns. (Without this, the column widths aren't re-set until
    // closing the menu.
    this.changeDetectorRef.detectChanges();
  }
}

Density

The table supports two density settings: comfy (default) & compact.

<table ts-table density="compact">
  ...
</table>

Outer border

Since the scrolling container around the table is now a consumer responsibility, adding a border around the table also must be done by the consumer:

.my-table-container {
  border: 1px solid color(utility, light);
}

Scrolling

Scrolling is controlled by the containing element that is placed around the table. If persistent scrollbars are desired, the scrollbars mixin can be used:

.my-table-container {
  @include visible-scrollbars;
}

Events

Table

EventDescriptionPayload
columnsChangeFired when any column is changedTsTableColumnsChangeEvent

NOTE: This event is not throttled or debounced and may be called repeatedly.

<table ts-table (columnsChange)="whenColumnsChange($event)">
  ...
</table>

The TsTableColumnsChangeEvent structure:

class TsTableColumnsChangeEvent {
  constructor(
    // The table instance that originated the event
    public table: TsTableComponent,
    // The updated array of columns
    public columns: TsColumn[],
  ) {}
}

Cell

EventDescriptionPayload
resizedFired when the header cell is resizedTsHeaderCellResizeEvent
<ng-container tsColumnDef="title">
  <th ts-header-cell *tsHeaderCellDef (resized)="whenCellIsResized($event)">
    Title
  </th>
  <td ts-cell *tsCellDef="let item">
    {{ item.title }}
  </td>
</ng-container>
class TsHeaderCellResizeEvent {
  constructor(
    // The header cell instance that originated the event
    public instance: TsHeaderCellDirective,
    // The new width
    public width: string,
  ) {}
}

Full example with pagination, sorting, and dynamic columns

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { merge, Observable, of } from 'rxjs';
import { startWith } from 'rxjs/operators/startWith';
import { map } from 'rxjs/operators/map';
import { switchMap } from 'rxjs/operators/switchMap';
import { catchError } from 'rxjs/operators/catchError';
import { TsSortDirective } from '@terminus/ui-sort';
import { TsTableDataSource, TsColumn } from '@terminus/ui-table';
import { TsPaginatorComponent } from '@terminus/ui-paginator';

@Component({
  selector: 'my-component',
  template: `
    <!-- Align the select to the right side -->
    <div fxLayout="row" fxLayoutAlign="end center">
      <ts-select
        label="Show/hide columns"
        [multipleAllowed]="true"
        valueKey="value"
        [(ngModel)]="displayedColumns"
        [items]="allColumns"
      ></ts-select>
    </div>

    <table
      ts-table
      [dataSource]="dataSource"
      [columns]="displayedColumns"
      tsSort
      #myTable="tsTable"
    >
      <ng-container tsColumnDef="created">
        <th ts-header-cell *tsHeaderCellDef ts-sort-header>Created</th>
        <td ts-cell *tsCellDef="let item">{{ item.created_at | date:shortDate }}</td>
        <td ts-footer-cell *tsFooterCellDef>Footer A</td>
      </ng-container>

      <ng-container tsColumnDef="number" alignment="right">
        <th ts-header-cell *tsHeaderCellDef>Number</th>
        <td ts-cell *tsCellDef="let item">{{ item.number }}</td>
        <td ts-footer-cell *tsFooterCellDef>Footer B</td>
      </ng-container>

      <ng-container tsColumnDef="title">
        <th ts-header-cell *tsHeaderCellDef>Title</th>
        <td ts-cell *tsCellDef="let item">{{ item.title }}</td>
        <td ts-footer-cell *tsFooterCellDef>Footer C</td>
      </ng-container>

      <tr ts-header-row *tsHeaderRowDef="myTable.columnNames"></tr>
      <tr ts-row *tsRowDef="let row; columns: myTable.columnNames"></tr>
      <tr ts-footer-row *tsFooterRowDef="myTable.columnNames"></tr>
    </table>

    <!-- Align the paginator to the right side -->
    <div fxLayout="row" fxLayoutAlign="end center">
      <ts-paginator
        [totalRecords]="resultsLength"
        recordCountTooHighMessage="Please refine your filters."
        (pageSelect)="onPageSelect($event)"
        (recordsPerPageChange)="perPageChange($event)"
        (firstPageChosen)="first($event)"
        (previousPageChosen)="previous($event)"
        (nextPageChosen)="next($event)"
        (lastPageChosen)="last($event)"
      ></ts-paginator>
    </div>
  `,
})
export class TableComponent implements AfterViewInit {
  allColumns: TsColumn[] = [
    {
      name: 'Created',
      value: 'created',
      width: 100,
    },
    {
      name: 'Title',
      value: 'title',
      width: 100,
    },
    {
      name: 'Comments',
      value: 'comments',
      width: 200,
    },
    {
      name: 'State',
      value: 'state',
      width: 100,
    },
    {
      name: 'Number',
      value: 'number',
      width: 100,
    },
  ];
  // Default to all columns visible
  displayedColumns: TsColumn[] = this.allColumns.slice();
  exampleDatabase: ExampleHttpDao | null;
  dataSource = new TsTableDataSource<GithubIssue>();
  resultsLength = 0;

  @ViewChild(TsSortDirective, {static: true})
  sort: TsSortDirective;

  @ViewChild(TsPaginatorComponent, {static: true})
  paginator: TsPaginatorComponent;

  constructor(
    private http: HttpClient,
  ) {}

  public ngAfterViewInit(): void {
    this.exampleDatabase = new ExampleHttpDao(this.http);

    // If the user changes the sort order, reset back to the first page.
    this.sort.sortChange.subscribe(() => this.paginator.currentPageIndex = 0);

    // Fetch new data anytime the sort is changed, the page is changed, or the records shown per page is changed
    merge(this.sort.sortChange, this.paginator.pageSelect, this.paginator.recordsPerPageChange)
      .pipe(
        startWith({}),
        switchMap(() => {
          return this.exampleDatabase.getRepoIssues(
            this.sort.active,
            this.sort.direction,
            this.paginator.currentPageIndex,
            this.paginator.recordsPerPage,
          );
        }),
        map(data => {
          console.log('Demo: fetched data: ', data)
          this.resultsLength = data.total_count;
          return data.items;
        }),
        catchError(() => {
          // Catch if the GitHub API has reached its rate limit. Return empty data.
          console.warn('GitHub API rate limit has been reached!')
          return of([]);
        })
      ).subscribe(data => {
        this.dataSource.data = data;
      });
  }
}

export interface GithubApi {
  items: GithubIssue[];
  total_count: number;
}

export interface GithubIssue {
  created_at: string;
  number: string;
  state: string;
  title: string;
}

/**
 * An example database that this example uses to retrieve data for the table.
 */
export class ExampleHttpDao {
  constructor(private http: HttpClient) {}

  public getRepoIssues(sort: string, order: string, page: number, perPage: number): Observable<GithubApi> {
    const href = `https://api.github.com/search/issues`;
    const requestUrl = `${href}?q=repo:GetTerminus/terminus-ui`;
    const requestParams = `&sort=${sort}&order=${order}&page=${page + 1}&per_page=${perPage}`;
    return this.http.get<GithubApi>(`${requestUrl}${requestParams}`);
  }
}

Test Helpers

Some helpers are exposed to assist with testing. These are imported from @terminus/ui-table/testing;

[source]

Function
getElements
getHeaderRow
getRows
getCells
getHeaderCells
expectTableToMatchContent
4.0.7

2 years ago

4.0.5

3 years ago

4.0.4

3 years ago

4.0.6

3 years ago

4.0.3

3 years ago

4.0.2

3 years ago

4.0.1

3 years ago

4.0.0

3 years ago

3.2.0

3 years ago

3.1.7

3 years ago

3.1.6

3 years ago

3.1.5

4 years ago

3.1.4

4 years ago

3.1.3

4 years ago

3.1.2

4 years ago

3.1.1

4 years ago

3.1.0

4 years ago

3.0.2

4 years ago

3.0.1

4 years ago

3.0.0

4 years ago

2.0.9

4 years ago

2.0.8

4 years ago

2.0.7

4 years ago

2.0.6

4 years ago

2.0.5

4 years ago

2.0.4

4 years ago

2.0.3

4 years ago

2.0.2

4 years ago

2.0.1

4 years ago

2.0.0

4 years ago

1.0.7

4 years ago

1.0.6

4 years ago

1.0.5

4 years ago

1.0.4

4 years ago

1.0.3

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago

0.0.1

4 years ago