Skip to content

Commit 1b88056

Browse files
committed
fix(table): data source should sort empty values correctly
1 parent d6fec35 commit 1b88056

File tree

3 files changed

+83
-23
lines changed

3 files changed

+83
-23
lines changed

src/cdk/coercion/number-property.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
export function coerceNumberProperty(value: any): number;
1111
export function coerceNumberProperty<D>(value: any, fallback: D): number | D;
1212
export function coerceNumberProperty(value: any, fallbackValue = 0) {
13+
return isNumberValue(value) ? Number(value) : fallbackValue;
14+
}
15+
16+
/** Whether the provided value is considered a number. */
17+
export function isNumberValue(value: any): boolean {
1318
// parseFloat(value) handles most of the cases we're interested in (it treats null, empty string,
1419
// and other non-number values as NaN, where Number just uses 0) but it considers the string
1520
// '123hello' to be a valid number. Therefore we also check if Number(value) is NaN.
16-
return isNaN(parseFloat(value as any)) || isNaN(Number(value)) ? fallbackValue : Number(value);
21+
return !(isNaN(parseFloat(value as any)) || isNaN(Number(value)));
1722
}

src/lib/table/table-data-source.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {combineLatest} from 'rxjs/operators/combineLatest';
1515
import {map} from 'rxjs/operators/map';
1616
import {startWith} from 'rxjs/operators/startWith';
1717
import {empty} from 'rxjs/observable/empty';
18+
import {isNumberValue} from '@angular/cdk/coercion';
1819

1920
/**
2021
* Data source that accepts a client-side data array and includes native support of filtering,
@@ -88,7 +89,8 @@ export class MatTableDataSource<T> implements DataSource<T> {
8889
private _paginator: MatPaginator|null;
8990

9091
/**
91-
* Data accessor function that is used for accessing data properties for sorting.
92+
* Data accessor function that is used for accessing data properties for sorting through
93+
* the default sortPredicate.
9294
* This default function assumes that the sort header IDs (which defaults to the column name)
9395
* matches the data's properties (e.g. column Xyz represents data['Xyz']).
9496
* May be set to a custom function for different behavior.
@@ -97,22 +99,46 @@ export class MatTableDataSource<T> implements DataSource<T> {
9799
*/
98100
sortingDataAccessor: ((data: T, sortHeaderId: string) => string|number) =
99101
(data: T, sortHeaderId: string): string|number => {
100-
const value: any = data[sortHeaderId];
102+
let value: any = data[sortHeaderId];
103+
return isNumberValue(value) ? Number(value) : value;
104+
}
101105

102-
// If the value is a string and only whitespace, return the value.
103-
// Otherwise +value will convert it to 0.
104-
if (typeof value === 'string' && !value.trim()) {
105-
return value;
106-
}
106+
/**
107+
* Returns a sorted array of the provided data according to the state of the MatSort. Called
108+
* after changes are made to the filtered data or when sort changes are emitted from MatSort.
109+
* By default, the function retrieves the active sort and its direction and compares data
110+
* by retrieving data using the sortingDataAccessor. May be overridden for a custom implementation
111+
* of data ordering.
112+
* @param data The array of data that should be sorted.
113+
* @param sort The connected MatSort that holds the current sort state.
114+
*/
115+
sortPredicate: ((data: T[], sort: MatSort) => T[]) = (data: T[], sort: MatSort): T[] => {
116+
const active = sort.active;
117+
const direction = sort.direction;
118+
if (!active || direction == '') { return data; }
119+
120+
return data.sort((a, b) => {
121+
let valueA = this.sortingDataAccessor(a, active);
122+
let valueB = this.sortingDataAccessor(b, active);
107123

108-
return isNaN(+value) ? value : +value;
124+
let comparatorResult = 0;
125+
if (valueA && valueB) {
126+
comparatorResult = valueA < valueB ? -1 : 1;
127+
} else if (valueA) {
128+
comparatorResult = 1;
129+
} else if (valueB) {
130+
comparatorResult = -1;
131+
}
132+
133+
return comparatorResult * (direction == 'asc' ? 1 : -1);
134+
});
109135
}
110136

