From 2dc796c165566b86a624f8893d533f5fc46279c8 Mon Sep 17 00:00:00 2001 From: Benjamin Charity Date: Mon, 7 Oct 2019 15:32:25 -0400 Subject: [PATCH] feat(Table): column widths can be defined by consumer BREAKING CHANGE: - `TsColumn` definitions must be passed to `TsTable`. - `minWidth` support has been removed in favor of `TsColumn` definitions. ISSUES CLOSED: #1617 --- .../app/components/table/table.component.html | 97 ++--- .../app/components/table/table.component.scss | 7 + demo/app/components/table/table.component.ts | 71 +++- demo/app/components/table/table.module.ts | 2 + terminus-ui/sort/src/sort-header.component.ts | 14 +- terminus-ui/table/package.json | 5 +- terminus-ui/table/src/cell.ts | 100 ++++-- terminus-ui/table/src/design-decisions.md | 2 + terminus-ui/table/src/row.ts | 1 + .../table/src/table-data-source.spec.ts | 5 +- terminus-ui/table/src/table.component.md | 334 ++++++++---------- terminus-ui/table/src/table.component.scss | 22 +- terminus-ui/table/src/table.component.spec.ts | 52 +-- terminus-ui/table/src/table.component.ts | 52 ++- .../table/testing/src/test-components.ts | 23 +- 15 files changed, 450 insertions(+), 337 deletions(-) diff --git a/demo/app/components/table/table.component.html b/demo/app/components/table/table.component.html index a3cf3a6ff..cddb4c6cb 100644 --- a/demo/app/components/table/table.component.html +++ b/demo/app/components/table/table.component.html @@ -17,20 +17,22 @@
- - - Created + + + Title - {{ item.created_at | date:"shortDate" }} + {{ item.title }} - + Updated @@ -39,92 +41,105 @@ - - - Number + + + Comments - {{ item.number }} + {{ item.comments }} - - - Title + + + Assignee - {{ item.title }} + {{ item.login }} - + - Body + Number - + {{ item.number }} - + - State + Labels - {{ item.state }} + + {{ l.name }}, + - + - Comments + Created - {{ item.comments }} + {{ item.created_at | date:"shortDate" }} - - - Assignee + + + Body - {{ item.login }} + - + - Labels + State - - {{ l.name }}, - + {{ item.state }} - + ID - {{ item.id }}, + {{ item.id }} + + + + + + View + + + + open_in_new + - + - +
- +
+ +
diff --git a/demo/app/components/table/table.component.scss b/demo/app/components/table/table.component.scss index 99241e9dc..ec464b980 100644 --- a/demo/app/components/table/table.component.scss +++ b/demo/app/components/table/table.component.scss @@ -6,3 +6,10 @@ overflow: auto; @include visible-scrollbars; } + +.truncate { + display: block; + max-height: 100px; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/demo/app/components/table/table.component.ts b/demo/app/components/table/table.component.ts index d80e2de70..f8c95f21b 100644 --- a/demo/app/components/table/table.component.ts +++ b/demo/app/components/table/table.component.ts @@ -4,12 +4,16 @@ import { Component, ViewChild, } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TsPaginatorComponent, TsPaginatorMenuItem, } from '@terminus/ui/paginator'; import { TsSortDirective } from '@terminus/ui/sort'; -import { TsTableDataSource } from '@terminus/ui/table'; +import { + TsColumn, + TsTableDataSource, +} from '@terminus/ui/table'; import { merge, Observable, @@ -76,10 +80,14 @@ const COLUMNS_SOURCE_GITHUB = [ export interface GithubApi { items: GithubIssue[]; + // NOTE: Format controlled by GitHub + // eslint-disable-next-line camelcase total_count: number; } export interface GithubIssue { + // NOTE: Format controlled by GitHub + // eslint-disable-next-line camelcase created_at: string; number: string; state: string; @@ -92,11 +100,10 @@ export interface GithubIssue { export class ExampleHttpDao { constructor(private http: HttpClient) {} - getRepoIssues(sort: string, order: string, page: number, perPage: number): Observable { - const href = 'https://api.github.com/search/issues'; + public getRepoIssues(sort: string, order: string, page: number, perPage: number): Observable { + 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(`${requestUrl}${requestParams}`); } } @@ -108,8 +115,8 @@ export class ExampleHttpDao { styleUrls: ['./table.component.scss'], }) export class TableComponent implements AfterViewInit { - allColumns = COLUMNS_SOURCE_GITHUB.slice(0); - displayedColumns: string[] = [ + public allColumns = COLUMNS_SOURCE_GITHUB.slice(0); + public displayedColumns = [ 'title', 'updated', 'comments', @@ -117,26 +124,52 @@ export class TableComponent implements AfterViewInit { 'number', 'labels', 'created', - 'id', 'body', + 'id', + 'html_url', + ]; + public exampleDatabase!: ExampleHttpDao; + public dataSource = new TsTableDataSource(); + public resultsLength = 0; + public resizableColumns: TsColumn[] = [ + { + name: 'title', + width: '400px', + }, + { name: 'updated' }, + { name: 'comments' }, + { + name: 'assignee', + width: '160px', + }, + { name: 'number' }, + { + name: 'labels', + width: '260px', + }, + { name: 'created' }, + { name: 'id' }, + { + name: 'body', + width: '500px', + }, + { name: 'html_url' }, ]; - exampleDatabase!: ExampleHttpDao; - dataSource: TsTableDataSource = new TsTableDataSource(); - resultsLength = 0; - @ViewChild(TsSortDirective, {static: true}) - sort!: TsSortDirective; + @ViewChild(TsSortDirective, { static: true }) + public sort!: TsSortDirective; - @ViewChild(TsPaginatorComponent, {static: true}) - paginator!: TsPaginatorComponent; + @ViewChild(TsPaginatorComponent, { static: true }) + public readonly paginator!: TsPaginatorComponent; constructor( + private domSanitizer: DomSanitizer, private http: HttpClient, ) {} - ngAfterViewInit(): void { + public ngAfterViewInit(): void { this.exampleDatabase = new ExampleHttpDao(this.http); // If the user changes the sort order, reset back to the first page. @@ -171,12 +204,16 @@ export class TableComponent implements AfterViewInit { } - perPageChange(e: number): void { + public perPageChange(e: number): void { console.log('DEMO records per page changed: ', e); } - onPageSelect(e: TsPaginatorMenuItem): void { + public onPageSelect(e: TsPaginatorMenuItem): void { console.log('DEMO page selected: ', e); } + public sanitize(content): SafeHtml { + return this.domSanitizer.bypassSecurityTrustHtml(content); + } + } diff --git a/demo/app/components/table/table.module.ts b/demo/app/components/table/table.module.ts index f3f5bd553..80a41a937 100644 --- a/demo/app/components/table/table.module.ts +++ b/demo/app/components/table/table.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { FlexLayoutModule } from '@angular/flex-layout'; import { FormsModule } from '@angular/forms'; import { TsCardModule } from '@terminus/ui/card'; +import { TsIconModule } from '@terminus/ui/icon'; import { TsOptionModule } from '@terminus/ui/option'; import { TsPaginatorModule } from '@terminus/ui/paginator'; import { TsSelectModule } from '@terminus/ui/select'; @@ -21,6 +22,7 @@ import { TableComponent } from './table.component'; FormsModule, TableRoutingModule, TsCardModule, + TsIconModule, TsOptionModule, TsPaginatorModule, TsSelectModule, diff --git a/terminus-ui/sort/src/sort-header.component.ts b/terminus-ui/sort/src/sort-header.component.ts index e88c65494..b76c6f50a 100644 --- a/terminus-ui/sort/src/sort-header.component.ts +++ b/terminus-ui/sort/src/sort-header.component.ts @@ -13,10 +13,7 @@ import { Optional, ViewEncapsulation, } from '@angular/core'; -import { - CanDisable, - mixinDisabled, -} from '@angular/material/core'; +import { CanDisable } from '@angular/material/core'; import { coerceBooleanProperty } from '@terminus/ngx-tools/coercion'; import { isBoolean } from '@terminus/ngx-tools/type-guards'; import { untilComponentDestroyed } from '@terminus/ngx-tools/utilities'; @@ -48,12 +45,10 @@ import { * https://getterminus.github.io/ui-demos-release/components/table */ @Component({ - // NOTE(B$): This component needs to be added to another component so we need a non-element - // selector + // NOTE: This component needs to be added to another component so we need a non-element selector // tslint:disable: component-selector selector: '[ts-sort-header]', // tslint:enable: component-selector - exportAs: 'tsSortHeader', templateUrl: './sort-header.component.html', styleUrls: ['./sort-header.component.scss'], host: { @@ -63,8 +58,6 @@ import { '(click)': '_handleClick()', }, preserveWhitespaces: false, - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, // NOTE: @Inputs are defined here rather than using decorators since we are extending the @Inputs of the base class // tslint:disable-next-line:no-inputs-metadata-property inputs: ['disabled'], @@ -74,6 +67,9 @@ import { tsSortAnimations.rightPointer, tsSortAnimations.indicatorToggle, ], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + exportAs: 'tsSortHeader', }) export class TsSortHeaderComponent implements TsSortableItem, CanDisable, OnInit, OnDestroy { public disabled = false; diff --git a/terminus-ui/table/package.json b/terminus-ui/table/package.json index 39d9583f0..6266a819b 100644 --- a/terminus-ui/table/package.json +++ b/terminus-ui/table/package.json @@ -1,7 +1,10 @@ { "ngPackage": { "lib": { - "entryFile": "src/public-api.ts" + "entryFile": "src/public-api.ts", + "umdModuleIds": { + "@terminus/ngx-tools/type-guards": "terminus.ngxTools.type-guards" + } } } } diff --git a/terminus-ui/table/src/cell.ts b/terminus-ui/table/src/cell.ts index 467db23cd..528601ea3 100644 --- a/terminus-ui/table/src/cell.ts +++ b/terminus-ui/table/src/cell.ts @@ -1,5 +1,5 @@ -// NOTE(B$): In this case, we want an actual selector (so content can be nested inside) for our -// directive. So we are disabling the `directive-selector` rule +// NOTE: In this case, we want an actual selector (so content can be nested inside) for our directive. So we are disabling the +// `directive-selector` rule // tslint:disable: directive-selector import { CdkCell, @@ -11,11 +11,21 @@ import { import { Directive, ElementRef, + Host, Input, isDevMode, + Optional, Renderer2, + SkipSelf, } from '@angular/core'; +import { TsUILibraryError } from '@terminus/ui/utilities'; +import { TsTableComponent } from './table.component'; + + +/** + * Allowed column alignments + */ export type TsTableColumnAlignment = 'left' | 'center' @@ -25,7 +35,11 @@ export type TsTableColumnAlignment /** * An array of the allowed {@link TsTableColumnAlignment} for checking values */ -export const tsTableColumnAlignmentTypesArray: TsTableColumnAlignment[] = ['left', 'center', 'right']; +export const tsTableColumnAlignmentTypesArray: TsTableColumnAlignment[] = [ + 'left', + 'center', + 'right', +]; /** @@ -71,9 +85,10 @@ export class TsHeaderCellDefDirective extends CdkHeaderCellDef {} }) export class TsHeaderCellDirective extends CdkHeaderCell { constructor( - columnDef: CdkColumnDef, - elementRef: ElementRef, + public columnDef: CdkColumnDef, public renderer: Renderer2, + private elementRef: ElementRef, + @Optional() @Host() @SkipSelf() parent: TsTableComponent, ) { super(columnDef, elementRef); elementRef.nativeElement.classList.add(`ts-column-${columnDef.cssClassFriendlyName}`); @@ -81,10 +96,14 @@ export class TsHeaderCellDirective extends CdkHeaderCell { // tslint:disable-next-line no-any const column: any = columnDef; - // Set inline style for min-width if passed in - if (column.minWidth) { - renderer.setStyle(elementRef.nativeElement, 'flex-basis', column.minWidth); + if (parent.columns) { + const width = parent.columns.filter(c => c.name === columnDef.name).map(v => v.width)[0]; + // istanbul ignore else + if (width) { + renderer.setStyle(elementRef.nativeElement, 'flex-basis', width); + } } + } } @@ -100,44 +119,64 @@ export class TsHeaderCellDirective extends CdkHeaderCell { }, }) export class TsCellDirective extends CdkCell { + public column: TsColumnDefDirective; + constructor( - columnDef: CdkColumnDef, - elementRef: ElementRef, - public renderer: Renderer2, + private columnDef: CdkColumnDef, + private elementRef: ElementRef, + private renderer: Renderer2, + @Optional() @Host() @SkipSelf() parent: TsTableComponent, ) { super(columnDef, elementRef); - - // NOTE: We are adding `noWrap` to the column in `TsColumnDefDirective` which doesn't exist - // in the `CdkColumnDef` so we cast it to 'any'. - // tslint:disable-next-line no-any - const column: any = columnDef; + // NOTE: Changing the type in the constructor from `CdkColumnDef` to `TsColumnDefDirective` doesn't seem to play well with the CDK. + // Coercing the type here is a hack, but allows us to reference properties that do not exist on the underlying `CdkColumnDef`. + this.column = columnDef as TsColumnDefDirective; // Set a custom class for each column elementRef.nativeElement.classList.add(`ts-column-${columnDef.cssClassFriendlyName}`); // Set the no-wrap class if needed - if (column.noWrap) { + if (this.column.noWrap) { elementRef.nativeElement.classList.add(`ts-column-no-wrap`); } - // Set inline style for min-width if passed in - if (column.minWidth) { - renderer.setStyle(elementRef.nativeElement, 'flex-basis', column.minWidth); + TsCellDirective.setColumnAlignment(this.column, renderer, elementRef); + + if (parent.columns) { + const width = parent.columns.filter(c => c.name === columnDef.name).map(v => v.width)[0]; + // istanbul ignore else + if (width) { + renderer.setStyle(elementRef.nativeElement, 'flex-basis', width); + } + } + + // eslint-disable-next-line no-underscore-dangle + if (columnDef._stickyEnd) { + elementRef.nativeElement.classList.add(`ts-cell--sticky-end`); } + } - // Skip the following in or to maintain backward compatibility with cells that do not use alignment + + /** + * Set the column alignment styles + * + * @param column - The column definition + * @param renderer - The Renderer2 + * @param elementRef - The element ref to add the class to + */ + private static setColumnAlignment(column: TsColumnDefDirective, renderer: Renderer2, elementRef: ElementRef): void { if (column.alignment) { // Verify the alignment value is allowed if (tsTableColumnAlignmentTypesArray.indexOf(column.alignment) < 0 && isDevMode()) { - console.warn(`TsCellDirective: "${column.alignment}" is not an allowed alignment. ` - + `See TsTableColumnAlignment for available options.`); - return; + throw new TsUILibraryError(`TsCellDirective: "${column.alignment}" is not an allowed alignment.` + + `See "TsTableColumnAlignment" type for available options.`); } // Set inline style for text-align renderer.setStyle(elementRef.nativeElement, 'textAlign', column.alignment); } } + } @@ -153,10 +192,14 @@ export class TsCellDirective extends CdkCell { provide: CdkColumnDef, useExisting: TsColumnDefDirective, }, + { + provide: 'MAT_SORT_HEADER_COLUMN_DEF', + useExisting: TsColumnDefDirective, + }, ], }) export class TsColumnDefDirective extends CdkColumnDef { - // NOTE(B$): We must rename here so that the property matches the extended CdkColumnDef class + // NOTE: We must rename here so that the property matches the extended CdkColumnDef class // tslint:disable: no-input-rename /** * Define a unique name for this column @@ -171,12 +214,6 @@ export class TsColumnDefDirective extends CdkColumnDef { @Input() public noWrap = false; - /** - * Define a minimum width for the column - */ - @Input() - public minWidth: string | undefined; - /** * Define an alignment type for the cell. */ @@ -194,4 +231,5 @@ export class TsColumnDefDirective extends CdkColumnDef { */ @Input() public stickyEnd = false; + } diff --git a/terminus-ui/table/src/design-decisions.md b/terminus-ui/table/src/design-decisions.md index 59c82f8f0..244b1d52c 100644 --- a/terminus-ui/table/src/design-decisions.md +++ b/terminus-ui/table/src/design-decisions.md @@ -22,3 +22,5 @@ recreate a spreadsheet. Row selection (single or multiple) is not implemented. UX needs a better use-case for this need before we take the time to build it in. + +This can be added by the consumer fairly easily. diff --git a/terminus-ui/table/src/row.ts b/terminus-ui/table/src/row.ts index 1195fe4d4..2bce71c3f 100644 --- a/terminus-ui/table/src/row.ts +++ b/terminus-ui/table/src/row.ts @@ -11,6 +11,7 @@ import { Directive, ViewEncapsulation, } from '@angular/core'; +import { TsTableComponent } from './table.component'; /** diff --git a/terminus-ui/table/src/table-data-source.spec.ts b/terminus-ui/table/src/table-data-source.spec.ts index ff9a6ad07..85f4ef904 100644 --- a/terminus-ui/table/src/table-data-source.spec.ts +++ b/terminus-ui/table/src/table-data-source.spec.ts @@ -10,20 +10,18 @@ interface Foo { // Additional tests for parts missed by the {@link TsTableComponent} integration test describe(`TsTableDataSource`, function() { let source: TsTableDataSource; - let seededSource: TsTableDataSource; + let seededSource: TsTableDataSource; beforeEach(() => { source = new TsTableDataSource(); seededSource = new TsTableDataSource([{ foo: 'bar' }]); }); - test(`should initialize an empty array if no data passed in`, () => { expect(source.data).toEqual([]); expect(seededSource.data).toEqual([{ foo: 'bar' }]); }); - describe(`in _renderChangesSubscription exists`, () => { test(`should be unsubscribed from`, () => { @@ -35,7 +33,6 @@ describe(`TsTableDataSource`, function() { }); - test(`should have a disconnected() noop`, () => { expect(source.disconnect()).toEqual(undefined); }); diff --git a/terminus-ui/table/src/table.component.md b/terminus-ui/table/src/table.component.md index 19ff0bf2e..2bf62db36 100644 --- a/terminus-ui/table/src/table.component.md +++ b/terminus-ui/table/src/table.component.md @@ -3,34 +3,33 @@ **Table of Contents** - [Basic usage](#basic-usage) - - [1. Define the table's columns](#1-define-the-tables-columns) - - [2. Define the table's rows](#2-define-the-tables-rows) - - [3. Provide data](#3-provide-data) - - [4. Full HTML example](#4-full-html-example) + - [1. Define the columns HTML](#1-define-the-columns-html) + - [2. Define the displayed columns](#2-define-the-displayed-columns) + - [3. Define the table's rows](#3-define-the-tables-rows) + - [4. Provide data](#4-provide-data) + - [Full HTML example](#full-html-example) - [Dynamically update table data](#dynamically-update-table-data) - [Dynamic columns](#dynamic-columns) - [Sorting by column](#sorting-by-column) - [Row selection](#row-selection) - [No-wrap for a column](#no-wrap-for-a-column) -- [Min-width for a column](#min-width-for-a-column) -- [Alignment for a cell](#alignment-for-a-cell) +- [Cell alignment](#cell-alignment) - [Sticky header](#sticky-header) - [Sticky column](#sticky-column) - - [StickyEnd](#stickyend) -- [Full example with pagination, sorting and dynamic columns](#full-example-with-pagination-sorting-and-dynamic-columns) + - [Sticky column at end](#sticky-column-at-end) +- [Full example with pagination, sorting, and dynamic columns](#full-example-with-pagination-sorting-and-dynamic-columns) -## Basic usage +## Basic usage -### 1. Define the table's columns +### 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. +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. +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. ```html @@ -39,20 +38,32 @@ and the row cell will render the name property of each row's data. + {{ item.username }} ``` -### 2. Define the table's rows +### 2. Define the displayed columns + +The table expects an array of `TsColumn` definitions to manage the displayed columns and (optionally) their associated widths. -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. +```typescript +const columns: TsColumn = [ + {name: 'title', width: '200px'}, + {name: 'body', width: '400px'}, + // The width will default to `7rem` if not defined here + {name: 'link'}, +]; +``` -It is not required to provide a list of all the defined column names, but only the ones that you -want to have rendered. +### 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. ```html @@ -63,11 +74,43 @@ want to have rendered. ``` +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 different arrays: + +```html + + + + + + + + + +``` + +Mapping the array of names manually is also fairly simple: + +```typescript +const columns: TsColumn = [ + {name: 'title', width: '200px'}, + {name: 'body', width: '400px'}, + {name: 'link'}, +]; +const columnName = this.columns.map(c => c.name); +``` + -### 3. Provide data +### 4. Provide data -The column and row definitions now capture how data will render - all that's left is to provide the -data itself. +The column and row definitions now 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. @@ -88,7 +131,7 @@ The `DataSource` can be seeded with initial data: this.myDataSource = new TsTableDataSource(INITIAL_DATA); ``` -An interface for your table item can be passed to `TsTableDataSource` for stricter typing: +An interface for your table data can be passed to `TsTableDataSource` for stricter typing: ```typescript export interface MyTableItem { @@ -96,16 +139,15 @@ export interface MyTableItem { id: number; } -this.myDataSource: TsTableDataSource = new TsTableDataSource(INITIAL_DATA) +this.myDataSource = new TsTableDataSource(INITIAL_DATA); ``` -### 4. Full HTML example +### Full HTML example ```html - - + @@ -129,13 +171,10 @@ this.myDataSource: TsTableDataSource = new TsTableDataSource(INITIA - - + - - - + ``` @@ -145,11 +184,10 @@ this.myDataSource: TsTableDataSource = new TsTableDataSource(INITIA Your data source was created during the bootstraping of your component: ```typescript -this.myDataSource = new TsTableDataSource(); +this.myDataSource = new TsTableDataSource(); ``` -Simply assign the new data to `myDataSource.data`. The table will flush the old data and display the -new data: +Simply assign the new data to `myDataSource.data`. The table will flush the old data and display the new data: ```typescript this.myDataSource.data = dataToRender; @@ -158,88 +196,50 @@ this.myDataSource.data = dataToRender; ## Dynamic columns -Enable dynamic columns using a `TsSelectComponent`: - -```typescript -// Define a data source -this.myDataSource = new TsTableDataSource(); - -// Define all available columns (`TsSelectComponent` requires an array of objects) -allColumns = [ - { - name: 'Username', - myValue: 'username', - }, - { - name: 'Age', - myValue: 'age', - }, -]; -``` - -Both the `TsSelectComponent` and the `tsRowDef` `columns` input should reference the same `ngModel` -(`displayedColumns` in this example). +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). ```html - - - + + - - - - - - - - + + ``` ## Sorting by column -To add sorting behavior to the table, add the `tsSort` directive to the `` 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 +To add sorting behavior to the table, add the `tsSort` directive to the `` 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. +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. ```html - - - + - + Position - {{ element.position }} - ``` In your class, get a reference to the `TsSortDirective` using `@ViewChild`: ```typescript -import { AfterViewInit } from '@angular/core'; +import { AfterViewInit, ViewChild } from '@angular/core'; import { TsSortDirective } from '@terminus/ui/sort'; export class TableComponent implements AfterViewInit { @@ -249,8 +249,9 @@ export class TableComponent implements AfterViewInit { public ngAfterViewInit(): void { // Subscribe to the sortChange event to reset pagination, fetch new data, etc - this.sort.sortChange.subscribe(() => { + this.sort.sortChange.subscribe(sortState => { // Table was sorted - go get new data! + console.log('Table sort changed! ', sortState); }); } } @@ -259,18 +260,17 @@ export class TableComponent implements AfterViewInit { ## Row selection -Possible but not implemented until a valid use-case arises. +This can be implemented at the consumer level by adding a column that contains a checkbox. ## No-wrap for a column -Sometimes a column's content should not wrap even at small viewport sizes. Adding the directive -`noWrap="true"` to the column will keep then contents from wrapping regardless of the viewport -width. +Sometimes a column's content should not wrap, even at small viewport sizes. Setting the `@Input` `[noWrap]="true"` on the column will keep +the contents from wrapping regardless of the viewport width. ```html - - + + Created @@ -281,32 +281,13 @@ width. ``` -## Min-width for a column +## Cell alignment -Defining a minimum width for specific columns can help the layout not compress certain columns past -a readable point. Add the directive `minWidth` and pass it any valid CSS min-width value (`100px`, -`10%`, `12rem`, etc..). +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`). ```html - - - - Created - - - {{ item.created_at | date:shortDate }} - - -``` - - -## Alignment for a cell - -Defining an alignment style for a cell can set the horizontal alignment of text inside the cell. Add the -directive `alignment` and pass it any valid TsTableColumnAlignment value (`left`, `center` or `right`). - -```html - + Created @@ -320,70 +301,63 @@ directive `alignment` and pass it any valid TsTableColumnAlignment value (`left` ## 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. +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. ```html - + ``` ## Sticky column -Defining a sticky column will pin it to the left side as the table scrolls horizontally. Add the sticky parameter to the `` of the column definition. This can be applied to more than one 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. ```html - - - Updated - - - {{ item.updated_at | date:"shortDate" }} - - + + + Updated + + + {{ item.updated_at | date:"shortDate" }} + + ``` -### StickyEnd +### Sticky column at end -Defining a stickyEnd column will pin it to the right side as the table scrolls horizontally. Add the stickyEnd parameter to the `` of the column definition. This can be applied to more than one column. +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. ```html - - - Updated - - - {{ item.updated_at | date:"shortDate" }} - - + + + Updated + + + {{ item.updated_at | date:"shortDate" }} + + ``` --- -## Full example with pagination, sorting and dynamic columns - +## Full example with pagination, sorting, and dynamic columns ```typescript -import { - Component, - ViewChild, - AfterViewInit, -} from '@angular/core'; +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, - TsSortDirective, - TsPaginatorComponent, - TsPaginatorMenuItem, -} from '@terminus/ui/table'; +import { TsTableDataSource, TsColumn } from '@terminus/ui/table'; +import { TsPaginatorComponent } from '@terminus/ui/paginator'; @Component({ @@ -400,14 +374,13 @@ import { > - - - + Created @@ -416,7 +389,7 @@ import { - + Number @@ -434,7 +407,7 @@ import { - + State @@ -443,7 +416,7 @@ import { - + Comments @@ -452,12 +425,8 @@ import { - - - - - - + + @@ -477,7 +446,7 @@ import { `, }) export class TableComponent implements AfterViewInit { - allColumns: any[] = [ + allColumns: TsColumn[] = [ { name: 'Created', value: 'created', @@ -485,10 +454,12 @@ export class TableComponent implements AfterViewInit { { name: 'Title', value: 'title', + width: '12.5rem', }, { name: 'Comments', value: 'comments', + width: '500px', }, { name: 'State', @@ -499,16 +470,11 @@ export class TableComponent implements AfterViewInit { value: 'number', }, ]; - displayedColumns: string[] = [ - 'created', - 'number', - 'title', - 'state', - 'comments', - ]; + // Default to all columns visible + displayedColumns: TsColumn[] = this.allColumns.slice(); exampleDatabase: ExampleHttpDao | null; - dataSource: TsTableDataSource = new TsTableDataSource(); - resultsLength: number = 0; + dataSource = new TsTableDataSource(); + resultsLength = 0; @ViewChild(TsSortDirective, {static: true}) sort: TsSortDirective; @@ -516,21 +482,17 @@ export class TableComponent implements AfterViewInit { @ViewChild(TsPaginatorComponent, {static: true}) paginator: TsPaginatorComponent; - constructor( private http: HttpClient, ) {} - ngAfterViewInit(): void { + 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; - }); + 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 + // 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({}), @@ -545,7 +507,6 @@ export class TableComponent implements AfterViewInit { map(data => { console.log('Demo: fetched data: ', data) this.resultsLength = data.total_count; - return data.items; }), catchError(() => { @@ -578,11 +539,10 @@ export interface GithubIssue { export class ExampleHttpDao { constructor(private http: HttpClient) {} - getRepoIssues(sort: string, order: string, page: number, perPage: number): Observable { - const href = 'https://api.github.com/search/issues'; + public getRepoIssues(sort: string, order: string, page: number, perPage: number): Observable { + 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(`${requestUrl}${requestParams}`); } } diff --git a/terminus-ui/table/src/table.component.scss b/terminus-ui/table/src/table.component.scss index 89376a1ac..9920fe6e3 100644 --- a/terminus-ui/table/src/table.component.scss +++ b/terminus-ui/table/src/table.component.scss @@ -21,7 +21,6 @@ $ts-row-hover: rgba(color(utility, xlight), .5); @include reset; @include typography; display: inline-block; - max-height: 100%; // // Rows @@ -71,23 +70,24 @@ $ts-row-hover: rgba(color(utility, xlight), .5); // Body cell .ts-cell { + $sticky-border: 1px solid color(utility, xlight); background-color: color(pure); padding: spacing(default); vertical-align: middle; - &.ts-table--sticky { - border-right: 1px solid color(utility, xlight); + &--sticky { + border-right: $sticky-border; + } + + &--sticky-end { + border-left: $sticky-border; } } // Header cell .ts-header-cell { background-color: color(pure); - // If the column isn't sortable, add padding here that would normally be added by - // `ts-sort-header-container` - &:not(.ts-sortable) { - padding: spacing(default); - } + padding: spacing(default); &.ts-sortable { border-bottom: 3px solid color(utility, light); @@ -100,11 +100,6 @@ $ts-row-hover: rgba(color(utility, xlight), .5); border-bottom-color: color(accent); color: color(accent); } - - //
inner container wrapping the sort control - .ts-sort-header-container { - padding: spacing(default); - } } // Any cell @@ -112,7 +107,6 @@ $ts-row-hover: rgba(color(utility, xlight), .5); .ts-header-cell { align-items: stretch; background-color: color(pure); - display: flex; flex: 1; flex-basis: 6em; min-height: inherit; diff --git a/terminus-ui/table/src/table.component.spec.ts b/terminus-ui/table/src/table.component.spec.ts index e6ac57cfd..03cbaaa25 100644 --- a/terminus-ui/table/src/table.component.spec.ts +++ b/terminus-ui/table/src/table.component.spec.ts @@ -1,18 +1,31 @@ /* eslint-disable no-underscore-dangle */ import { ComponentFixture } from '@angular/core/testing'; -import { createComponent } from '@terminus/ngx-tools/testing'; +import { + createComponent, + ElementRefMock, +} from '@terminus/ngx-tools/testing'; import * as testComponents from '@terminus/ui/table/testing'; // eslint-disable-next-line no-duplicate-imports import { expectTableToMatchContent, + getCells, getHeaderCells, getHeaderRow, } from '@terminus/ui/table/testing'; +import { + TsCellDirective, + TsColumnDefDirective, +} from './cell'; import { TsTableDataSource } from './table-data-source'; import { TsTableModule } from './table.module'; +interface TestData { + a: string|number|undefined; + b: string|number|undefined; + c: string|number|undefined; +} describe(`TsTableComponent`, function() { @@ -34,7 +47,6 @@ describe(`TsTableComponent`, function() { ]); }); - test(`should create a table with special when row`, function() { const fixture = createComponent(testComponents.TableWithWhenRowApp, [], [TsTableModule]); fixture.detectChanges(); @@ -50,7 +62,6 @@ describe(`TsTableComponent`, function() { }); }); - describe(`with TsTableDataSource`, function() { let tableElement: HTMLElement; let fixture: ComponentFixture; @@ -66,7 +77,6 @@ describe(`TsTableComponent`, function() { dataSource = fixture.componentInstance.dataSource; }); - test(`should create table and display data source contents`, function() { expectTableToMatchContent(tableElement, [ ['Column A', 'Column B', 'Column C'], @@ -76,7 +86,6 @@ describe(`TsTableComponent`, function() { ]); }); - test(`changing data should update the table contents`, function() { // Add data component.underlyingDataSource.addData(); @@ -102,14 +111,12 @@ describe(`TsTableComponent`, function() { ]); }); - test(`should add the no wrap class`, function() { const noWrapColumn = fixture.nativeElement.querySelector('.ts-column-no-wrap'); expect(noWrapColumn).toBeTruthy(); }); - test(`should add the min-width style`, function() { const column = fixture.nativeElement.querySelector('.ts-cell.ts-column-column_b'); const headerColumn = fixture.nativeElement.querySelector('.ts-header-cell.ts-column-column_b'); @@ -121,13 +128,12 @@ describe(`TsTableComponent`, function() { headerStyle = headerColumn.style._values['flex-basis']; } - expect(style).toEqual('100px'); - expect(headerStyle).toEqual('100px'); + expect(style).toEqual('7rem'); + expect(headerStyle).toEqual('7rem'); }); }); - describe(`table column alignment`, () => { let fixture: ComponentFixture; @@ -171,20 +177,17 @@ describe(`TsTableComponent`, function() { }); + test(`should throw error for invalid alignment arguments`, () => { + const col = new TsColumnDefDirective(); + Object.defineProperties(col, { alignment: { get: () => 'foo' } }); - describe(`invalid alignment argument`, () => { - - test(`should throw warning`, () => { - window.console.warn = jest.fn(); - const fixture = createComponent(testComponents.TableColumnInvalidAlignmentTableApp, [], [TsTableModule]); - fixture.detectChanges(); - - expect(window.console.warn).toHaveBeenCalled(); - }); + const actual = () => { + const test = new TsCellDirective(col, new ElementRefMock(), {} as any, {} as any); + }; + expect(actual).toThrowError('TsCellDirective: '); }); - describe(`pinned header and column`, () => { let fixture: ComponentFixture; let tableElement: HTMLElement; @@ -198,13 +201,16 @@ describe(`TsTableComponent`, function() { test(`should set a header to be sticky`, () => { const header = getHeaderRow(tableElement); - expect(header.classList).toContain('ts-table--sticky'); + expect(header.classList).toContain('ts-cell--sticky'); }); test(`should set a column to be sticky`, () => { const headerCells = getHeaderCells(tableElement); - expect(headerCells[0].classList).toContain('ts-table--sticky'); - expect(headerCells[1].classList).not.toContain('ts-table--sticky'); + const cells = getCells(tableElement); + expect(headerCells[0].classList).toContain('ts-cell--sticky'); + expect(headerCells[1].classList).not.toContain('ts-cell--sticky'); + expect(cells[0].classList).not.toContain('ts-cell--sticky-end'); + expect(cells[2].classList).toContain('ts-cell--sticky-end'); }); }); diff --git a/terminus-ui/table/src/table.component.ts b/terminus-ui/table/src/table.component.ts index 2d59c0447..94ea13a30 100644 --- a/terminus-ui/table/src/table.component.ts +++ b/terminus-ui/table/src/table.component.ts @@ -2,8 +2,27 @@ import { CdkTable } from '@angular/cdk/table'; import { ChangeDetectionStrategy, Component, + Input, ViewEncapsulation, } from '@angular/core'; +import { isUndefined } from '@terminus/ngx-tools/type-guards'; + + +/** + * The definition for a single column + */ +export interface TsColumn { + // The column name + name: string; + // The desired width as a string (eg '200px', '14rem' etc) + width?: string; + // Allow any other data properties the consumer may need + // tslint:disable-next-line no-any + [key: string]: any; +} + +// Default column width = ~112px +const DEFAULT_COLUMN_WIDTH = '7rem'; /** @@ -47,13 +66,40 @@ import { provide: CdkTable, useExisting: TsTableComponent, }], - exportAs: 'tsTable', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + exportAs: 'tsTable', }) export class TsTableComponent extends CdkTable { /** - * Override CDK's class + * Override the sticky CSS class set by the `CdkTable` */ - protected stickyCssClass = 'ts-table--sticky'; + protected stickyCssClass = 'ts-cell--sticky'; + + /** + * Return a simple array of column names + * + * Used by {@link TsHeaderRowDefDirective} and {@link TsRowDefDirective}. + */ + public get columnNames(): string[] { + return this.columns.map(c => c.name); + } + + /** + * Define the array of columns + */ + @Input() + public set columns(value: ReadonlyArray) { + this._columns = value.map(column => { + if (isUndefined(column.width)) { + column.width = DEFAULT_COLUMN_WIDTH; + } + return column; + }); + } + public get columns(): ReadonlyArray { + return this._columns; + } + private _columns: ReadonlyArray; + } diff --git a/terminus-ui/table/testing/src/test-components.ts b/terminus-ui/table/testing/src/test-components.ts index b87ae0a04..4318223c8 100644 --- a/terminus-ui/table/testing/src/test-components.ts +++ b/terminus-ui/table/testing/src/test-components.ts @@ -21,7 +21,7 @@ import { @Component({ template: ` - + Column A {{ row.a }} @@ -53,13 +53,17 @@ export class TableApp { public dataSource: FakeDataSource | null = new FakeDataSource(); public columnsToRender = ['column_a', 'column_b', 'column_c']; + public columns = this.columnsToRender.map(c => ({ + name: c, + width: '112px', + })); public isFourthRow = (i: number, _rowData: TestData) => i === 3; } @Component({ template: ` - + Column A {{ row.a }} @@ -80,18 +84,20 @@ export class TableWithWhenRowApp { public table!: TsTableComponent; public dataSource: FakeDataSource | null = new FakeDataSource(); public isFourthRow = (i: number, _rowData: TestData) => i === 3; + public columnsToRender = ['column_a']; + public columns = this.columnsToRender.map(c => ({ name: c })); } @Component({ template: ` - + Column A {{ row.a }} - + Column B {{ row.b }} @@ -110,6 +116,7 @@ export class ArrayDataSourceTableApp { public underlyingDataSource = new FakeDataSource(); public dataSource = new TsTableDataSource(); public columnsToRender = ['column_a', 'column_b', 'column_c']; + public columns = this.columnsToRender.map(c => ({ name: c })); @ViewChild(TsTableComponent, { static: true }) public table!: TsTableComponent; @@ -134,7 +141,7 @@ export class ArrayDataSourceTableApp { @Component({ template: ` - + Column A {{ row.a }} @@ -159,6 +166,7 @@ export class TableColumnAlignmentTableApp { public underlyingDataSource = new FakeDataSource(); public dataSource = new TsTableDataSource(); public columnsToRender = ['column_a', 'column_b', 'column_c']; + public columns = this.columnsToRender.map(c => ({ name: c })); @ViewChild(TsTableComponent, { static: true }) public table!: TsTableComponent; @@ -177,7 +185,7 @@ export class TableColumnAlignmentTableApp { @Component({ template: ` - + Column A {{ row.a }} @@ -191,6 +199,7 @@ export class TableColumnInvalidAlignmentTableApp { public underlyingDataSource = new FakeDataSource(); public dataSource = new TsTableDataSource(); public columnsToRender = ['column_a']; + public columns = this.columnsToRender.map(c => ({ name: c })); @ViewChild(TsTableComponent, { static: true }) public table!: TsTableComponent; @@ -221,7 +230,7 @@ export class TableColumnInvalidAlignmentTableApp { {{ row.b }} - + Column C {{ row.c }}