Skip to content

Commit 697c633

Browse files
committed
feat(table): add text column for simple columns
1 parent 0fd6456 commit 697c633

20 files changed

+628
-222
lines changed

src/cdk/table/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './row';
1212
export * from './table-module';
1313
export * from './sticky-styler';
1414
export * from './can-stick';
15+
export * from './text-column';
1516

1617
/** Re-export DataSource for a more intuitive experience for users of just the table. */
1718
export {DataSource} from '@angular/cdk/collections';

src/cdk/table/table-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
CdkColumnDef, CdkHeaderCellDef, CdkHeaderCell, CdkCell, CdkCellDef,
1818
CdkFooterCellDef, CdkFooterCell
1919
} from './cell';
20+
import {CdkTextColumn} from './text-column';
2021

2122
const EXPORTED_DECLARATIONS = [
2223
CdkTable,
@@ -37,6 +38,7 @@ const EXPORTED_DECLARATIONS = [
3738
DataRowOutlet,
3839
HeaderRowOutlet,
3940
FooterRowOutlet,
41+
CdkTextColumn,
4042
];
4143

4244
@NgModule({

src/cdk/table/table.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2247,7 +2247,7 @@ function getActualTableContent(tableElement: Element): string[][] {
22472247
return actualTableContent.map(row => row.map(cell => cell.textContent!.trim()));
22482248
}
22492249

2250-
function expectTableToMatchContent(tableElement: Element, expected: any[]) {
2250+
export function expectTableToMatchContent(tableElement: Element, expected: any[]) {
22512251
const missedExpectations: string[] = [];
22522252
function checkCellContent(actualCell: string, expectedCell: string) {
22532253
if (actualCell !== expectedCell) {

src/cdk/table/text-column.spec.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import {Component} from '@angular/core';
2+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
3+
import {CdkTableModule} from './table-module';
4+
import {expectTableToMatchContent} from './table.spec';
5+
import {TextColumnOptions, TEXT_COLUMN_OPTIONS} from './text-column';
6+
7+
describe('CdkTextColumn', () => {
8+
let fixture: ComponentFixture<BasicTextColumnApp>;
9+
let component: BasicTextColumnApp;
10+
let tableElement: HTMLElement;
11+
12+
beforeEach(async(() => {
13+
TestBed.configureTestingModule({
14+
imports: [CdkTableModule],
15+
declarations: [
16+
BasicTextColumnApp,
17+
],
18+
}).compileComponents();
19+
}));
20+
21+
beforeEach(() => {
22+
fixture = TestBed.createComponent(BasicTextColumnApp);
23+
component = fixture.componentInstance;
24+
fixture.detectChanges();
25+
26+
tableElement = fixture.nativeElement.querySelector('.cdk-table');
27+
});
28+
29+
it('should render the basic columns', () => {
30+
expectTableToMatchContent(tableElement, [
31+
['PropertyA', 'PropertyB', 'PropertyC'],
32+
['a_1', 'b_1', 'c_1'],
33+
['a_2', 'b_2', 'c_2'],
34+
]);
35+
});
36+
37+
it('should allow for alternate header text', () => {
38+
component.headerTextB = 'column-b';
39+
fixture.detectChanges();
40+
41+
expectTableToMatchContent(tableElement, [
42+
['PropertyA', 'column-b', 'PropertyC'],
43+
['a_1', 'b_1', 'c_1'],
44+
['a_2', 'b_2', 'c_2'],
45+
]);
46+
});
47+
48+
it('should allow for custom data accessor', () => {
49+
component.dataAccessorA = (data: TestData) => data.propertyA + '!';
50+
fixture.detectChanges();
51+
52+
expectTableToMatchContent(tableElement, [
53+
['PropertyA', 'PropertyB', 'PropertyC'],
54+
['a_1!', 'b_1', 'c_1'],
55+
['a_2!', 'b_2', 'c_2'],
56+
]);
57+
});
58+
59+
it('should allow for custom data accessor', () => {
60+
component.dataAccessorA = (data: TestData) => data.propertyA + '!';
61+
fixture.detectChanges();
62+
63+
expectTableToMatchContent(tableElement, [
64+
['PropertyA', 'PropertyB', 'PropertyC'],
65+
['a_1!', 'b_1', 'c_1'],
66+
['a_2!', 'b_2', 'c_2'],
67+
]);
68+
});
69+
70+
it('should update values when data changes', () => {
71+
component.data = [
72+
{propertyA: 'changed-a_1', propertyB: 'b_1', propertyC: 'c_1'},
73+
{propertyA: 'changed-a_2', propertyB: 'b_2', propertyC: 'c_2'},
74+
];
75+
fixture.detectChanges();
76+
77+
expectTableToMatchContent(tableElement, [
78+
['PropertyA', 'PropertyB', 'PropertyC'],
79+
['changed-a_1', 'b_1', 'c_1'],
80+
['changed-a_2', 'b_2', 'c_2'],
81+
]);
82+
});
83+
84+
it('should be able to justify the text', () => {
85+
component.justifyC = 'end';
86+
fixture.detectChanges();
87+
88+
const headerB = tableElement.querySelector('th:nth-child(2')! as HTMLElement;
89+
const headerC = tableElement.querySelector('th:nth-child(3')! as HTMLElement;
90+
expect(headerB.style.textAlign).toBe('start');
91+
expect(headerC.style.textAlign).toBe('end');
92+
93+
const cellB = tableElement.querySelector('td:nth-child(2')! as HTMLElement;
94+
const cellC = tableElement.querySelector('td:nth-child(3')! as HTMLElement;
95+
expect(cellB.style.textAlign).toBe('start');
96+
expect(cellC.style.textAlign).toBe('end');
97+
});
98+
99+
describe('with options', () => {
100+
function createTestComponent(options: TextColumnOptions) {
101+
// Reset the previously configured testing module to be able set new providers.
102+
// The testing module has been initialized in the root describe group for the ripples.
103+
TestBed.resetTestingModule();
104+
TestBed.configureTestingModule({
105+
imports: [CdkTableModule],
106+
declarations: [BasicTextColumnApp],
107+
providers: [{provide: TEXT_COLUMN_OPTIONS, useValue: options}]
108+
});
109+
110+
fixture = TestBed.createComponent(BasicTextColumnApp);
111+
fixture.detectChanges();
112+
113+
tableElement = fixture.nativeElement.querySelector('.cdk-table');
114+
}
115+
116+
it('should be able to provide a header text transformation', () => {
117+
const defaultHeaderTextTransformation = (name: string) => `${name}!`;
118+
createTestComponent({ defaultHeaderTextTransformation });
119+
120+
expectTableToMatchContent(tableElement, [
121+
['propertyA!', 'propertyB!', 'propertyC!'],
122+
['a_1', 'b_1', 'c_1'],
123+
['a_2', 'b_2', 'c_2'],
124+
]);
125+
});
126+
127+
it('should be able to provide a general data accessor', () => {
128+
const defaultDataAccessor = (data: TestData, name: string) => {
129+
switch (name) {
130+
case 'propertyA':
131+
return `A: ${data.propertyA}`;
132+
case 'propertyB':
133+
return `B: ${data.propertyB}`;
134+
case 'propertyC':
135+
return `C: ${data.propertyC}`;
136+
default:
137+
return '';
138+
}
139+
};
140+
createTestComponent({ defaultDataAccessor });
141+
142+
expectTableToMatchContent(tableElement, [
143+
['PropertyA', 'PropertyB', 'PropertyC'],
144+
['A: a_1', 'B: b_1', 'C: c_1'],
145+
['A: a_2', 'B: b_2', 'C: c_2'],
146+
]);
147+
});
148+
});
149+
});
150+
151+
interface TestData {
152+
propertyA: string;
153+
propertyB: string;
154+
propertyC: string;
155+
}
156+
157+
@Component({
158+
template: `
159+
<cdk-table [dataSource]="data">
160+
<cdk-text-column name="propertyA" [dataAccessor]="dataAccessorA"></cdk-text-column>
161+
<cdk-text-column name="propertyB" [headerText]="headerTextB"></cdk-text-column>
162+
<cdk-text-column name="propertyC" [justify]="justifyC"></cdk-text-column>
163+
164+
<cdk-header-row *cdkHeaderRowDef="displayedColumns"></cdk-header-row>
165+
<cdk-row *cdkRowDef="let row; columns: displayedColumns"></cdk-row>
166+
</cdk-table>
167+
`
168+
})
169+
class BasicTextColumnApp {
170+
displayedColumns = ['propertyA', 'propertyB', 'propertyC'];
171+
172+
data: TestData[] = [
173+
{propertyA: 'a_1', propertyB: 'b_1', propertyC: 'c_1'},
174+
{propertyA: 'a_2', propertyB: 'b_2', propertyC: 'c_2'},
175+
];
176+
177+
headerTextB: string;
178+
dataAccessorA: (data: TestData) => string;
179+
justifyC = 'start';
180+
}

src/cdk/table/text-column.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
Component,
11+
Inject,
12+
InjectionToken,
13+
Input,
14+
OnDestroy,
15+
OnInit,
16+
Optional,
17+
ViewChild,
18+
ChangeDetectionStrategy,
19+
ViewEncapsulation
20+
} from '@angular/core';
21+
import {CdkColumnDef} from './cell';
22+
import {CdkTable} from './table';
23+
24+
/** Configurable options for `CdkTextColumn`. */
25+
export interface TextColumnOptions {
26+
/**
27+
* Default function that provides the header text based on the column name if a header
28+
* text is not provided.
29+
*/
30+
defaultHeaderTextTransformation?: (name: string) => string;
31+
32+
/** Default data accessor to use if one is not provided. */
33+
defaultDataAccessor?: (data: any, name: string) => string;
34+
}
35+
36+
/** Injection token that can be used to specify the text column options. */
37+
export const TEXT_COLUMN_OPTIONS = new InjectionToken<TextColumnOptions>('text-column-options');
38+
39+
/**
40+
* Column that simply shows text content for the header and row cells. Assumes that the table
41+
* is using the native table implementation (`<table>`).
42+
*
43+
* By default, the name of this column will be the header text and data property accessor.
44+
* The header text can be overridden with the `headerText` input. Cell values can be overridden with
45+
* the `dataAccessor` input. Change the text justification to the start or end using the `justify`
46+
* input.
47+
*/
48+
@Component({
49+
moduleId: module.id,
50+
selector: 'cdk-text-column',
51+
template: `
52+
<ng-container cdkColumnDef>
53+
<th cdk-header-cell *cdkHeaderCellDef [style.text-align]="justify">
54+
{{headerText}}
55+
</th>
56+
<td cdk-cell *cdkCellDef="let data" [style.text-align]="justify">
57+
{{dataAccessor(data, name)}}
58+
</td>
59+
</ng-container>
60+
`,
61+
encapsulation: ViewEncapsulation.None,
62+
// Change detection is intentionally not set to OnPush. This component's template will be provided
63+
// to the table to be inserted into its view. This is problematic when change detection runs since
64+
// the bindings in this template will be evaluated _after_ the table's view is evaluated, which
65+
// mean's the template in the table's view will not have the updated value (and in fact will cause
66+
// an ExpressionChangedAfterItHasBeenCheckedError).
67+
// tslint:disable-next-line:validate-decorators
68+
changeDetection: ChangeDetectionStrategy.Default,
69+
})
70+
export class CdkTextColumn<T> implements OnDestroy, OnInit {
71+
/** Column name that should be used to reference this column. */
72+
@Input()
73+
get name(): string { return this._name; }
74+
set name(name: string) {
75+
this._name = name;
76+
this.columnDef.name = name;
77+
}
78+
_name: string;
79+
80+
/**
81+
* Text label that should be used for the column header. If this property is not
82+
* set, the header text will default to the column name with its first letter capitalized.
83+
*/
84+
@Input() headerText: string;
85+
86+
/**
87+
* Accessor function to retrieve the data rendered for each cell. If this
88+
* property is not set, the data cells will render the value found in the data's property matching
89+
* the column's name. For example, if the column is named `id`, then the rendered value will be
90+
* value defined by the data's `id` property.
91+
*/
92+
@Input() dataAccessor: (data: T, name: string) => string;
93+
94+
/** Alignment of the cell values. */
95+
@Input() justify: 'start' | 'end' = 'start';
96+
97+
@ViewChild(CdkColumnDef) columnDef: CdkColumnDef;
98+
99+
constructor(@Optional() private table: CdkTable<T>,
100+
@Optional() @Inject(TEXT_COLUMN_OPTIONS) private options: TextColumnOptions) {
101+
this.options = options || {};
102+
}
103+
104+
ngOnInit() {
105+
if (this.headerText === undefined) {
106+
this.headerText = this._createDefaultHeaderText();
107+
}
108+
109+
if (!this.dataAccessor) {
110+
this.dataAccessor = this.options.defaultDataAccessor ||
111+
((data: T, name: string) => (data as any)[name]);
112+
}
113+
114+
if (this.table) {
115+
this.table.addColumnDef(this.columnDef);
116+
}
117+
}
118+
119+
ngOnDestroy() {
120+
if (this.table) {
121+
this.table.removeColumnDef(this.columnDef);
122+
}
123+
}
124+
125+
/**
126+
* Creates a default header text. Use the options' header text transformation function if one
127+
* has been provided. Otherwise simply capitalize the column name.
128+
*/
129+
_createDefaultHeaderText() {
130+
if (this.options && this.options.defaultHeaderTextTransformation) {
131+
return this.options.defaultHeaderTextTransformation(this.name);
132+
}
133+
134+
return this.name.charAt(0).toUpperCase() + this.name.slice(1);
135+
}
136+
}

src/lib/table/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './cell';
1111
export * from './table';
1212
export * from './row';
1313
export * from './table-data-source';
14+
export * from './text-column';

src/lib/table/table-module.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
MatRow,
2727
MatRowDef
2828
} from './row';
29+
import {MatTextColumn} from './text-column';
2930
import {CommonModule} from '@angular/common';
3031
import {MatCommonModule} from '@angular/material/core';
3132

@@ -51,10 +52,16 @@ const EXPORTED_DECLARATIONS = [
5152
MatHeaderRow,
5253
MatRow,
5354
MatFooterRow,
55+
56+
MatTextColumn,
5457
];
5558

5659
@NgModule({
57-
imports: [CdkTableModule, CommonModule, MatCommonModule],
60+
imports: [
61+
CdkTableModule,
62+
CommonModule,
63+
MatCommonModule,
64+
],
5865
exports: EXPORTED_DECLARATIONS,
5966
declarations: EXPORTED_DECLARATIONS,
6067
})

0 commit comments

Comments
 (0)