Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

‎src/components-examples/material/table/table-sorting/table-sorting-example.html‎

Lines changed: 34 additions & 20 deletions
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>

‎src/components-examples/material/table/table-sorting/table-sorting-example.ts‎

Lines changed: 101 additions & 19 deletions
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

‎src/material/sort/sort-header.ts‎

Lines changed: 8 additions & 4 deletions
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. */

‎src/material/sort/sort.md‎

Lines changed: 8 additions & 0 deletions
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

‎src/material/sort/sort.spec.ts‎

Lines changed: 20 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /