Skip to content

Commit

Permalink
feat(sort): add the ability to disable sort toggling
Browse files Browse the repository at this point in the history
Adds the ability for a `mat-sort-header` or `mat-sort` instance to be disabled, preventing the user from changing the sorting direction.

Fixes angular#8622.
  • Loading branch information
crisbeto committed Nov 27, 2017
1 parent 6e865b7 commit 0c03032
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 16 deletions.
11 changes: 10 additions & 1 deletion src/demo-app/table/table-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,13 @@ <h3>MatTable With MatTableDataSource Example</h3>

<table-header-demo (shiftColumns)="displayedColumns.push(displayedColumns.shift())"
(toggleColorColumn)="toggleColorColumn()" *ngIf="selection.isEmpty()">

<button mat-menu-item (click)="progressSortingDisabled = !progressSortingDisabled">
<mat-icon>sort</mat-icon>
Toggle Progress Sorting
</button>
</table-header-demo>

<div class="example-header example-selection-header"
*ngIf="!selection.isEmpty()">
{{selection.selected.length}}
Expand Down Expand Up @@ -257,7 +263,10 @@ <h3>MatTable With MatTableDataSource Example</h3>

<!-- Column Definition: Progress -->
<ng-container matColumnDef="progress">
<mat-header-cell *matHeaderCellDef mat-sort-header> Progress </mat-header-cell>
<mat-header-cell
*matHeaderCellDef
[disabled]="progressSortingDisabled"
mat-sort-header> Progress </mat-header-cell>
<mat-cell *matCellDef="let row">
<div class="demo-progress-stat">{{row.progress}}%</div>
<div class="demo-progress-indicator-container">
Expand Down
1 change: 1 addition & 0 deletions src/demo-app/table/table-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class TableDemo {
displayedColumns: UserProperties[] = [];
trackByStrategy: TrackByStrategy = 'reference';
changeReferences = false;
progressSortingDisabled = false;
highlights = new Set<string>();
wasExpanded = new Set<UserData>();

Expand Down
1 change: 1 addition & 0 deletions src/demo-app/table/table-header-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
<mat-icon>color_lens</mat-icon>
Toggle Color Column
</button>
<ng-content></ng-content>
</mat-menu>
</div>
3 changes: 2 additions & 1 deletion src/lib/sort/sort-header.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<div class="mat-sort-header-container"
[class.mat-sort-header-position-before]="arrowPosition == 'before'">
<button class="mat-sort-header-button" type="button"
[attr.aria-label]="_intl.sortButtonLabel(id)">
[attr.aria-label]="_intl.sortButtonLabel(id)"
[attr.disabled]="_isDisabled() || null">
<ng-content></ng-content>
</button>

Expand Down
6 changes: 5 additions & 1 deletion src/lib/sort/sort-header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ $mat-sort-header-arrow-transition: 225ms cubic-bezier(0.4, 0, 0.2, 1);
.mat-sort-header-container {
display: flex;
cursor: pointer;

.mat-sort-header-disabled & {
cursor: default;
}
}

.mat-sort-header-position-before {
Expand All @@ -20,7 +24,7 @@ $mat-sort-header-arrow-transition: 225ms cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
padding: 0;
cursor: pointer;
cursor: inherit;
outline: 0;
font: inherit;
color: currentColor;
Expand Down
33 changes: 28 additions & 5 deletions src/lib/sort/sort-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ import {MatSort, MatSortable} from './sort';
import {MatSortHeaderIntl} from './sort-header-intl';
import {getSortHeaderNotContainedWithinSortError} from './sort-errors';
import {AnimationCurves, AnimationDurations} from '@angular/material/core';
import {CanDisable, mixinDisabled} from '@angular/material/core';


const SORT_ANIMATION_TRANSITION =
AnimationDurations.ENTERING + ' ' + AnimationCurves.STANDARD_CURVE;

// Boilerplate for applying mixins to the sort header.
/** @docs-private */
export class MatSortHeaderBase {}
export const _MatSortHeaderMixinBase = mixinDisabled(MatSortHeaderBase);


/**
* Applies sorting behavior (click to change sort) and styles to an element, including an
* arrow to display the current sort direction.
Expand All @@ -50,12 +58,14 @@ const SORT_ANIMATION_TRANSITION =
templateUrl: 'sort-header.html',
styleUrls: ['sort-header.css'],
host: {
'(click)': '_sort.sort(this)',
'(click)': '_handleClick()',
'[class.mat-sort-header-sorted]': '_isSorted()',
'[class.mat-sort-header-disabled]': '_isDisabled()',
},
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush,
inputs: ['disabled'],
animations: [
trigger('indicator', [
state('asc', style({transform: 'translateY(0px)'})),
Expand Down Expand Up @@ -93,7 +103,7 @@ const SORT_ANIMATION_TRANSITION =
])
]
})
export class MatSortHeader implements MatSortable {
export class MatSortHeader extends _MatSortHeaderMixinBase implements MatSortable, CanDisable {
private _rerenderSubscription: Subscription;

/**
Expand All @@ -118,13 +128,15 @@ export class MatSortHeader implements MatSortable {
changeDetectorRef: ChangeDetectorRef,
@Optional() public _sort: MatSort,
@Optional() public _cdkColumnDef: CdkColumnDef) {

super();

if (!_sort) {
throw getSortHeaderNotContainedWithinSortError();
}

this._rerenderSubscription = merge(_sort.sortChange, _intl.changes).subscribe(() => {
changeDetectorRef.markForCheck();
});
this._rerenderSubscription = merge(_sort.sortChange, _sort._stateChanges, _intl.changes)
.subscribe(() => changeDetectorRef.markForCheck());
}

ngOnInit() {
Expand All @@ -140,9 +152,20 @@ export class MatSortHeader implements MatSortable {
this._rerenderSubscription.unsubscribe();
}

/** Handles click events on the header. */
_handleClick() {
if (!this._isDisabled()) {
this._sort.sort(this);
}
}

/** Whether this MatSortHeader is currently sorted in either ascending or descending order. */
_isSorted() {
return this._sort.active == this.id &&
(this._sort.direction === 'asc' || this._sort.direction === 'desc');
}

_isDisabled() {
return this._sort.disabled || this.disabled;
}
}
13 changes: 9 additions & 4 deletions src/lib/sort/sort.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ direction to sort (`asc` or `desc`).
By default, a sort header starts its sorting at `asc` and then `desc`. Triggering the sort header
after `desc` will remove sorting.

To reverse the sort order for all headers, set the `matSortStart` to `desc` on the `matSort`
directive. To reverse the order only for a specific header, set the `start` input only on the header
To reverse the sort order for all headers, set the `matSortStart` to `desc` on the `matSort`
directive. To reverse the order only for a specific header, set the `start` input only on the header
instead.

To prevent the user from clearing the sort sort state from an already sorted column, set
`matSortDisableClear` to `true` on the `matSort` to affect all headers, or set `disableClear` to
To prevent the user from clearing the sort sort state from an already sorted column, set
`matSortDisableClear` to `true` on the `matSort` to affect all headers, or set `disableClear` to
`true` on a specific header.

#### Disabling sorting

If you want to prevent the user from changing the sorting order of any column, you can use the
`matSortDisabled` binding on the `mat-sort`, or the `disabled` on an single `mat-sort-header`.

#### Using sort with the mat-table

When used on an `mat-table` header, it is not required to set an `mat-sort-header` id on because
Expand Down
46 changes: 45 additions & 1 deletion src/lib/sort/sort.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,43 @@ describe('MatSort', () => {
testSingleColumnSortDirectionSequence(fixture, ['desc', 'asc', '']);
});

it('should allow for the cycling the sort direction to be disabled per column', () => {
const button = fixture.nativeElement.querySelector('#defaultSortHeaderA button');

component.sort('defaultSortHeaderA');
expect(component.matSort.direction).toBe('asc');
expect(button.getAttribute('disabled')).toBeFalsy();

component.disabledColumnSort = true;
fixture.detectChanges();

component.sort('defaultSortHeaderA');
expect(component.matSort.direction).toBe('asc');
expect(button.getAttribute('disabled')).toBe('true');
});

it('should allow for the cycling the sort direction to be disabled for all columns', () => {
const button = fixture.nativeElement.querySelector('#defaultSortHeaderA button');

component.sort('defaultSortHeaderA');
expect(component.matSort.active).toBe('defaultSortHeaderA');
expect(component.matSort.direction).toBe('asc');
expect(button.getAttribute('disabled')).toBeFalsy();

component.disableAllSort = true;
fixture.detectChanges();

component.sort('defaultSortHeaderA');
expect(component.matSort.active).toBe('defaultSortHeaderA');
expect(component.matSort.direction).toBe('asc');
expect(button.getAttribute('disabled')).toBe('true');

component.sort('defaultSortHeaderB');
expect(component.matSort.active).toBe('defaultSortHeaderA');
expect(component.matSort.direction).toBe('asc');
expect(button.getAttribute('disabled')).toBe('true');
});

it('should reset sort direction when a different column is sorted', () => {
component.sort('defaultSortHeaderA');
expect(component.matSort.active).toBe('defaultSortHeaderA');
Expand Down Expand Up @@ -211,11 +248,16 @@ function testSingleColumnSortDirectionSequence(fixture: ComponentFixture<SimpleM
template: `
<div matSort
[matSortActive]="active"
[matSortDisabled]="disableAllSort"
[matSortStart]="start"
[matSortDirection]="direction"
[matSortDisableClear]="disableClear"
(matSortChange)="latestSortEvent = $event">
<div id="defaultSortHeaderA" #defaultSortHeaderA mat-sort-header="defaultSortHeaderA">
<div
id="defaultSortHeaderA"
#defaultSortHeaderA
mat-sort-header="defaultSortHeaderA"
[disabled]="disabledColumnSort">
A
</div>
<div id="defaultSortHeaderB" #defaultSortHeaderB mat-sort-header="defaultSortHeaderB">
Expand All @@ -233,6 +275,8 @@ class SimpleMatSortApp {
start: SortDirection = 'asc';
direction: SortDirection = '';
disableClear: boolean;
disabledColumnSort = false;
disableAllSort = false;

@ViewChild(MatSort) matSort: MatSort;
@ViewChild('defaultSortHeaderA') matSortHeaderDefaultA: MatSortHeader;
Expand Down
33 changes: 30 additions & 3 deletions src/lib/sort/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, EventEmitter, Input, isDevMode, Output} from '@angular/core';
import {
Directive,
EventEmitter,
Input,
isDevMode,
Output,
OnChanges,
OnDestroy,
} from '@angular/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {CanDisable, mixinDisabled} from '@angular/material/core';
import {SortDirection} from './sort-direction';
import {
getSortInvalidDirectionError,
getSortDuplicateSortableIdError,
getSortHeaderMissingIdError
} from './sort-errors';
import {Subject} from 'rxjs/Subject';

/** Interface for a directive that holds sorting state consumed by `MatSortHeader`. */
export interface MatSortable {
Expand All @@ -36,15 +46,24 @@ export interface Sort {
direction: SortDirection;
}

// Boilerplate for applying mixins to MatSort.
/** @docs-private */
export class MatSortBase {}
export const _MatSortMixinBase = mixinDisabled(MatSortBase);

/** Container for MatSortables to manage the sort state and provide default sort parameters. */
@Directive({
selector: '[matSort]',
exportAs: 'matSort'
exportAs: 'matSort',
inputs: ['disabled: matSortDisabled']
})
export class MatSort {
export class MatSort extends _MatSortMixinBase implements CanDisable, OnChanges, OnDestroy {
/** Collection of all registered sortables that this directive manages. */
sortables = new Map<string, MatSortable>();

/** Used to notify any child components listening to state changes. */
_stateChanges = new Subject<void>();

/** The id of the most recently sorted MatSortable. */
@Input('matSortActive') active: string;

Expand Down Expand Up @@ -125,6 +144,14 @@ export class MatSort {
if (nextDirectionIndex >= sortDirectionCycle.length) { nextDirectionIndex = 0; }
return sortDirectionCycle[nextDirectionIndex];
}

ngOnChanges() {
this._stateChanges.next();
}

ngOnDestroy() {
this._stateChanges.complete();
}
}

/** Returns the sort direction cycle to use given the provided parameters of order and clear. */
Expand Down

0 comments on commit 0c03032

Please sign in to comment.