Skip to content

Commit ce69439

Browse files
committed
feat(sort): add multi-sort support
Adds multi-column sorting capability to MatSort, allowing to sort a table based on multiple columns at once by toggling matSortMultiple. This feature adds a new sortState variable inside MatSort that should be used as a source of truth when matSortMultiple is enabled. It also adds a two helper methods to check sort state: isActive, which returns if the provided column ID is currently sorted, and getCurrentSortDirection that returns the SortDirection of the provided column ID. Fixes #24102
1 parent 57d9a2f commit ce69439

File tree

9 files changed

+371
-90
lines changed

9 files changed

+371
-90
lines changed

Diff for: src/components-examples/material/table/table-sorting/table-sorting-example.html

+34-20
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,50 @@
1+
<mat-button-toggle-group [(ngModel)]="multiSortEnabled">
2+
<mat-button-toggle [value]="false">Single-sorting</mat-button-toggle>
3+
<mat-button-toggle [value]="true">Multi-sorting</mat-button-toggle>
4+
</mat-button-toggle-group>
5+
16
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="announceSortChange($event)"
7+
[matSortMultiple]="multiSortEnabled"
28
class="mat-elevation-z8">
39

4-
<!-- Position Column -->
5-
<ng-container matColumnDef="position">
6-
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by number">
7-
No.
10+
<!-- First name Column -->
11+
<ng-container matColumnDef="firstName">
12+
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by first name">
13+
First name
814
</th>
9-
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
15+
<td mat-cell *matCellDef="let element"> {{element.firstName}} </td>
16+
</ng-container>
17+
18+
<!-- Last name Column -->
19+
<ng-container matColumnDef="lastName">
20+
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by last name">
21+
Last name
22+
</th>
23+
<td mat-cell *matCellDef="let element"> {{element.lastName}} </td>
1024
</ng-container>
1125

12-
<!-- Name Column -->
13-
<ng-container matColumnDef="name">
14-
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by name">
15-
Name
26+
<!-- Position Column -->
27+
<ng-container matColumnDef="position">
28+
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by position">
29+
Position
1630
</th>
17-
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
31+
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
1832
</ng-container>
1933

20-
<!-- Weight Column -->
21-
<ng-container matColumnDef="weight">
22-
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by weight">
23-
Weight
34+
<!-- Office Column -->
35+
<ng-container matColumnDef="office">
36+
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by office">
37+
Office
2438
</th>
25-
<td mat-cell *matCellDef="let element"> {{element.weight}} </td>
39+
<td mat-cell *matCellDef="let element"> {{element.office}} </td>
2640
</ng-container>
2741

28-
<!-- Symbol Column -->
29-
<ng-container matColumnDef="symbol">
30-
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by symbol">
31-
Symbol
42+
<!-- Salary Column -->
43+
<ng-container matColumnDef="salary">
44+
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by salary">
45+
Salary
3246
</th>
33-
<td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
47+
<td mat-cell *matCellDef="let element"> {{element.salary}} </td>
3448
</ng-container>
3549

3650
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>

Diff for: src/components-examples/material/table/table-sorting/table-sorting-example.ts

+101-19
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,106 @@
11
import {LiveAnnouncer} from '@angular/cdk/a11y';
22
import {AfterViewInit, Component, ViewChild, inject} from '@angular/core';
3+
import {FormsModule} from '@angular/forms';
4+
import {MatButtonToggleModule} from '@angular/material/button-toggle';
35
import {MatSort, Sort, MatSortModule} from '@angular/material/sort';
46
import {MatTableDataSource, MatTableModule} from '@angular/material/table';
57

