Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components/sort): add multi-sort support #28458

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
<mat-button-toggle-group [(ngModel)]="multiSortEnabled">
<mat-button-toggle [value]="false">Single-sorting</mat-button-toggle>
<mat-button-toggle [value]="true">Multi-sorting</mat-button-toggle>
</mat-button-toggle-group>

<table mat-table [dataSource]="dataSource" matSort (matSortChange)="announceSortChange($event)"
[matSortMultiple]="multiSortEnabled"
class="mat-elevation-z8">

<!-- Position Column -->
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by number">
No.
<!-- First name Column -->
<ng-container matColumnDef="firstName">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by first name">
First name
</th>
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
<td mat-cell *matCellDef="let element"> {{element.firstName}} </td>
</ng-container>

<!-- Last name Column -->
<ng-container matColumnDef="lastName">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by last name">
Last name
</th>
<td mat-cell *matCellDef="let element"> {{element.lastName}} </td>
</ng-container>

<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by name">
Name
<!-- Position Column -->
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by position">
Position
</th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
</ng-container>

<!-- Weight Column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by weight">
Weight
<!-- Office Column -->
<ng-container matColumnDef="office">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by office">
Office
</th>
<td mat-cell *matCellDef="let element"> {{element.weight}} </td>
<td mat-cell *matCellDef="let element"> {{element.office}} </td>
</ng-container>

<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by symbol">
Symbol
<!-- Salary Column -->
<ng-container matColumnDef="salary">
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by salary">
Salary
</th>
<td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
<td mat-cell *matCellDef="let element"> {{element.salary}} </td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,106 @@
import {LiveAnnouncer} from '@angular/cdk/a11y';
import {AfterViewInit, Component, ViewChild, inject} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatSort, Sort, MatSortModule} from '@angular/material/sort';
import {MatTableDataSource, MatTableModule} from '@angular/material/table';

export interface PeriodicElement {
name: string;
position: number;
weight: number;
symbol: string;
export interface EmployeeData {
firstName: string;
lastName: string;
position: string;
office: string;
salary: number;
}
const ELEMENT_DATA: PeriodicElement[] = [
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},

const EMPLOYEE_DATA: EmployeeData[] = [
{
firstName: 'Garrett',
lastName: 'Winters',
position: 'Accountant',
office: 'Tokyo',
salary: 170750,
},
{firstName: 'Airi', lastName: 'Satou', position: 'Accountant', office: 'Tokyo', salary: 162700},
{
firstName: 'Donna',
lastName: 'Snider',
position: 'Customer Support',
office: 'New York',
salary: 112000,
},
{
firstName: 'Serge',
lastName: 'Baldwin',
position: 'Data Coordinator',
office: 'Singapore',
salary: 138575,
},
{firstName: 'Thor', lastName: 'Walton', position: 'Developer', office: 'New York', salary: 98540},
{
firstName: 'Gavin',
lastName: 'Joyce',
position: 'Developer',
office: 'Edinburgh',
salary: 92575,
},
{firstName: 'Suki', lastName: 'Burks', position: 'Developer', office: 'London', salary: 114500},
{
firstName: 'Jonas',
lastName: 'Alexander',
position: 'Developer',
office: 'San Francisco',
salary: 86500,
},
{
firstName: 'Jackson',
lastName: 'Bradshaw',
position: 'Director',
office: 'New York',
salary: 645750,
},
{
firstName: 'Brielle',
lastName: 'Williamson',
position: 'Integration Specialist',
office: 'New York',
salary: 372000,
},
{
firstName: 'Michelle',
lastName: 'House',
position: 'Integration Specialist',
office: 'Sydney',
salary: 95400,
},
{
firstName: 'Michael',
lastName: 'Bruce',
position: 'Javascript Developer',
office: 'Singapore',
salary: 183000,
},
{
firstName: 'Ashton',
lastName: 'Cox',
position: 'Junior Technical Author',
office: 'San Francisco',
salary: 86000,
},
{
firstName: 'Michael',
lastName: 'Silva',
position: 'Marketing Designer',
office: 'London',
salary: 198500,
},
{
firstName: 'Timothy',
lastName: 'Mooney',
position: 'Office Manager',
office: 'London',
salary: 136200,
},
];
/**
* @title Table with sorting
Expand All @@ -28,13 +109,14 @@ const ELEMENT_DATA: PeriodicElement[] = [
selector: 'table-sorting-example',
styleUrl: 'table-sorting-example.css',
templateUrl: 'table-sorting-example.html',
imports: [MatTableModule, MatSortModule],
imports: [MatTableModule, MatSortModule, FormsModule, MatButtonToggleModule],
})
export class TableSortingExample implements AfterViewInit {
private _liveAnnouncer = inject(LiveAnnouncer);

displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
dataSource = new MatTableDataSource(ELEMENT_DATA);
multiSortEnabled = false;
displayedColumns: string[] = ['firstName', 'lastName', 'position', 'office', 'salary'];
dataSource = new MatTableDataSource(EMPLOYEE_DATA);

@ViewChild(MatSort) sort: MatSort;

Expand Down
35 changes: 32 additions & 3 deletions src/material/sort/sort-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,41 @@ export class MatSortHeader implements MatSortable, OnDestroy, OnInit, AfterViewI

/** Whether this MatSortHeader is currently sorted in either ascending or descending order. */
_isSorted() {
const currentSortDirection = this._sort.getCurrentSortDirection(this.id);

return (
this._sort.active == this.id &&
(this._sort.direction === 'asc' || this._sort.direction === 'desc')
this._sort.isActive(this.id) &&
(currentSortDirection === 'asc' || currentSortDirection === 'desc')
);
}

/** Returns the animation state for the arrow direction (indicator and pointers). */
_getArrowDirectionState() {
return `${this._isSorted() ? 'active-' : ''}${this._arrowDirection}`;
}

/** Returns the arrow position state (opacity, translation). */
_getArrowViewState() {
const fromState = this._viewState.fromState;
return (fromState ? `${fromState}-to-` : '') + this._viewState.toState;
}

/**
* Updates the direction the arrow should be pointing. If it is not sorted, the arrow should be
* facing the start direction. Otherwise if it is sorted, the arrow should point in the currently
* active sorted direction. The reason this is updated through a function is because the direction
* should only be changed at specific times - when deactivated but the hint is displayed and when
* the sort is active and the direction changes. Otherwise the arrow's direction should linger
* in cases such as the sort becoming deactivated but we want to animate the arrow away while
* preserving its direction, even though the next sort direction is actually different and should
* only be changed once the arrow displays again (hint or activation).
*/
_updateArrowDirection() {
this._arrowDirection = this._isSorted()
? this._sort.getCurrentSortDirection(this.id)
: this.start || this._sort.start;
}

_isDisabled() {
return this._sort.disabled || this.disabled;
}
Expand All @@ -244,7 +273,7 @@ export class MatSortHeader implements MatSortable, OnDestroy, OnInit, AfterViewI
return 'none';
}