111137
/**
112138
* Checks if a data object matches the data source's filter string. By default, each data object
113139
* is converted to a string of its properties and returns true if the filter has
114140
* at least one occurrence in that string. By default, the filter string has its whitespace
115-
* trimmed and the match is case-insensitive. May be overriden for a custom implementation of
141+
* trimmed and the match is case-insensitive. May be overridden for a custom implementation of
116142
* filter matching.
117143
* @param data Data object used to check against the filter.
118144
* @param filter Filter string that has been set on the data source.
@@ -172,7 +198,7 @@ export class MatTableDataSource<T> implements DataSource<T> {
172198
_filterData(data: T[]) {
173199
// If there is a filter string, filter out data that does not contain it.
174200
// Each data object is converted to a string using the function defined by filterTermAccessor.
175-
// May be overriden for customization.
201+
// May be overridden for customization.
176202
this.filteredData =
177203
!this.filter ? data : data.filter(obj => this.filterPredicate(obj, this.filter));
178204

@@ -188,16 +214,9 @@ export class MatTableDataSource<T> implements DataSource<T> {
188214
*/
189215
_orderData(data: T[]): T[] {
190216
// If there is no active sort or direction, return the data without trying to sort.
191-
if (!this.sort || !this.sort.active || this.sort.direction == '') { return data; }
192-
193-
const active = this.sort.active;
194-
const direction = this.sort.direction;
217+
if (!this.sort) { return data; }
195218

196-
return data.slice().sort((a, b) => {
197-
let valueA = this.sortingDataAccessor(a, active);
198-
let valueB = this.sortingDataAccessor(b, active);
199-
return (valueA < valueB ? -1 : 1) * (direction == 'asc' ? 1 : -1);
200-
});
219+
return this.sortPredicate(data.slice(), this.sort);
201220
}
202221

203222
/**

src/lib/table/table.spec.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,42 @@ describe('MatTable', () => {
209209
['a_2', 'b_2', 'c_2'],
210210
['a_3', 'b_3', 'c_3'],
211211
]);
212+
213+
// Expect that empty string row comes before the other values
214+
component.sort.sort(component.sortHeader);
215+
fixture.detectChanges();
216+
expectTableToMatchContent(tableElement, [
217+
['Column A\xa0Sorted by a descending', 'Column B', 'Column C'],
218+
['a_3', 'b_3', 'c_3'],
219+
['a_2', 'b_2', 'c_2'],
220+
['', 'b_1', 'c_1'],
221+
]);
222+
});
223+
224+
it('should by default correctly sort undefined values', () => {
225+
// Activate column A sort
226+
dataSource.data[0].a = undefined;
227+
228+
// Expect that undefined row comes before the other values
229+
component.sort.sort(component.sortHeader);
230+
fixture.detectChanges();
231+
expectTableToMatchContent(tableElement, [
232+
['Column A\xa0Sorted by a ascending', 'Column B', 'Column C'],
233+
['', 'b_1', 'c_1'],
234+
['a_2', 'b_2', 'c_2'],
235+
['a_3', 'b_3', 'c_3'],
236+
]);
237+
238+
239+
// Expect that undefined row comes after the other values
240+
component.sort.sort(component.sortHeader);
241+
fixture.detectChanges();
242+
expectTableToMatchContent(tableElement, [
243+
['Column A\xa0Sorted by a descending', 'Column B', 'Column C'],
244+
['a_3', 'b_3', 'c_3'],
245+
['a_2', 'b_2', 'c_2'],
246+
['', 'b_1', 'c_1'],
247+
]);
212248
});
213249

214250
it('should be able to page the table contents', fakeAsync(() => {
@@ -243,9 +279,9 @@ describe('MatTable', () => {
243279
});
244280

245281
interface TestData {
246-
a: string;
247-
b: string;
248-
c: string;
282+
a: string|undefined;
283+
b: string|undefined;
284+
c: string|undefined;
249285
}
250286

251287
class FakeDataSource extends DataSource<TestData> {

0 commit comments

Comments
 (0)