Skip to content

Commit b61df23

Browse files
authored
feat(cdk-experimental/selection): add selection state to a list of items (#18424)
1 parent a58c725 commit b61df23

29 files changed

+1131
-12
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
/src/cdk-experimental/dialog/** @jelbourn @crisbeto
121121
/src/cdk-experimental/popover-edit/** @kseamon @andrewseguin
122122
/src/cdk-experimental/scrolling/** @mmalerba
123+
/src/cdk-experimental/selection/** @yifange @jelbourn
123124

124125
# Docs examples & guides
125126
/guides/** @jelbourn
@@ -196,6 +197,7 @@
196197
/src/dev-app/typography/** @crisbeto
197198
/src/dev-app/virtual-scroll/** @mmalerba
198199
/src/dev-app/youtube-player/** @nathantate
200+
/src/dev-app/selection/** @yifange @jelbourn
199201

200202
# E2E app
201203
/src/e2e-app/* @jelbourn

src/cdk-experimental/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
33
"dialog",
44
"popover-edit",
55
"scrolling",
6+
"selection",
67
]
78

89
# List of all entry-point targets of the Angular cdk-experimental package.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_module")
4+
5+
ng_module(
6+
name = "selection",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/cdk-experimental/selection",
12+
deps = [
13+
"//src/cdk/coercion",
14+
"//src/cdk/collections",
15+
"//src/cdk/table",
16+
"@npm//@angular/core",
17+
"@npm//@angular/forms",
18+
"@npm//rxjs",
19+
],
20+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './public-api';
10+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './selection';
10+
export * from './select-all';
11+
export * from './selection-toggle';
12+
export * from './selection-column';
13+
export * from './row-selection';
14+
export * from './selection-set';
15+
export * from './selection-module';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion';
10+
import {Directive, Input} from '@angular/core';
11+
12+
import {CdkSelection} from './selection';
13+
14+
/**
15+
* Applies `cdk-selected` class and `aria-selected` to an element.
16+
*
17+
* Must be used within a parent `CdkSelection` directive.
18+
* Must be provided with the value. The index is required if `trackBy` is used on the `CdkSelection`
19+
* directive.
20+
*/
21+
@Directive({
22+
selector: '[cdkRowSelection]',
23+
host: {
24+
'[class.cdk-selected]': '_selection.isSelected(this.value, this.index)',
25+
'[attr.aria-selected]': '_selection.isSelected(this.value, this.index)',
26+
},
27+
})
28+
export class CdkRowSelection<T> {
29+
@Input('cdkRowSelectionValue') value: T;
30+
31+
@Input('cdkRowSelectionIndex')
32+
get index(): number|undefined { return this._index; }
33+
set index(index: number|undefined) { this._index = coerceNumberProperty(index); }
34+
private _index?: number;
35+
36+
constructor(readonly _selection: CdkSelection<T>) {}
37+
38+
static ngAcceptInputType_index: NumberInput;
39+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directive, Inject, isDevMode, OnDestroy, OnInit, Optional, Self} from '@angular/core';
10+
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
11+
import {Observable, of as observableOf, Subject} from 'rxjs';
12+
import {switchMap, takeUntil} from 'rxjs/operators';
13+
14+
import {CdkSelection} from './selection';
15+
16+
/**
17+
* Makes the element a select-all toggle.
18+
*
19+
* Must be used within a parent `CdkSelection` directive. It toggles the selection states
20+
* of all the selection toggles connected with the `CdkSelection` directive.
21+
* If the element implements `ControlValueAccessor`, e.g. `MatCheckbox`, the directive
22+
* automatically connects it with the select-all state provided by the `CdkSelection` directive. If
23+
* not, use `checked$` to get the checked state, `indeterminate$` to get the indeterminate state,
24+
* and `toggle()` to change the selection state.
25+
*/
26+
@Directive({
27+
selector: '[cdkSelectAll]',
28+
exportAs: 'cdkSelectAll',
29+
})
30+
export class CdkSelectAll<T> implements OnDestroy, OnInit {
31+
/**
32+
* The checked state of the toggle.
33+
* Resolves to `true` if all the values are selected, `false` if no value is selected.
34+
*/
35+
readonly checked: Observable<boolean> = this._selection.change.pipe(
36+
switchMap(() => observableOf(this._selection.isAllSelected())),
37+
);
38+
39+
/**
40+
* The indeterminate state of the toggle.
41+
* Resolves to `true` if part (not all) of the values are selected, `false` if all values or no
42+
* value at all are selected.
43+
*/
44+
readonly indeterminate: Observable<boolean> = this._selection.change.pipe(
45+
switchMap(() => observableOf(this._selection.isPartialSelected())),
46+
);
47+
48+
/**
49+
* Toggles the select-all state.
50+
* @param event The click event if the toggle is triggered by a (mouse or keyboard) click. If
51+
* using with a native `<input type="checkbox">`, the parameter is required for the
52+
* indeterminate state to work properly.
53+
*/
54+
toggle(event?: MouseEvent) {
55+
// This is needed when applying the directive on a native <input type="checkbox">
56+
// checkbox. The default behavior needs to be prevented in order to support the indeterminate
57+
// state. The timeout is also needed so the checkbox can show the latest state.
58+
if (event) {
59+
event.preventDefault();
60+
}
61+
62+
setTimeout(() => {
63+
this._selection.toggleSelectAll();
64+
});
65+
}
66+
67+
private readonly _destroyed = new Subject<void>();
68+
69+
constructor(
70+
@Optional() private readonly _selection: CdkSelection<T>,
71+
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) private readonly _controlValueAccessor:
72+
ControlValueAccessor[]) {}
73+
74+
ngOnInit() {
75+
this._assertValidParentSelection();
76+
this._configureControlValueAccessor();
77+
}
78+
79+
private _configureControlValueAccessor() {
80+
if (this._controlValueAccessor && this._controlValueAccessor.length) {
81+
this._controlValueAccessor[0].registerOnChange((e: unknown) => {
82+
if (e === true || e === false) {
83+
this.toggle();
84+
}
85+
});
86+
this.checked.pipe(takeUntil(this._destroyed)).subscribe((state) => {
87+
this._controlValueAccessor[0].writeValue(state);
88+
});
89+
}
90+
}
91+
92+
private _assertValidParentSelection() {
93+
if (!this._selection && isDevMode()) {
94+
throw Error('CdkSelectAll: missing CdkSelection in the parent');
95+
}
96+
97+
if (!this._selection.multiple && isDevMode()) {
98+
throw Error('CdkSelectAll: CdkSelection must have cdkSelectionMultiple set to true');
99+
}
100+
}
101+
102+
ngOnDestroy() {
103+
this._destroyed.next();
104+
this._destroyed.complete();
105+
}
106+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef, CdkTable} from '@angular/cdk/table';
10+
import {
11+
Component,
12+
Input,
13+
isDevMode,
14+
OnDestroy,
15+
OnInit,
16+
Optional,
17+
ViewChild,
18+
ChangeDetectionStrategy,
19+
ViewEncapsulation,
20+
} from '@angular/core';
21+
22+
import {CdkSelection} from './selection';
23+
24+
/**
25+
* Column that adds row selecting checkboxes and a select-all checkbox if `cdkSelectionMultiple` is
26+
* `true`.
27+
*
28+
* Must be used within a parent `CdkSelection` directive.
29+
*/
30+
@Component({
31+
selector: 'cdk-selection-column',
32+
template: `
33+
<ng-container cdkColumnDef>
34+
<th cdkHeaderCell *cdkHeaderCellDef>
35+
<input type="checkbox" *ngIf="selection.multiple"
36+
cdkSelectAll
37+
#allToggler="cdkSelectAll"
38+
[checked]="allToggler.checked | async"
39+
[indeterminate]="allToggler.indeterminate | async"
40+
(click)="allToggler.toggle($event)">
41+
</th>
42+
<td cdkCell *cdkCellDef="let row; let i = $index">
43+
<input type="checkbox"
44+
#toggler="cdkSelectionToggle"
45+
cdkSelectionToggle
46+
[cdkSelectionToggleValue]="row"
47+
[cdkSelectionToggleIndex]="i"
48+
(click)="toggler.toggle()"
49+
[checked]="toggler.checked | async">
50+
</td>
51+
</ng-container>
52+
`,
53+
changeDetection: ChangeDetectionStrategy.OnPush,
54+
encapsulation: ViewEncapsulation.None,
55+
})
56+
export class CdkSelectionColumn<T> implements OnInit, OnDestroy {
57+
/** Column name that should be used to reference this column. */
58+
@Input('cdkSelectionColumnName')
59+
get name(): string {
60+
return this._name;
61+
}
62+
set name(name: string) {
63+
this._name = name;
64+
65+
this._syncColumnDefName();
66+
}
67+
private _name: string;
68+
69+
@ViewChild(CdkColumnDef, {static: true}) private readonly _columnDef: CdkColumnDef;
70+
@ViewChild(CdkCellDef, {static: true}) private readonly _cell: CdkCellDef;
71+
@ViewChild(CdkHeaderCellDef, {static: true}) private readonly _headerCell: CdkHeaderCellDef;
72+
73+
constructor(
74+
@Optional() private _table: CdkTable<T>,
75+
@Optional() readonly selection: CdkSelection<T>,
76+
) {}
77+
78+
ngOnInit() {
79+
if (!this.selection && isDevMode()) {
80+
throw Error('CdkSelectionColumn: missing CdkSelection in the parent');
81+
}
82+
83+
this._syncColumnDefName();
84+
85+
if (this._table) {
86+
this._columnDef.cell = this._cell;
87+
this._columnDef.headerCell = this._headerCell;
88+
this._table.addColumnDef(this._columnDef);
89+
} else {
90+
if (isDevMode()) {
91+
throw Error('CdkSelectionColumn: missing parent table');
92+
}
93+
}
94+
}
95+
96+
ngOnDestroy() {
97+
if (this._table) {
98+
this._table.removeColumnDef(this._columnDef);
99+
}
100+
}
101+
102+
private _syncColumnDefName() {
103+
if (this._columnDef) {
104+
this._columnDef.name = this._name;
105+
}
106+
}
107+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {CdkTableModule} from '@angular/cdk/table';
10+
import {CommonModule} from '@angular/common';
11+
import {NgModule} from '@angular/core';
12+
13+
import {CdkRowSelection} from './row-selection';
14+
import {CdkSelectAll} from './select-all';
15+
import {CdkSelection} from './selection';
16+
import {CdkSelectionColumn} from './selection-column';
17+
import {CdkSelectionToggle} from './selection-toggle';
18+
19+
@NgModule({
20+
imports: [
21+
CommonModule,
22+
CdkTableModule,
23+
],
24+
exports: [
25+
CdkSelection,
26+
CdkSelectionToggle,
27+
CdkSelectAll,
28+
CdkSelectionColumn,
29+
CdkRowSelection,
30+
],
31+
declarations: [
32+
CdkSelection,
33+
CdkSelectionToggle,
34+
CdkSelectAll,
35+
CdkSelectionColumn,
36+
CdkRowSelection,
37+
],
38+
})
39+
export class CdkSelectionModule {
40+
}

0 commit comments

Comments
 (0)