return this._sort.direction == 'asc' ? 'ascending' : 'descending';
return this._sort.getCurrentSortDirection(this.id) == 'asc' ? 'ascending' : 'descending';
}

/** Whether the arrow inside the sort header should be rendered. */
Expand Down
8 changes: 8 additions & 0 deletions src/material/sort/sort.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ To prevent the user from clearing the sort state from an already sorted column,
`matSortDisableClear` to `true` on the `matSort` to affect all headers, or set `disableClear` to
`true` on a specific header.

#### Enabling multi-sort

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.

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.

> Notice that the order on which the columns are sorted does matter.

#### Disabling sorting

If you want to prevent the user from changing the sorting order of any column, you can use the
Expand Down
20 changes: 20 additions & 0 deletions src/material/sort/sort.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ describe('MatSort', () => {
fixture = TestBed.createComponent(SimpleMatSortApp);
component = fixture.componentInstance;
fixture.detectChanges();

component.matSort.matSortMultiple = false;
component.matSort.sortState.clear();
});

it('should have the sort headers register and deregister themselves', () => {
Expand Down Expand Up @@ -293,6 +296,23 @@ describe('MatSort', () => {
expect(descriptionElement?.textContent).toBe('Sort 2nd column');
});

it('should be able to store sorting for multiple columns when using multisort', () => {
component.matSort.matSortMultiple = true;
component.start = 'asc';
testSingleColumnSortDirectionSequence(fixture, ['asc', 'desc', ''], 'defaultA');
testSingleColumnSortDirectionSequence(fixture, ['asc', 'desc', ''], 'defaultB');

expect(component.matSort.sortState.size).toBe(2);

const defaultAState = component.matSort.sortState.get('defaultA');
expect(defaultAState).toBeTruthy();
expect(defaultAState?.direction).toBe(component.start);

const defaultBState = component.matSort.sortState.get('defaultB');
expect(defaultBState).toBeTruthy();
expect(defaultBState?.direction).toBe(component.start);
});

it('should render arrows after sort header by default', () => {
const matSortWithArrowPositionFixture = TestBed.createComponent(MatSortWithArrowPosition);

Expand Down
Loading
Loading