-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(cdk-experimental/selection): add selection state to a list of items #18424
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
006e1e3
Add CdkSelection and examples
yifange 8b279b5
Remove the test build rules
yifange 1926ee2
Address jelbourn's review comments
yifange 2fefe74
Address review comments #2
yifange 9794c61
fix check-rollup-globals
yifange bea5c34
add `CODEOWNERS` entries for `cdk-experimental/selection`
yifange 6f6cc11
fix tslint
yifange File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package(default_visibility = ["//visibility:public"]) | ||
|
||
load("//tools:defaults.bzl", "ng_module") | ||
|
||
ng_module( | ||
name = "selection", | ||
srcs = glob( | ||
["**/*.ts"], | ||
exclude = ["**/*.spec.ts"], | ||
), | ||
module_name = "@angular/cdk-experimental/selection", | ||
deps = [ | ||
"//src/cdk/coercion", | ||
"//src/cdk/collections", | ||
"//src/cdk/table", | ||
"@npm//@angular/core", | ||
"@npm//@angular/forms", | ||
"@npm//rxjs", | ||
], | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
export * from './public-api'; | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
export * from './selection'; | ||
export * from './select-all'; | ||
export * from './selection-toggle'; | ||
export * from './selection-column'; | ||
export * from './row-selection'; | ||
export * from './selection-set'; | ||
export * from './selection-module'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion'; | ||
import {Directive, Input} from '@angular/core'; | ||
|
||
import {CdkSelection} from './selection'; | ||
|
||
/** | ||
* Applies `cdk-selected` class and `aria-selected` to an element. | ||
* | ||
* Must be used within a parent `CdkSelection` directive. | ||
* Must be provided with the value. The index is required if `trackBy` is used on the `CdkSelection` | ||
* directive. | ||
*/ | ||
@Directive({ | ||
selector: '[cdkRowSelection]', | ||
host: { | ||
'[class.cdk-selected]': '_selection.isSelected(this.value, this.index)', | ||
'[attr.aria-selected]': '_selection.isSelected(this.value, this.index)', | ||
}, | ||
}) | ||
export class CdkRowSelection<T> { | ||
@Input('cdkRowSelectionValue') value: T; | ||
|
||
@Input('cdkRowSelectionIndex') | ||
get index(): number|undefined { return this._index; } | ||
set index(index: number|undefined) { this._index = coerceNumberProperty(index); } | ||
private _index?: number; | ||
|
||
constructor(readonly _selection: CdkSelection<T>) {} | ||
|
||
static ngAcceptInputType_index: NumberInput; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Directive, Inject, isDevMode, OnDestroy, OnInit, Optional, Self} from '@angular/core'; | ||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; | ||
import {Observable, of as observableOf, Subject} from 'rxjs'; | ||
import {switchMap, takeUntil} from 'rxjs/operators'; | ||
|
||
import {CdkSelection} from './selection'; | ||
|
||
/** | ||
* Makes the element a select-all toggle. | ||
* | ||
* Must be used within a parent `CdkSelection` directive. It toggles the selection states | ||
* of all the selection toggles connected with the `CdkSelection` directive. | ||
* If the element implements `ControlValueAccessor`, e.g. `MatCheckbox`, the directive | ||
* automatically connects it with the select-all state provided by the `CdkSelection` directive. If | ||
* not, use `checked$` to get the checked state, `indeterminate$` to get the indeterminate state, | ||
* and `toggle()` to change the selection state. | ||
*/ | ||
@Directive({ | ||
selector: '[cdkSelectAll]', | ||
exportAs: 'cdkSelectAll', | ||
}) | ||
export class CdkSelectAll<T> implements OnDestroy, OnInit { | ||
/** | ||
* The checked state of the toggle. | ||
* Resolves to `true` if all the values are selected, `false` if no value is selected. | ||
*/ | ||
readonly checked: Observable<boolean> = this._selection.change.pipe( | ||
switchMap(() => observableOf(this._selection.isAllSelected())), | ||
); | ||
|
||
/** | ||
* The indeterminate state of the toggle. | ||
* Resolves to `true` if part (not all) of the values are selected, `false` if all values or no | ||
* value at all are selected. | ||
*/ | ||
readonly indeterminate: Observable<boolean> = this._selection.change.pipe( | ||
switchMap(() => observableOf(this._selection.isPartialSelected())), | ||
); | ||
|
||
/** | ||
* Toggles the select-all state. | ||
* @param event The click event if the toggle is triggered by a (mouse or keyboard) click. If | ||
* using with a native `<input type="checkbox">`, the parameter is required for the | ||
* indeterminate state to work properly. | ||
*/ | ||
toggle(event?: MouseEvent) { | ||
// This is needed when applying the directive on a native <input type="checkbox"> | ||
// checkbox. The default behavior needs to be prevented in order to support the indeterminate | ||
// state. The timeout is also needed so the checkbox can show the latest state. | ||
if (event) { | ||
event.preventDefault(); | ||
} | ||
|
||
setTimeout(() => { | ||
this._selection.toggleSelectAll(); | ||
}); | ||
} | ||
|
||
private readonly _destroyed = new Subject<void>(); | ||
|
||
constructor( | ||
@Optional() private readonly _selection: CdkSelection<T>, | ||
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) private readonly _controlValueAccessor: | ||
ControlValueAccessor[]) {} | ||
|
||
ngOnInit() { | ||
yifange marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this._assertValidParentSelection(); | ||
this._configureControlValueAccessor(); | ||
} | ||
|
||
private _configureControlValueAccessor() { | ||
if (this._controlValueAccessor && this._controlValueAccessor.length) { | ||
this._controlValueAccessor[0].registerOnChange((e: unknown) => { | ||
if (e === true || e === false) { | ||
this.toggle(); | ||
} | ||
}); | ||
this.checked.pipe(takeUntil(this._destroyed)).subscribe((state) => { | ||
this._controlValueAccessor[0].writeValue(state); | ||
}); | ||
} | ||
} | ||
|
||
private _assertValidParentSelection() { | ||
if (!this._selection && isDevMode()) { | ||
throw Error('CdkSelectAll: missing CdkSelection in the parent'); | ||
} | ||
|
||
if (!this._selection.multiple && isDevMode()) { | ||
throw Error('CdkSelectAll: CdkSelection must have cdkSelectionMultiple set to true'); | ||
} | ||
} | ||
|
||
ngOnDestroy() { | ||
this._destroyed.next(); | ||
this._destroyed.complete(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef, CdkTable} from '@angular/cdk/table'; | ||
import { | ||
Component, | ||
Input, | ||
isDevMode, | ||
OnDestroy, | ||
OnInit, | ||
Optional, | ||
ViewChild, | ||
ChangeDetectionStrategy, | ||
ViewEncapsulation, | ||
} from '@angular/core'; | ||
|
||
import {CdkSelection} from './selection'; | ||
|
||
/** | ||
* Column that adds row selecting checkboxes and a select-all checkbox if `cdkSelectionMultiple` is | ||
* `true`. | ||
* | ||
* Must be used within a parent `CdkSelection` directive. | ||
*/ | ||
@Component({ | ||
selector: 'cdk-selection-column', | ||
template: ` | ||
<ng-container cdkColumnDef> | ||
<th cdkHeaderCell *cdkHeaderCellDef> | ||
<input type="checkbox" *ngIf="selection.multiple" | ||
cdkSelectAll | ||
#allToggler="cdkSelectAll" | ||
[checked]="allToggler.checked | async" | ||
[indeterminate]="allToggler.indeterminate | async" | ||
(click)="allToggler.toggle($event)"> | ||
</th> | ||
<td cdkCell *cdkCellDef="let row; let i = $index"> | ||
<input type="checkbox" | ||
#toggler="cdkSelectionToggle" | ||
cdkSelectionToggle | ||
[cdkSelectionToggleValue]="row" | ||
[cdkSelectionToggleIndex]="i" | ||
(click)="toggler.toggle()" | ||
[checked]="toggler.checked | async"> | ||
</td> | ||
</ng-container> | ||
`, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
encapsulation: ViewEncapsulation.None, | ||
}) | ||
export class CdkSelectionColumn<T> implements OnInit, OnDestroy { | ||
/** Column name that should be used to reference this column. */ | ||
@Input('cdkSelectionColumnName') | ||
get name(): string { | ||
return this._name; | ||
} | ||
set name(name: string) { | ||
this._name = name; | ||
|
||
this._syncColumnDefName(); | ||
} | ||
private _name: string; | ||
|
||
@ViewChild(CdkColumnDef, {static: true}) private readonly _columnDef: CdkColumnDef; | ||
@ViewChild(CdkCellDef, {static: true}) private readonly _cell: CdkCellDef; | ||
@ViewChild(CdkHeaderCellDef, {static: true}) private readonly _headerCell: CdkHeaderCellDef; | ||
|
||
constructor( | ||
@Optional() private _table: CdkTable<T>, | ||
@Optional() readonly selection: CdkSelection<T>, | ||
) {} | ||
|
||
ngOnInit() { | ||
if (!this.selection && isDevMode()) { | ||
throw Error('CdkSelectionColumn: missing CdkSelection in the parent'); | ||
} | ||
|
||
this._syncColumnDefName(); | ||
|
||
if (this._table) { | ||
this._columnDef.cell = this._cell; | ||
this._columnDef.headerCell = this._headerCell; | ||
this._table.addColumnDef(this._columnDef); | ||
} else { | ||
if (isDevMode()) { | ||
throw Error('CdkSelectionColumn: missing parent table'); | ||
} | ||
} | ||
} | ||
|
||
ngOnDestroy() { | ||
if (this._table) { | ||
this._table.removeColumnDef(this._columnDef); | ||
} | ||
} | ||
|
||
private _syncColumnDefName() { | ||
if (this._columnDef) { | ||
this._columnDef.name = this._name; | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {CdkTableModule} from '@angular/cdk/table'; | ||
import {CommonModule} from '@angular/common'; | ||
import {NgModule} from '@angular/core'; | ||
|
||
import {CdkRowSelection} from './row-selection'; | ||
import {CdkSelectAll} from './select-all'; | ||
import {CdkSelection} from './selection'; | ||
import {CdkSelectionColumn} from './selection-column'; | ||
import {CdkSelectionToggle} from './selection-toggle'; | ||
|
||
@NgModule({ | ||
imports: [ | ||
CommonModule, | ||
CdkTableModule, | ||
], | ||
exports: [ | ||
CdkSelection, | ||
CdkSelectionToggle, | ||
CdkSelectAll, | ||
CdkSelectionColumn, | ||
CdkRowSelection, | ||
], | ||
declarations: [ | ||
CdkSelection, | ||
CdkSelectionToggle, | ||
CdkSelectAll, | ||
CdkSelectionColumn, | ||
CdkRowSelection, | ||
], | ||
}) | ||
export class CdkSelectionModule { | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate more on the need for
setTimeout
? As it is, this will cause an extra round of change detection which we generally try to avoidThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I was unsure about what I was doing here...
Here is an example: https://stackblitz.com/edit/angular-native-checkbox
The problem is I don't seem to be able to control what the native input checkbox shows with the
[checked]
binding when the indeterminate state is involved.I had to use
event.preventDefault()
as the native checkbox seems to think clicking an indeterminate checkbox should check it, while we wish to uncheck it.But with
event.preventDefault()
, the checkbox doesn't show the checkmark change even though the actual value is updated when clicked. Adding thatsetTimeout
makes it work.I don't like the extra change detection, but I don't know how to solve it properly.