6-
export interface PeriodicElement {
7-
name: string;
8-
position: number;
9-
weight: number;
10-
symbol: string;
8+
export interface EmployeeData {
9+
firstName: string;
10+
lastName: string;
11+
position: string;
12+
office: string;
13+
salary: number;
1114
}
12-
const ELEMENT_DATA: PeriodicElement[] = [
13-
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
14-
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
15-
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
16-
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
17-
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
18-
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
19-
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
20-
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
21-
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
22-
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
15+
16+
const EMPLOYEE_DATA: EmployeeData[] = [
17+
{
18+
firstName: 'Garrett',
19+
lastName: 'Winters',
20+
position: 'Accountant',
21+
office: 'Tokyo',
22+
salary: 170750,
23+
},
24+
{firstName: 'Airi', lastName: 'Satou', position: 'Accountant', office: 'Tokyo', salary: 162700},
25+
{
26+
firstName: 'Donna',
27+
lastName: 'Snider',
28+
position: 'Customer Support',
29+
office: 'New York',
30+
salary: 112000,
31+
},
32+
{
33+
firstName: 'Serge',
34+
lastName: 'Baldwin',
35+
position: 'Data Coordinator',
36+
office: 'Singapore',
37+
salary: 138575,
38+
},
39+
{firstName: 'Thor', lastName: 'Walton', position: 'Developer', office: 'New York', salary: 98540},
40+
{
41+
firstName: 'Gavin',
42+
lastName: 'Joyce',
43+
position: 'Developer',
44+
office: 'Edinburgh',
45+
salary: 92575,
46+
},
47+
{firstName: 'Suki', lastName: 'Burks', position: 'Developer', office: 'London', salary: 114500},
48+
{
49+
firstName: 'Jonas',
50+
lastName: 'Alexander',
51+
position: 'Developer',
52+
office: 'San Francisco',
53+
salary: 86500,
54+
},
55+
{
56+
firstName: 'Jackson',
57+
lastName: 'Bradshaw',
58+
position: 'Director',
59+
office: 'New York',
60+
salary: 645750,
61+
},
62+
{
63+
firstName: 'Brielle',
64+
lastName: 'Williamson',
65+
position: 'Integration Specialist',
66+
office: 'New York',
67+
salary: 372000,
68+
},
69+
{
70+
firstName: 'Michelle',
71+
lastName: 'House',
72+
position: 'Integration Specialist',
73+
office: 'Sydney',
74+
salary: 95400,
75+
},
76+
{
77+
firstName: 'Michael',
78+
lastName: 'Bruce',
79+
position: 'Javascript Developer',
80+
office: 'Singapore',
81+
salary: 183000,
82+
},
83+
{
84+
firstName: 'Ashton',
85+
lastName: 'Cox',
86+
position: 'Junior Technical Author',
87+
office: 'San Francisco',
88+
salary: 86000,
89+
},
90+
{
91+
firstName: 'Michael',
92+
lastName: 'Silva',
93+
position: 'Marketing Designer',
94+
office: 'London',
95+
salary: 198500,
96+
},
97+
{
98+
firstName: 'Timothy',
99+
lastName: 'Mooney',
100+
position: 'Office Manager',
101+
office: 'London',
102+
salary: 136200,
103+
},
23104
];
24105
/**
25106
* @title Table with sorting
@@ -28,13 +109,14 @@ const ELEMENT_DATA: PeriodicElement[] = [
28109
selector: 'table-sorting-example',
29110
styleUrl: 'table-sorting-example.css',
30111
templateUrl: 'table-sorting-example.html',
31-
imports: [MatTableModule, MatSortModule],
112+
imports: [MatTableModule, MatSortModule, FormsModule, MatButtonToggleModule],
32113
})
33114
export class TableSortingExample implements AfterViewInit {
34115
private _liveAnnouncer = inject(LiveAnnouncer);
35116

36-
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
37-
dataSource = new MatTableDataSource(ELEMENT_DATA);
117+
multiSortEnabled = false;
118+
displayedColumns: string[] = ['firstName', 'lastName', 'position', 'office', 'salary'];
119+
dataSource = new MatTableDataSource(EMPLOYEE_DATA);
38120

39121
@ViewChild(MatSort) sort: MatSort;
40122

Diff for: src/material/sort/sort-header.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,11 @@ export class MatSortHeader implements MatSortable, OnDestroy, OnInit, AfterViewI
295295

296296
/** Whether this MatSortHeader is currently sorted in either ascending or descending order. */
297297
_isSorted() {
298+
const currentSortDirection = this._sort.getCurrentSortDirection(this.id);
299+
298300
return (
299-
this._sort.active == this.id &&
300-
(this._sort.direction === 'asc' || this._sort.direction === 'desc')
301+
this._sort.isActive(this.id) &&
302+
(currentSortDirection === 'asc' || currentSortDirection === 'desc')
301303
);
302304
}
303305

@@ -323,7 +325,9 @@ export class MatSortHeader implements MatSortable, OnDestroy, OnInit, AfterViewI
323325
* only be changed once the arrow displays again (hint or activation).
324326
*/
325327
_updateArrowDirection() {
326-
this._arrowDirection = this._isSorted() ? this._sort.direction : this.start || this._sort.start;
328+
this._arrowDirection = this._isSorted()
329+
? this._sort.getCurrentSortDirection(this.id)
330+
: this.start || this._sort.start;
327331
}
328332

329333
_isDisabled() {
@@ -341,7 +345,7 @@ export class MatSortHeader implements MatSortable, OnDestroy, OnInit, AfterViewI
341345
return 'none';
342346
}
343347

344-
return this._sort.direction == 'asc' ? 'ascending' : 'descending';
348+
return this._sort.getCurrentSortDirection(this.id) == 'asc' ? 'ascending' : 'descending';
345349
}
346350

347351
/** Whether the arrow inside the sort header should be rendered. */

Diff for: src/material/sort/sort.md

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ To prevent the user from clearing the sort state from an already sorted column,
2727
`matSortDisableClear` to `true` on the `matSort` to affect all headers, or set `disableClear` to
2828
`true` on a specific header.
2929

30+
#### Enabling multi-sort
31+
32+
By default the sorting behavior only accepts sorting by a single column. In order to change that and have multi-column sorting, set the `matSortMultiple` on the `matSort` directive.
33+
34+
When using multi-sorting, there's no changes to the `matSortChange` events to avoid breaking backwards compatibility. If you need to get the current sortState containing all sorted columns, you need to access the `matTable.sortState` field directly.
35+
36+
> Notice that the order on which the columns are sorted does matter.
37+
3038
#### Disabling sorting
3139

3240
If you want to prevent the user from changing the sorting order of any column, you can use the

Diff for: src/material/sort/sort.spec.ts

+20
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ describe('MatSort', () => {
5757
fixture = TestBed.createComponent(SimpleMatSortApp);
5858
component = fixture.componentInstance;
5959
fixture.detectChanges();
60+
61+
component.matSort.matSortMultiple = false;
62+
component.matSort.sortState.clear();
6063
});
6164

6265
it('should have the sort headers register and deregister themselves', () => {
@@ -456,6 +459,23 @@ describe('MatSort', () => {
456459
expect(descriptionElement?.textContent).toBe('Sort 2nd column');
457460
});
458461

462+
it('should be able to store sorting for multiple columns when using multisort', () => {
463+
component.matSort.matSortMultiple = true;
464+
component.start = 'asc';
465+
testSingleColumnSortDirectionSequence(fixture, ['asc', 'desc', ''], 'defaultA');
466+
testSingleColumnSortDirectionSequence(fixture, ['asc', 'desc', ''], 'defaultB');
467+
468+
expect(component.matSort.sortState.size).toBe(2);
469+
470+
const defaultAState = component.matSort.sortState.get('defaultA');
471+
expect(defaultAState).toBeTruthy();
472+
expect(defaultAState?.direction).toBe(component.start);
473+
474+
const defaultBState = component.matSort.sortState.get('defaultB');
475+
expect(defaultBState).toBeTruthy();
476+
expect(defaultBState?.direction).toBe(component.start);
477+
});
478+
459479
it('should render arrows after sort header by default', () => {
460480
const matSortWithArrowPositionFixture = TestBed.createComponent(MatSortWithArrowPosition);
461481

0 commit comments

Comments
 (0)