From 006e1e3531bc4a310a2dfa7f8f6b32c7de60596d Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Mon, 30 Dec 2019 11:09:38 -0800 Subject: [PATCH 1/7] Add CdkSelection and examples No test coverage so far (cherry picked from commit 500597faf7571655552f3011a5365f566fc5d21c) --- src/cdk-experimental/config.bzl | 1 + src/cdk-experimental/selection/BUILD.bazel | 53 ++++ src/cdk-experimental/selection/index.ts | 10 + src/cdk-experimental/selection/public-api.ts | 14 ++ .../selection/row-selection.ts | 49 ++++ src/cdk-experimental/selection/select-all.ts | 114 +++++++++ .../selection/selection-column.ts | 100 ++++++++ .../selection/selection-module.ts | 40 ++++ .../selection/selection-set.ts | 123 ++++++++++ .../selection/selection-toggle.ts | 104 ++++++++ src/cdk-experimental/selection/selection.ts | 226 ++++++++++++++++++ src/components-examples/BUILD.bazel | 1 + .../cdk-experimental/selection/BUILD.bazel | 29 +++ .../cdk-selection-column-example.css | 7 + .../cdk-selection-column-example.html | 30 +++ .../cdk-selection-column-example.ts | 58 +++++ .../cdk-selection-list-example.html | 45 ++++ .../cdk-selection-list-example.ts | 53 ++++ .../cdk-experimental/selection/index.ts | 35 +++ src/dev-app/BUILD.bazel | 1 + src/dev-app/dev-app/dev-app-layout.ts | 4 +- src/dev-app/dev-app/dev-app-module.ts | 1 + src/dev-app/dev-app/routes.ts | 6 +- src/dev-app/example/example-list.ts | 14 +- src/dev-app/example/example-module.ts | 5 +- src/dev-app/selection/BUILD.bazel | 14 ++ .../selection/selection-demo-module.ts | 25 ++ src/dev-app/selection/selection-demo.ts | 21 ++ 28 files changed, 1171 insertions(+), 12 deletions(-) create mode 100644 src/cdk-experimental/selection/BUILD.bazel create mode 100644 src/cdk-experimental/selection/index.ts create mode 100644 src/cdk-experimental/selection/public-api.ts create mode 100644 src/cdk-experimental/selection/row-selection.ts create mode 100644 src/cdk-experimental/selection/select-all.ts create mode 100644 src/cdk-experimental/selection/selection-column.ts create mode 100644 src/cdk-experimental/selection/selection-module.ts create mode 100644 src/cdk-experimental/selection/selection-set.ts create mode 100644 src/cdk-experimental/selection/selection-toggle.ts create mode 100644 src/cdk-experimental/selection/selection.ts create mode 100644 src/components-examples/cdk-experimental/selection/BUILD.bazel create mode 100644 src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css create mode 100644 src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html create mode 100644 src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts create mode 100644 src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html create mode 100644 src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts create mode 100644 src/components-examples/cdk-experimental/selection/index.ts create mode 100644 src/dev-app/selection/BUILD.bazel create mode 100644 src/dev-app/selection/selection-demo-module.ts create mode 100644 src/dev-app/selection/selection-demo.ts diff --git a/src/cdk-experimental/config.bzl b/src/cdk-experimental/config.bzl index e5155d7465ca..68a19d8fb46a 100644 --- a/src/cdk-experimental/config.bzl +++ b/src/cdk-experimental/config.bzl @@ -3,6 +3,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [ "dialog", "popover-edit", "scrolling", + "selection", ] # List of all entry-point targets of the Angular cdk-experimental package. diff --git a/src/cdk-experimental/selection/BUILD.bazel b/src/cdk-experimental/selection/BUILD.bazel new file mode 100644 index 000000000000..d1556eaa44d7 --- /dev/null +++ b/src/cdk-experimental/selection/BUILD.bazel @@ -0,0 +1,53 @@ +package(default_visibility = ["//visibility:public"]) + +load("//src/e2e-app:test_suite.bzl", "e2e_test_suite") +load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite") + +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", + ], +) + +ng_test_library( + name = "unit_test_sources", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["**/*.e2e.spec.ts"], + ), + deps = [ + ":selection", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_test_sources"], +) + +ng_e2e_test_library( + name = "e2e_test_sources", + srcs = glob(["**/*.e2e.spec.ts"]), + deps = [ + "//src/cdk/testing/private/e2e", + ], +) + +e2e_test_suite( + name = "e2e_tests", + deps = [ + ":e2e_test_sources", + "//src/cdk/testing/private/e2e", + ], +) diff --git a/src/cdk-experimental/selection/index.ts b/src/cdk-experimental/selection/index.ts new file mode 100644 index 000000000000..e1fc5bfc0361 --- /dev/null +++ b/src/cdk-experimental/selection/index.ts @@ -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'; + diff --git a/src/cdk-experimental/selection/public-api.ts b/src/cdk-experimental/selection/public-api.ts new file mode 100644 index 000000000000..8cae343cd78d --- /dev/null +++ b/src/cdk-experimental/selection/public-api.ts @@ -0,0 +1,14 @@ +/** + * @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-module'; diff --git a/src/cdk-experimental/selection/row-selection.ts b/src/cdk-experimental/selection/row-selection.ts new file mode 100644 index 000000000000..e1a94d105330 --- /dev/null +++ b/src/cdk-experimental/selection/row-selection.ts @@ -0,0 +1,49 @@ +/** + * @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, HostBinding, 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]', +}) +export class CdkRowSelection { + @Input() + get cdkRowSelectionValue(): T { + return this._value; + } + set cdkRowSelectionValue(value: T) { + this._value = value; + } + _value: T; + + @Input() + get cdkRowSelectionIndex(): number|undefined { + return this._index; + } + set cdkRowSelectionIndex(index: number|undefined) { + this._index = index; + } + _index?: number; + + constructor(private readonly _selection: CdkSelection) {} + + @HostBinding('class.cdk-selected') + @HostBinding('attr.aria-selected') + get isSelected() { + return this._selection.isSelected(this._value, this._index); + } +} diff --git a/src/cdk-experimental/selection/select-all.ts b/src/cdk-experimental/selection/select-all.ts new file mode 100644 index 000000000000..95a197cc238d --- /dev/null +++ b/src/cdk-experimental/selection/select-all.ts @@ -0,0 +1,114 @@ +/** + * @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, OnDestroy, OnInit, Optional, Self} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {BehaviorSubject, 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 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$ = new BehaviorSubject(false); + + /** + * 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$ = new BehaviorSubject(false); + + /** + * 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 , the parameter is required for the + * indeterminate state to work properly. + */ + toggle(event?: MouseEvent) { + // This is needed when applying the directive on a native + // 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(); + + constructor( + @Optional() private readonly _selection: CdkSelection, + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) private readonly _controlValueAccessor: + ControlValueAccessor[]) {} + + ngOnInit() { + if (!this._selection) { + throw new Error('CdkSelectAll: missing CdkSelection in the parent'); + } + + if (!this._selection.cdkSelectionMultiple) { + throw new Error('CdkSelectAll: CdkSelection must have cdkSelectionMultiple set to true'); + } + + this._selection.cdkSelectionChange + .pipe( + switchMap(() => observableOf(this._selection.isAllSelected())), + takeUntil(this._destroyed$), + ) + .subscribe((state) => { + this.checked$.next(state); + }); + + this._selection.cdkSelectionChange + .pipe( + switchMap(() => observableOf(this._selection.isPartialSelected())), + takeUntil(this._destroyed$), + ) + .subscribe((state) => { + this.indeterminate$.next(state); + }); + + 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); + }); + } + } + + ngOnDestroy() { + this._destroyed$.next(); + this._destroyed$.complete(); + } +} diff --git a/src/cdk-experimental/selection/selection-column.ts b/src/cdk-experimental/selection/selection-column.ts new file mode 100644 index 000000000000..6b160111acb7 --- /dev/null +++ b/src/cdk-experimental/selection/selection-column.ts @@ -0,0 +1,100 @@ +/** + * @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, + OnDestroy, + OnInit, + Optional, + ViewChild, +} 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: ` + + + + + + + + + `, +}) +export class CdkSelectionColumn implements OnInit, OnDestroy { + /** Column name that should be used to reference this column. */ + @Input() + get cdkSelectionColumnName(): string { + return this._name; + } + set cdkSelectionColumnName(name: string) { + this._name = name; + + this.syncColumnDefName(); + } + _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, + @Optional() readonly selection: CdkSelection, + ) {} + + ngOnInit() { + if (!this.selection) { + throw new 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 { + throw new Error('CdkSelectionColumn: missing parent table'); + } + } + + ngOnDestroy() { + if (this.table) { + this.table.removeColumnDef(this._columnDef); + } + } + + private syncColumnDefName() { + if (this._columnDef) { + this._columnDef.name = this._name; + } + } +} diff --git a/src/cdk-experimental/selection/selection-module.ts b/src/cdk-experimental/selection/selection-module.ts new file mode 100644 index 000000000000..63ba7286b28d --- /dev/null +++ b/src/cdk-experimental/selection/selection-module.ts @@ -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 { +} diff --git a/src/cdk-experimental/selection/selection-set.ts b/src/cdk-experimental/selection/selection-set.ts new file mode 100644 index 000000000000..a807d8b3e0e3 --- /dev/null +++ b/src/cdk-experimental/selection/selection-set.ts @@ -0,0 +1,123 @@ +/** + * @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 {TrackByFunction} from '@angular/core'; +import {Subject} from 'rxjs'; + +interface TrackBySelection { + isSelected(value: SelectableWithIndex): boolean; + select(...values: Array>): void; + deselect(...values: Array>): void; + changed$: Subject>; +} + +/** + * A selectable value with an optional index. The index is required when the selection is used with + * `trackBy`. + */ +export interface SelectableWithIndex { + value: T; + index?: number; +} + +/** + * Represents the change in the selection set. + */ +export interface SelectionChange { + before: Array>; + after: Array>; +} + +/** + * Maintains a set of selected items. Support selecting and deselecting items, and checking if a + * value is selected. + * When constructed with a `trackByFn`, all the items will be identified by applying the `trackByFn` + * on them. Because `trackByFn` requires the index of the item to be passed in, the `index` field is + * expected to be set when calling `isSelected`, `select` and `deselect`. + */ +export class SelectionSet implements TrackBySelection { + private selectionMap = new Map>, SelectableWithIndex>(); + changed$ = new Subject>(); + + constructor(private _multiple = false, private _trackByFn?: TrackByFunction) {} + + isSelected(value: SelectableWithIndex): boolean { + return this.selectionMap.has(this._getTrackedByValue(value)); + } + + select(...selects: Array>) { + if (!this._multiple && selects.length > 1) { + throw new Error('SelectionSet: not multiple selection'); + } + + const before = this._getCurrentSelection(); + + if (!this._multiple) { + this.selectionMap.clear(); + } + + const toSelect: Array> = []; + for (const select of selects) { + if (this.isSelected(select)) { + continue; + } + + toSelect.push(select); + this._markSelected(this._getTrackedByValue(select), select); + } + + const after = this._getCurrentSelection(); + + this.changed$.next({before, after}); + } + + deselect(...selects: Array>) { + if (!this._multiple && selects.length > 1) { + throw new Error('SelectionSet: not multiple selection'); + } + + const before = this._getCurrentSelection(); + const toDeselect: Array> = []; + + for (const select of selects) { + if (!this.isSelected(select)) { + continue; + } + + toDeselect.push(select); + this._markDeselected(this._getTrackedByValue(select)); + } + + const after = this._getCurrentSelection(); + this.changed$.next({before, after}); + } + + private _markSelected(key: T|ReturnType>, toSelect: SelectableWithIndex) { + this.selectionMap.set(key, toSelect); + } + + private _markDeselected(key: T|ReturnType>) { + this.selectionMap.delete(key); + } + + private _getTrackedByValue(select: SelectableWithIndex) { + if (!this._trackByFn) { + return select.value; + } + + if (select.index == null) { + throw new Error('SelectionSet: index required when trackByFn is used.'); + } + + return this._trackByFn(select.index, select.value); + } + + private _getCurrentSelection(): Array> { + return Array.from(this.selectionMap.values()); + } +} diff --git a/src/cdk-experimental/selection/selection-toggle.ts b/src/cdk-experimental/selection/selection-toggle.ts new file mode 100644 index 000000000000..790162a35c39 --- /dev/null +++ b/src/cdk-experimental/selection/selection-toggle.ts @@ -0,0 +1,104 @@ +/** + * @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, Input, OnDestroy, OnInit, Optional, Self} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {BehaviorSubject, of as observableOf, Subject} from 'rxjs'; +import {distinctUntilChanged, switchMap, takeUntil} from 'rxjs/operators'; + +import {CdkSelection} from './selection'; + +/** + * Makes the element a selection toggle. + * + * Must be used within a parent `CdkSelection` directive. + * Must be provided with the value. If `trackBy` is used on `CdkSelection`, the index of the value + * is required. If the element implements `ControlValueAccessor`, e.g. `MatCheckbox`, the directive + * automatically connects it with the selection state provided by the `CdkSelection` directive. If + * not, use `checked$` to get the checked state of the value, and `toggle()` to change the selection + * state. + */ +@Directive({ + selector: '[cdkSelectionToggle]', + exportAs: 'cdkSelectionToggle', +}) +export class CdkSelectionToggle implements OnDestroy, OnInit { + /** The value that is associated with the toggle */ + @Input() + get cdkSelectionToggleValue(): T { + return this._value; + } + set cdkSelectionToggleValue(value: T) { + this._value = value; + } + private _value: T; + + /** The index of the value in the list. Required when used with `trackBy` */ + @Input() + get cdkSelectionToggleIndex(): number|undefined { + return this._index; + } + set cdkSelectionToggleIndex(index: number|undefined) { + this._index = index; + } + private _index?: number; + + /** The checked state of the selection toggle */ + readonly checked$ = new BehaviorSubject(false); + + /** Toggles the selection */ + toggle() { + this._selection.toggleSelection(this._value, this._index); + } + + private _destroyed$ = new Subject(); + + constructor( + @Optional() private _selection: CdkSelection, + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) private _controlValueAccessors: + ControlValueAccessor[], + ) {} + + ngOnInit() { + if (!this._selection) { + throw new Error('CdkSelectAll: missing CdkSelection in the parent'); + } + + if (this._controlValueAccessors && this._controlValueAccessors.length) { + this._controlValueAccessors[0].registerOnChange((e: unknown) => { + if (typeof e === 'boolean') { + this.toggle(); + } + }); + + this.checked$.pipe(takeUntil(this._destroyed$)).subscribe((state) => { + this._controlValueAccessors[0].writeValue(state); + }); + } + + this.checked$.next(this._isSelected()); + this._selection.cdkSelectionChange + .pipe( + switchMap(() => observableOf(this._isSelected())), + distinctUntilChanged(), + takeUntil(this._destroyed$), + ) + .subscribe((state: boolean) => { + this.checked$.next(state); + }); + } + + ngOnDestroy() { + this._destroyed$.next(); + this._destroyed$.complete(); + } + + private _isSelected(): boolean { + return this._selection.isSelected(this._value, this._index); + } +} diff --git a/src/cdk-experimental/selection/selection.ts b/src/cdk-experimental/selection/selection.ts new file mode 100644 index 000000000000..ec6c4177b755 --- /dev/null +++ b/src/cdk-experimental/selection/selection.ts @@ -0,0 +1,226 @@ +/** + * @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 {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {CollectionViewer, DataSource, isDataSource, ListRange} from '@angular/cdk/collections'; +import { + AfterContentChecked, + Directive, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + TrackByFunction +} from '@angular/core'; +import {Observable, of as observableOf, ReplaySubject, Subscription} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; + +import {SelectableWithIndex, SelectionChange, SelectionSet} from './selection-set'; + +/** + * Manages the selection states of the items and provides methods to check and update the selection + * states. + * It must be applied to the parent element if `cdkSelectionToggle`, `cdkSelectAll`, + * `cdkRowSelection` and `cdkSelectionColumn` are applied. + */ +@Directive({ + selector: '[cdkSelection]', + exportAs: 'cdkSelection', +}) +export class CdkSelection implements OnInit, AfterContentChecked, CollectionViewer, OnDestroy { + viewChange: Observable; + + @Input() + get dataSource(): TableDataSource { + return this._dataSource; + } + set dataSource(dataSource: TableDataSource) { + if (this._dataSource !== dataSource) { + this._switchDataSource(dataSource); + } + } + private _dataSource: TableDataSource; + + @Input() + get trackBy(): TrackByFunction { + return this._trackByFn; + } + set trackBy(fn: TrackByFunction) { + this._trackByFn = fn; + } + private _trackByFn: TrackByFunction; + + /** Whether to support multiple selection */ + @Input() + get cdkSelectionMultiple(): boolean { + return this._multiple; + } + set cdkSelectionMultiple(multiple: boolean) { + this._multiple = coerceBooleanProperty(multiple); + } + private _multiple: boolean; + + /** Emits when selection changes. */ + @Output() cdkSelectionChange = new EventEmitter>(); + + /** Latest data provided by the data source. */ + private _data: T[]|readonly T[]; + + /** Subscription that listens for the data provided by the data source. */ + private _renderChangeSubscription: Subscription|null; + + private _destroyed$ = new ReplaySubject(1); + + private _selection: SelectionSet; + + private _switchDataSource(dataSource: TableDataSource) { + this._data = []; + + if (isDataSource(this._dataSource)) { + this._dataSource.disconnect(this); + } + + if (this._renderChangeSubscription) { + this._renderChangeSubscription.unsubscribe(); + this._renderChangeSubscription = null; + } + + this._dataSource = dataSource; + } + + private _observeRenderChanges() { + if (!this._dataSource) { + return; + } + + let dataStream: Observable>|undefined; + + if (isDataSource(this._dataSource)) { + dataStream = this._dataSource.connect(this); + } else if (this._dataSource instanceof Observable) { + dataStream = this._dataSource; + } else if (Array.isArray(this._dataSource)) { + dataStream = observableOf(this._dataSource); + } + + if (dataStream == null) { + throw new Error('Unknown data source'); + } + + this._renderChangeSubscription = + dataStream.pipe(takeUntil(this._destroyed$)).subscribe((data) => { + this._data = data || []; + }); + } + + ngOnInit() { + this._selection = new SelectionSet(this._multiple, this._trackByFn); + this._selection.changed$.pipe(takeUntil(this._destroyed$)).subscribe((change) => { + this.updateSelectAllState(); + this.cdkSelectionChange.emit(change); + }); + } + + ngAfterContentChecked() { + if (this._dataSource && !this._renderChangeSubscription) { + this._observeRenderChanges(); + } + } + + ngOnDestroy() { + this._destroyed$.next(); + this._destroyed$.complete(); + + if (isDataSource(this._dataSource)) { + this._dataSource.disconnect(this); + } + } + + /** Toggles selection for a given value. `index` is required if `trackBy` is used. */ + toggleSelection(value: T, index?: number) { + if (this.trackBy && index == null) { + throw new Error('CdkSelection: index required when trackBy is used'); + } + + if (this.isSelected(value, index)) { + this._selection.deselect({value, index}); + } else { + this._selection.select({value, index}); + } + } + + /** + * Toggles select-all. If no value is selected, select all values. If all values or some of the + * values are selected, de-select all values. + */ + toggleSelectAll() { + if (!this._multiple) { + throw new Error('CdkSelection: multiple selection not enabled'); + } + + if (this.selectAllState === 'none') { + this.selectAll(); + } else { + this.clearAll(); + } + } + + /** Checks whether a value is selected. `index` is required if `trackBy` is used. */ + isSelected(value: T, index?: number) { + if (this.trackBy && index == null) { + throw new Error('CdkSelection: index required when trackBy is used'); + } + + return this._selection.isSelected({value, index}); + } + + /** Checks whether all values are selected. */ + isAllSelected() { + return this._data.every((value, index) => this._selection.isSelected({value, index})); + } + + /** Checks whether partially selected. */ + isPartialSelected() { + return !this.isAllSelected() && + this._data.some((value, index) => this._selection.isSelected({value, index})); + } + + private selectAll() { + const toSelect: Array> = []; + this._data.forEach((value, index) => { + toSelect.push({value, index}); + }); + + this._selection.select(...toSelect); + } + + private clearAll() { + const toDeselect: Array> = []; + this._data.forEach((value, index) => { + toDeselect.push({value, index}); + }); + + this._selection.deselect(...toDeselect); + } + + private updateSelectAllState() { + if (this.isAllSelected()) { + this.selectAllState = 'all'; + } else if (this.isPartialSelected()) { + this.selectAllState = 'partial'; + } else { + this.selectAllState = 'none'; + } + } + + selectAllState: SelectAllState = 'none'; +} + +type SelectAllState = 'all'|'none'|'partial'; +type TableDataSource = DataSource|Observable|T[]>|ReadonlyArray|T[]; diff --git a/src/components-examples/BUILD.bazel b/src/components-examples/BUILD.bazel index b3fde2ba93d3..cf51973985b6 100644 --- a/src/components-examples/BUILD.bazel +++ b/src/components-examples/BUILD.bazel @@ -55,6 +55,7 @@ EXAMPLE_PACKAGES = [ "//src/components-examples/cdk/clipboard", "//src/components-examples/cdk/a11y", "//src/components-examples/cdk-experimental/popover-edit", + "//src/components-examples/cdk-experimental/selection", ] ng_module( diff --git a/src/components-examples/cdk-experimental/selection/BUILD.bazel b/src/components-examples/cdk-experimental/selection/BUILD.bazel new file mode 100644 index 000000000000..7915e6cf928d --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/BUILD.bazel @@ -0,0 +1,29 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_module") + +ng_module( + name = "selection", + srcs = glob(["**/*.ts"]), + assets = glob([ + "**/*.html", + "**/*.css", + ]), + module_name = "@angular/components-examples/cdk-experimental/selection", + deps = [ + "//src/cdk-experimental/selection", + "//src/cdk/collections", + "//src/cdk/table", + "//src/material/checkbox", + "@npm//@angular/forms", + ], +) + +filegroup( + name = "source-files", + srcs = glob([ + "**/*.html", + "**/*.css", + "**/*.ts", + ]), +) diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css new file mode 100644 index 000000000000..134e458661a4 --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css @@ -0,0 +1,7 @@ +table { + border-collapse: collapse; +} + +tr.cdk-selected { + background-color: yellow; +} diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html new file mode 100644 index 000000000000..14412783c932 --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html @@ -0,0 +1,30 @@ +Selected: {{selected}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No. {{element.position}} Name {{element.name}} Weight {{element.weight}} Symbol {{element.symbol}}
diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts new file mode 100644 index 000000000000..cb2146c54a33 --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts @@ -0,0 +1,58 @@ +import {SelectionChange} from '@angular/cdk-experimental/selection/selection-set'; +import {Component, OnDestroy} from '@angular/core'; +import {ReplaySubject} from 'rxjs'; + +/** + * @title CDK Selection Column on a CDK table. + */ +@Component({ + selector: 'cdk-selection-column-example', + templateUrl: 'cdk-selection-column-example.html', + styleUrls: ['cdk-selection-column-example.css'], +}) +export class CdkSelectionColumnExample implements OnDestroy { + private readonly destroyed$ = new ReplaySubject(1); + + displayedColumns: string[] = ['select', 'position', 'name', 'weight', 'symbol']; + dataSource = ELEMENT_DATA; + selected: string[] = []; + + ngOnDestroy() { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + selectionChanged(event: SelectionChange) { + this.selected = event.after.map((select) => select.value.name); + } +} + +interface PeriodicElement { + name: string; + position: number; + weight: number; + symbol: string; +} + +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'}, + {position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'}, + {position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'}, + {position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'}, + {position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'}, + {position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'}, + {position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'}, + {position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'}, + {position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'}, + {position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'}, + {position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'}, +]; diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html new file mode 100644 index 000000000000..64a9e5758306 --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html @@ -0,0 +1,45 @@ +

native input

+Selected: {{selected1}} +
    + +
  • + + {{item}} +
  • +
+ +

mat-checkbox

+Selected: {{selected2}} +
    + +
  • + + {{item}} +
  • +
+ +

Single select with mat-checkbox

+Selected: {{selected3}} +
    +
  • + + {{item}} +
  • +
+ +

with trackBy

+Selected: {{selected4}} +
    + +
  • + + {{item}} +
  • +
+ + + diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts new file mode 100644 index 000000000000..3e4d6d45a897 --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts @@ -0,0 +1,53 @@ +import {SelectionChange} from '@angular/cdk-experimental/selection/selection-set'; +import {Component, OnDestroy} from '@angular/core'; +import {ReplaySubject} from 'rxjs'; + +/** + * @title CDK Selection on a simple list. + */ +@Component({ + selector: 'cdk-selection-list-example', + templateUrl: 'cdk-selection-list-example.html', +}) +export class CdkSelectionListExample implements OnDestroy { + private readonly destroyed$ = new ReplaySubject(1); + + data = ELEMENT_NAMES; + + selected1: string[] = []; + selected2: string[] = []; + selected3: string[] = []; + selected4: string[] = []; + + ngOnDestroy() { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + getCurrentSelected(event: SelectionChange) { + return event.after.map((select) => select.value); + } + + trackByFn(index: number, value: string) { + return index; + } + + changeElementName() { + this.data = ELEMENT_SYMBOLS; + } + + reset() { + this.data = ELEMENT_NAMES; + } +} + +const ELEMENT_NAMES = [ + 'Hydrogen', 'Helium', 'Lithium', 'Beryllium', 'Boron', 'Carbon', 'Nitrogen', + 'Oxygen', 'Fluorine', 'Neon', 'Sodium', 'Magnesium', 'Aluminum', 'Silicon', + 'Phosphorus', 'Sulfur', 'Chlorine', 'Argon', 'Potassium', 'Calcium', +]; + +const ELEMENT_SYMBOLS = [ + 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', + 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca' +]; diff --git a/src/components-examples/cdk-experimental/selection/index.ts b/src/components-examples/cdk-experimental/selection/index.ts new file mode 100644 index 000000000000..e252984ec552 --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/index.ts @@ -0,0 +1,35 @@ +import {CdkSelectionModule} from '@angular/cdk-experimental/selection'; +import {CdkTableModule} from '@angular/cdk/table'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCheckboxModule} from '@angular/material/checkbox'; + +import {CdkSelectionColumnExample} from './cdk-selection-column/cdk-selection-column-example'; +import {CdkSelectionListExample} from './cdk-selection-list/cdk-selection-list-example'; + +export { + CdkSelectionColumnExample, + CdkSelectionListExample, +}; + +const EXAMPLES = [ + CdkSelectionListExample, + CdkSelectionColumnExample, +]; + +@NgModule({ + imports: [ + CdkSelectionModule, + CdkTableModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + MatCheckboxModule, + + ], + declarations: EXAMPLES, + exports: EXAMPLES, +}) +export class CdkSelectionExamplesModule { +} diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel index c92f5b73fc33..429e57175d05 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -66,6 +66,7 @@ ng_module( "//src/dev-app/ripple", "//src/dev-app/screen-type", "//src/dev-app/select", + "//src/dev-app/selection", "//src/dev-app/sidenav", "//src/dev-app/slide-toggle", "//src/dev-app/slider", diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts index 192858a501e5..ca7d0cb5c902 100644 --- a/src/dev-app/dev-app/dev-app-layout.ts +++ b/src/dev-app/dev-app/dev-app-layout.ts @@ -9,8 +9,9 @@ import {Directionality} from '@angular/cdk/bidi'; import {OverlayContainer} from '@angular/cdk/overlay'; import {ChangeDetectorRef, Component, ElementRef, Inject, ViewEncapsulation} from '@angular/core'; -import {DevAppRippleOptions} from './ripple-options'; + import {DevAppDirectionality} from './dev-app-directionality'; +import {DevAppRippleOptions} from './ripple-options'; /** Root component for the dev-app demos. */ @Component({ @@ -56,6 +57,7 @@ export class DevAppLayout { {name: 'Ripple', route: '/ripple'}, {name: 'Screen Type', route: '/screen-type'}, {name: 'Select', route: '/select'}, + {name: 'Selection', route: '/selection'}, {name: 'Sidenav', route: '/sidenav'}, {name: 'Slide Toggle', route: '/slide-toggle'}, {name: 'Slider', route: '/slider'}, diff --git a/src/dev-app/dev-app/dev-app-module.ts b/src/dev-app/dev-app/dev-app-module.ts index 707148465808..d09902019f09 100644 --- a/src/dev-app/dev-app/dev-app-module.ts +++ b/src/dev-app/dev-app/dev-app-module.ts @@ -14,6 +14,7 @@ import {MatListModule} from '@angular/material/list'; import {MatSidenavModule} from '@angular/material/sidenav'; import {MatToolbarModule} from '@angular/material/toolbar'; import {RouterModule} from '@angular/router'; + import {DevApp404} from './dev-app-404'; import {DevAppHome} from './dev-app-home'; import {DevAppLayout} from './dev-app-layout'; diff --git a/src/dev-app/dev-app/routes.ts b/src/dev-app/dev-app/routes.ts index 09d2a91e3c29..e3b8a26659e8 100644 --- a/src/dev-app/dev-app/routes.ts +++ b/src/dev-app/dev-app/routes.ts @@ -39,10 +39,7 @@ export const DEV_APP_ROUTES: Routes = [ path: 'focus-origin', loadChildren: 'focus-origin/focus-origin-demo-module#FocusOriginDemoModule' }, - { - path: 'focus-trap', - loadChildren: 'focus-trap/focus-trap-demo-module#FocusTrapDemoModule' - }, + {path: 'focus-trap', loadChildren: 'focus-trap/focus-trap-demo-module#FocusTrapDemoModule'}, {path: 'google-map', loadChildren: 'google-map/google-map-demo-module#GoogleMapDemoModule'}, {path: 'grid-list', loadChildren: 'grid-list/grid-list-demo-module#GridListDemoModule'}, {path: 'icon', loadChildren: 'icon/icon-demo-module#IconDemoModule'}, @@ -123,6 +120,7 @@ export const DEV_APP_ROUTES: Routes = [ path: 'youtube-player', loadChildren: 'youtube-player/youtube-player-demo-module#YouTubePlayerDemoModule', }, + {path: 'selection', loadChildren: 'selection/selection-demo-module#SelectionDemoModule'}, {path: 'examples', loadChildren: 'examples-page/examples-page-module#ExamplesPageModule'}, {path: '**', component: DevApp404}, ]; diff --git a/src/dev-app/example/example-list.ts b/src/dev-app/example/example-list.ts index c6c4e0e0a8fb..d71d8863f2f9 100644 --- a/src/dev-app/example/example-list.ts +++ b/src/dev-app/example/example-list.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Input} from '@angular/core'; -import {EXAMPLE_COMPONENTS} from '@angular/components-examples'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {EXAMPLE_COMPONENTS} from '@angular/components-examples'; +import {Component, Input} from '@angular/core'; + +console.log(EXAMPLE_COMPONENTS); /** Displays a set of components-examples in a mat-accordion. */ @Component({ @@ -60,8 +62,12 @@ export class ExampleList { @Input() ids: string[]; @Input() - get expandAll(): boolean { return this._expandAll; } - set expandAll(v: boolean) { this._expandAll = coerceBooleanProperty(v); } + get expandAll(): boolean { + return this._expandAll; + } + set expandAll(v: boolean) { + this._expandAll = coerceBooleanProperty(v); + } _expandAll: boolean; exampleComponents = EXAMPLE_COMPONENTS; diff --git a/src/dev-app/example/example-module.ts b/src/dev-app/example/example-module.ts index e7aca21a6ae3..03d206fbdfb9 100644 --- a/src/dev-app/example/example-module.ts +++ b/src/dev-app/example/example-module.ts @@ -7,12 +7,11 @@ */ import {CommonModule} from '@angular/common'; +import {ExampleModule as DocsExampleModule} from '@angular/components-examples'; import {NgModule} from '@angular/core'; import {MatExpansionModule} from '@angular/material/expansion'; -import {ExampleModule as DocsExampleModule} from '@angular/components-examples'; -import {Example} from './example'; - +import {Example} from './example'; import {ExampleList} from './example-list'; @NgModule({ diff --git a/src/dev-app/selection/BUILD.bazel b/src/dev-app/selection/BUILD.bazel new file mode 100644 index 000000000000..e3b3567853f5 --- /dev/null +++ b/src/dev-app/selection/BUILD.bazel @@ -0,0 +1,14 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_module") + +ng_module( + name = "selection", + srcs = glob(["**/*.ts"]), + deps = [ + "//src/components-examples/cdk-experimental/selection", + "//src/dev-app/example", + "@npm//@angular/forms", + "@npm//@angular/router", + ], +) diff --git a/src/dev-app/selection/selection-demo-module.ts b/src/dev-app/selection/selection-demo-module.ts new file mode 100644 index 000000000000..6c47adfd3d2e --- /dev/null +++ b/src/dev-app/selection/selection-demo-module.ts @@ -0,0 +1,25 @@ +/** + * @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 {CdkSelectionExamplesModule} from '@angular/components-examples/cdk-experimental/selection'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {RouterModule} from '@angular/router'; + +import {SelectionDemo} from './selection-demo'; + +@NgModule({ + imports: [ + CdkSelectionExamplesModule, + FormsModule, + RouterModule.forChild([{path: '', component: SelectionDemo}]), + ], + declarations: [SelectionDemo], +}) +export class SelectionDemoModule { +} diff --git a/src/dev-app/selection/selection-demo.ts b/src/dev-app/selection/selection-demo.ts new file mode 100644 index 000000000000..f2666fb1753b --- /dev/null +++ b/src/dev-app/selection/selection-demo.ts @@ -0,0 +1,21 @@ +/** + * @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 {Component} from '@angular/core'; + +@Component({ + template: ` +

CDK selection with a list

+ + +

CDK selection column and CDK row selection with CDK table

+ + `, +}) +export class SelectionDemo { +} From 8b279b54fb74088b0cc1ccde4952c88929d51d75 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Thu, 6 Feb 2020 15:24:35 -0800 Subject: [PATCH 2/7] Remove the test build rules Since there is no test added yet. (cherry picked from commit 5d5327c1965c3a671c0d5cd8715f9d7452afc6f4) --- src/cdk-experimental/selection/BUILD.bazel | 32 ---------------------- 1 file changed, 32 deletions(-) diff --git a/src/cdk-experimental/selection/BUILD.bazel b/src/cdk-experimental/selection/BUILD.bazel index d1556eaa44d7..1a87f4a0dfa6 100644 --- a/src/cdk-experimental/selection/BUILD.bazel +++ b/src/cdk-experimental/selection/BUILD.bazel @@ -19,35 +19,3 @@ ng_module( "@npm//rxjs", ], ) - -ng_test_library( - name = "unit_test_sources", - srcs = glob( - ["**/*.spec.ts"], - exclude = ["**/*.e2e.spec.ts"], - ), - deps = [ - ":selection", - ], -) - -ng_web_test_suite( - name = "unit_tests", - deps = [":unit_test_sources"], -) - -ng_e2e_test_library( - name = "e2e_test_sources", - srcs = glob(["**/*.e2e.spec.ts"]), - deps = [ - "//src/cdk/testing/private/e2e", - ], -) - -e2e_test_suite( - name = "e2e_tests", - deps = [ - ":e2e_test_sources", - "//src/cdk/testing/private/e2e", - ], -) From 1926ee2d9eefb21d6289866dc63482b520507a6f Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Thu, 13 Feb 2020 17:36:01 -0800 Subject: [PATCH 3/7] Address jelbourn's review comments --- src/cdk-experimental/selection/BUILD.bazel | 3 +- .../selection/row-selection.ts | 32 ++++---- src/cdk-experimental/selection/select-all.ts | 60 +++++++-------- .../selection/selection-column.ts | 23 +++--- .../selection/selection-set.ts | 28 ++++--- .../selection/selection-toggle.ts | 76 ++++++++++--------- src/cdk-experimental/selection/selection.ts | 51 ++++++------- .../cdk-selection-column-example.css | 2 +- .../cdk-selection-column-example.html | 2 +- .../cdk-selection-list-example.html | 10 +-- 10 files changed, 140 insertions(+), 147 deletions(-) diff --git a/src/cdk-experimental/selection/BUILD.bazel b/src/cdk-experimental/selection/BUILD.bazel index 1a87f4a0dfa6..65e8c1f30348 100644 --- a/src/cdk-experimental/selection/BUILD.bazel +++ b/src/cdk-experimental/selection/BUILD.bazel @@ -1,7 +1,6 @@ package(default_visibility = ["//visibility:public"]) -load("//src/e2e-app:test_suite.bzl", "e2e_test_suite") -load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite") +load("//tools:defaults.bzl", "ng_module") ng_module( name = "selection", diff --git a/src/cdk-experimental/selection/row-selection.ts b/src/cdk-experimental/selection/row-selection.ts index e1a94d105330..f2902febab9e 100644 --- a/src/cdk-experimental/selection/row-selection.ts +++ b/src/cdk-experimental/selection/row-selection.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, HostBinding, Input} from '@angular/core'; +import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion'; +import {Directive, Input} from '@angular/core'; import {CdkSelection} from './selection'; @@ -19,31 +20,24 @@ import {CdkSelection} from './selection'; */ @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 { - @Input() - get cdkRowSelectionValue(): T { - return this._value; - } - set cdkRowSelectionValue(value: T) { - this._value = value; - } - _value: T; + @Input('cdkRowSelectionValue') _value: T; - @Input() - get cdkRowSelectionIndex(): number|undefined { + @Input('cdkRowSelectionIndex') + get index(): number|undefined { return this._index; } - set cdkRowSelectionIndex(index: number|undefined) { - this._index = index; + set index(index: number|undefined) { + this._index = coerceNumberProperty(index); } _index?: number; - constructor(private readonly _selection: CdkSelection) {} + constructor(readonly _selection: CdkSelection) {} - @HostBinding('class.cdk-selected') - @HostBinding('attr.aria-selected') - get isSelected() { - return this._selection.isSelected(this._value, this._index); - } + static ngAcceptInputType_index: NumberInput; } diff --git a/src/cdk-experimental/selection/select-all.ts b/src/cdk-experimental/selection/select-all.ts index 95a197cc238d..3b5401969a7f 100644 --- a/src/cdk-experimental/selection/select-all.ts +++ b/src/cdk-experimental/selection/select-all.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Inject, OnDestroy, OnInit, Optional, Self} from '@angular/core'; +import {Directive, Inject, isDevMode, OnDestroy, OnInit, Optional, Self} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {BehaviorSubject, of as observableOf, Subject} from 'rxjs'; +import {Observable, of as observableOf, ReplaySubject} from 'rxjs'; import {switchMap, takeUntil} from 'rxjs/operators'; import {CdkSelection} from './selection'; @@ -32,14 +32,18 @@ export class CdkSelectAll 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$ = new BehaviorSubject(false); + readonly checked: Observable = 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$ = new BehaviorSubject(false); + readonly indeterminate: Observable = this._selection.change.pipe( + switchMap(() => observableOf(this._selection.isPartialSelected())), + ); /** * Toggles the select-all state. @@ -60,7 +64,7 @@ export class CdkSelectAll implements OnDestroy, OnInit { }); } - private readonly _destroyed$ = new Subject(); + private readonly _destroyed = new ReplaySubject(1); constructor( @Optional() private readonly _selection: CdkSelection, @@ -68,47 +72,35 @@ export class CdkSelectAll implements OnDestroy, OnInit { ControlValueAccessor[]) {} ngOnInit() { - if (!this._selection) { - throw new Error('CdkSelectAll: missing CdkSelection in the parent'); - } - - if (!this._selection.cdkSelectionMultiple) { - throw new Error('CdkSelectAll: CdkSelection must have cdkSelectionMultiple set to true'); - } - - this._selection.cdkSelectionChange - .pipe( - switchMap(() => observableOf(this._selection.isAllSelected())), - takeUntil(this._destroyed$), - ) - .subscribe((state) => { - this.checked$.next(state); - }); - - this._selection.cdkSelectionChange - .pipe( - switchMap(() => observableOf(this._selection.isPartialSelected())), - takeUntil(this._destroyed$), - ) - .subscribe((state) => { - this.indeterminate$.next(state); - }); + 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.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._destroyed.next(); + this._destroyed.complete(); } } diff --git a/src/cdk-experimental/selection/selection-column.ts b/src/cdk-experimental/selection/selection-column.ts index 6b160111acb7..399d4f8530cb 100644 --- a/src/cdk-experimental/selection/selection-column.ts +++ b/src/cdk-experimental/selection/selection-column.ts @@ -10,6 +10,7 @@ import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef, CdkTable} from '@angular/cdk import { Component, Input, + isDevMode, OnDestroy, OnInit, Optional, @@ -29,11 +30,11 @@ import {CdkSelection} from './selection'; template: ` - @@ -43,18 +44,18 @@ import {CdkSelection} from './selection'; [cdkSelectionToggleValue]="row" [cdkSelectionToggleIndex]="i" (click)="toggler.toggle()" - [checked]="toggler.checked$ | async"> + [checked]="toggler.checked | async"> `, }) export class CdkSelectionColumn implements OnInit, OnDestroy { /** Column name that should be used to reference this column. */ - @Input() - get cdkSelectionColumnName(): string { + @Input('cdkSelectionColumnName') + get name(): string { return this._name; } - set cdkSelectionColumnName(name: string) { + set name(name: string) { this._name = name; this.syncColumnDefName(); @@ -71,8 +72,8 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { ) {} ngOnInit() { - if (!this.selection) { - throw new Error('CdkSelectionColumn: missing CdkSelection in the parent'); + if (!this.selection && isDevMode()) { + throw Error('CdkSelectionColumn: missing CdkSelection in the parent'); } this.syncColumnDefName(); @@ -82,7 +83,9 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { this._columnDef.headerCell = this._headerCell; this.table.addColumnDef(this._columnDef); } else { - throw new Error('CdkSelectionColumn: missing parent table'); + if (isDevMode()) { + throw Error('CdkSelectionColumn: missing parent table'); + } } } diff --git a/src/cdk-experimental/selection/selection-set.ts b/src/cdk-experimental/selection/selection-set.ts index a807d8b3e0e3..179b950b52f3 100644 --- a/src/cdk-experimental/selection/selection-set.ts +++ b/src/cdk-experimental/selection/selection-set.ts @@ -6,14 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {TrackByFunction} from '@angular/core'; +import {isDevMode, TrackByFunction} from '@angular/core'; import {Subject} from 'rxjs'; +/** + * Maintains a set of selected values. One or more values can be added to or removed from the + * selection. + */ interface TrackBySelection { isSelected(value: SelectableWithIndex): boolean; select(...values: Array>): void; deselect(...values: Array>): void; - changed$: Subject>; + changed: Subject>; } /** @@ -42,7 +46,7 @@ export interface SelectionChange { */ export class SelectionSet implements TrackBySelection { private selectionMap = new Map>, SelectableWithIndex>(); - changed$ = new Subject>(); + changed = new Subject>(); constructor(private _multiple = false, private _trackByFn?: TrackByFunction) {} @@ -51,8 +55,8 @@ export class SelectionSet implements TrackBySelection { } select(...selects: Array>) { - if (!this._multiple && selects.length > 1) { - throw new Error('SelectionSet: not multiple selection'); + if (!this._multiple && selects.length > 1 && isDevMode()) { + throw Error('SelectionSet: not multiple selection'); } const before = this._getCurrentSelection(); @@ -73,12 +77,12 @@ export class SelectionSet implements TrackBySelection { const after = this._getCurrentSelection(); - this.changed$.next({before, after}); + this.changed.next({before, after}); } deselect(...selects: Array>) { - if (!this._multiple && selects.length > 1) { - throw new Error('SelectionSet: not multiple selection'); + if (!this._multiple && selects.length > 1 && isDevMode()) { + throw Error('SelectionSet: not multiple selection'); } const before = this._getCurrentSelection(); @@ -94,7 +98,7 @@ export class SelectionSet implements TrackBySelection { } const after = this._getCurrentSelection(); - this.changed$.next({before, after}); + this.changed.next({before, after}); } private _markSelected(key: T|ReturnType>, toSelect: SelectableWithIndex) { @@ -110,11 +114,11 @@ export class SelectionSet implements TrackBySelection { return select.value; } - if (select.index == null) { - throw new Error('SelectionSet: index required when trackByFn is used.'); + if (select.index == null && isDevMode()) { + throw Error('SelectionSet: index required when trackByFn is used.'); } - return this._trackByFn(select.index, select.value); + return this._trackByFn(select.index!, select.value); } private _getCurrentSelection(): Array> { diff --git a/src/cdk-experimental/selection/selection-toggle.ts b/src/cdk-experimental/selection/selection-toggle.ts index 790162a35c39..af8af9609b90 100644 --- a/src/cdk-experimental/selection/selection-toggle.ts +++ b/src/cdk-experimental/selection/selection-toggle.ts @@ -6,10 +6,20 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, Inject, Input, OnDestroy, OnInit, Optional, Self} from '@angular/core'; +import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion'; +import { + Directive, + Inject, + Input, + isDevMode, + OnDestroy, + OnInit, + Optional, + Self +} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {BehaviorSubject, of as observableOf, Subject} from 'rxjs'; -import {distinctUntilChanged, switchMap, takeUntil} from 'rxjs/operators'; +import {BehaviorSubject, Observable, of as observableOf, ReplaySubject, Subject} from 'rxjs'; +import {distinctUntilChanged, startWith, switchMap, takeUntil} from 'rxjs/operators'; import {CdkSelection} from './selection'; @@ -29,34 +39,30 @@ import {CdkSelection} from './selection'; }) export class CdkSelectionToggle implements OnDestroy, OnInit { /** The value that is associated with the toggle */ - @Input() - get cdkSelectionToggleValue(): T { - return this._value; - } - set cdkSelectionToggleValue(value: T) { - this._value = value; - } - private _value: T; + @Input('cdkSelectionToggleValue') private _value: T; /** The index of the value in the list. Required when used with `trackBy` */ - @Input() - get cdkSelectionToggleIndex(): number|undefined { + @Input('cdkSelectionToggleIndex') + get index(): number|undefined { return this._index; } - set cdkSelectionToggleIndex(index: number|undefined) { - this._index = index; + set index(index: number|undefined) { + this._index = coerceNumberProperty(index); } private _index?: number; /** The checked state of the selection toggle */ - readonly checked$ = new BehaviorSubject(false); + readonly checked: Observable = this._selection.change.pipe( + switchMap(() => observableOf(this._isSelected())), + distinctUntilChanged(), + ); /** Toggles the selection */ toggle() { this._selection.toggleSelection(this._value, this._index); } - private _destroyed$ = new Subject(); + private _destroyed = new ReplaySubject(1); constructor( @Optional() private _selection: CdkSelection, @@ -65,10 +71,22 @@ export class CdkSelectionToggle implements OnDestroy, OnInit { ) {} ngOnInit() { - if (!this._selection) { - throw new Error('CdkSelectAll: missing CdkSelection in the parent'); + this._assertValidParentSelection(); + this._configureControlValueAccessor(); + } + + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + } + + private _assertValidParentSelection() { + if (!this._selection && isDevMode()) { + throw Error('CdkSelectAll: missing CdkSelection in the parent'); } + } + private _configureControlValueAccessor() { if (this._controlValueAccessors && this._controlValueAccessors.length) { this._controlValueAccessors[0].registerOnChange((e: unknown) => { if (typeof e === 'boolean') { @@ -76,29 +94,15 @@ export class CdkSelectionToggle implements OnDestroy, OnInit { } }); - this.checked$.pipe(takeUntil(this._destroyed$)).subscribe((state) => { + this.checked.pipe(takeUntil(this._destroyed)).subscribe((state) => { this._controlValueAccessors[0].writeValue(state); }); } - - this.checked$.next(this._isSelected()); - this._selection.cdkSelectionChange - .pipe( - switchMap(() => observableOf(this._isSelected())), - distinctUntilChanged(), - takeUntil(this._destroyed$), - ) - .subscribe((state: boolean) => { - this.checked$.next(state); - }); - } - - ngOnDestroy() { - this._destroyed$.next(); - this._destroyed$.complete(); } private _isSelected(): boolean { return this._selection.isSelected(this._value, this._index); } + + static ngAcceptInputType_index: NumberInput; } diff --git a/src/cdk-experimental/selection/selection.ts b/src/cdk-experimental/selection/selection.ts index ec6c4177b755..ca683bcf9cad 100644 --- a/src/cdk-experimental/selection/selection.ts +++ b/src/cdk-experimental/selection/selection.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {CollectionViewer, DataSource, isDataSource, ListRange} from '@angular/cdk/collections'; import { AfterContentChecked, Directive, EventEmitter, Input, + isDevMode, OnDestroy, OnInit, Output, @@ -47,27 +48,20 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } private _dataSource: TableDataSource; - @Input() - get trackBy(): TrackByFunction { - return this._trackByFn; - } - set trackBy(fn: TrackByFunction) { - this._trackByFn = fn; - } - private _trackByFn: TrackByFunction; + @Input('trackBy') private _trackByFn: TrackByFunction; /** Whether to support multiple selection */ - @Input() - get cdkSelectionMultiple(): boolean { + @Input('cdkSelectionMultiple') + get multiple(): boolean { return this._multiple; } - set cdkSelectionMultiple(multiple: boolean) { + set multiple(multiple: boolean) { this._multiple = coerceBooleanProperty(multiple); } private _multiple: boolean; /** Emits when selection changes. */ - @Output() cdkSelectionChange = new EventEmitter>(); + @Output('cdkSelectionChange') change = new EventEmitter>(); /** Latest data provided by the data source. */ private _data: T[]|readonly T[]; @@ -75,13 +69,14 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV /** Subscription that listens for the data provided by the data source. */ private _renderChangeSubscription: Subscription|null; - private _destroyed$ = new ReplaySubject(1); + private _destroyed = new ReplaySubject(1); private _selection: SelectionSet; private _switchDataSource(dataSource: TableDataSource) { this._data = []; + // TODO: Move this logic to a shared function in `cdk/collections`. if (isDataSource(this._dataSource)) { this._dataSource.disconnect(this); } @@ -109,21 +104,21 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV dataStream = observableOf(this._dataSource); } - if (dataStream == null) { - throw new Error('Unknown data source'); + if (dataStream == null && isDevMode()) { + throw Error('Unknown data source'); } this._renderChangeSubscription = - dataStream.pipe(takeUntil(this._destroyed$)).subscribe((data) => { + dataStream!.pipe(takeUntil(this._destroyed)).subscribe((data) => { this._data = data || []; }); } ngOnInit() { this._selection = new SelectionSet(this._multiple, this._trackByFn); - this._selection.changed$.pipe(takeUntil(this._destroyed$)).subscribe((change) => { + this._selection.changed.pipe(takeUntil(this._destroyed)).subscribe((change) => { this.updateSelectAllState(); - this.cdkSelectionChange.emit(change); + this.change.emit(change); }); } @@ -134,8 +129,8 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } ngOnDestroy() { - this._destroyed$.next(); - this._destroyed$.complete(); + this._destroyed.next(); + this._destroyed.complete(); if (isDataSource(this._dataSource)) { this._dataSource.disconnect(this); @@ -144,8 +139,8 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV /** Toggles selection for a given value. `index` is required if `trackBy` is used. */ toggleSelection(value: T, index?: number) { - if (this.trackBy && index == null) { - throw new Error('CdkSelection: index required when trackBy is used'); + if (this._trackByFn && index == null && isDevMode()) { + throw Error('CdkSelection: index required when trackBy is used'); } if (this.isSelected(value, index)) { @@ -160,8 +155,8 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV * values are selected, de-select all values. */ toggleSelectAll() { - if (!this._multiple) { - throw new Error('CdkSelection: multiple selection not enabled'); + if (!this._multiple && isDevMode()) { + throw Error('CdkSelection: multiple selection not enabled'); } if (this.selectAllState === 'none') { @@ -173,8 +168,8 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV /** Checks whether a value is selected. `index` is required if `trackBy` is used. */ isSelected(value: T, index?: number) { - if (this.trackBy && index == null) { - throw new Error('CdkSelection: index required when trackBy is used'); + if (this._trackByFn && index == null && isDevMode()) { + throw Error('CdkSelection: index required when trackBy is used'); } return this._selection.isSelected({value, index}); @@ -220,6 +215,8 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } selectAllState: SelectAllState = 'none'; + + static ngAcceptInputType_multiple: BooleanInput; } type SelectAllState = 'all'|'none'|'partial'; diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css index 134e458661a4..99b8307a1da4 100644 --- a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css @@ -1,4 +1,4 @@ -table { +.example-table { border-collapse: collapse; } diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html index 14412783c932..ad8231d5c0cf 100644 --- a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.html @@ -1,5 +1,5 @@ Selected: {{selected}} - +
diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html index 64a9e5758306..8877ac3e1b25 100644 --- a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.html @@ -2,12 +2,12 @@

native input

Selected: {{selected1}}
  • + [checked]="toggler.checked | async" (click)="toggler.toggle()"> {{item}}
@@ -15,7 +15,7 @@

native input

mat-checkbox

Selected: {{selected2}}
    - +
  • {{item}} @@ -34,7 +34,7 @@

    Single select with mat-checkbox

    with trackBy

    Selected: {{selected4}}
      - +
    • {{item}} From 2fefe740cbd29284ba9ced83cf386452c6074405 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Tue, 18 Feb 2020 15:38:01 -0800 Subject: [PATCH 4/7] Address review comments #2 --- .../selection/row-selection.ts | 16 ++++++--------- src/cdk-experimental/selection/select-all.ts | 6 +++--- .../selection/selection-column.ts | 2 +- .../selection/selection-toggle.ts | 20 ++++++++----------- src/cdk-experimental/selection/selection.ts | 12 +++++------ 5 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/cdk-experimental/selection/row-selection.ts b/src/cdk-experimental/selection/row-selection.ts index f2902febab9e..005b1fb2d82e 100644 --- a/src/cdk-experimental/selection/row-selection.ts +++ b/src/cdk-experimental/selection/row-selection.ts @@ -21,21 +21,17 @@ import {CdkSelection} from './selection'; @Directive({ selector: '[cdkRowSelection]', host: { - '[class.cdk-selected]': '_selection.isSelected(this._value, this._index)', - '[attr.aria-selected]': '_selection.isSelected(this._value, this._index)', + '[class.cdk-selected]': '_selection.isSelected(this.value, this.index)', + '[attr.aria-selected]': '_selection.isSelected(this.value, this.index)', }, }) export class CdkRowSelection { - @Input('cdkRowSelectionValue') _value: T; + @Input('cdkRowSelectionValue') value: T; @Input('cdkRowSelectionIndex') - get index(): number|undefined { - return this._index; - } - set index(index: number|undefined) { - this._index = coerceNumberProperty(index); - } - _index?: number; + get index(): number|undefined { return this._index; } + set index(index: number|undefined) { this._index = coerceNumberProperty(index); } + private _index?: number; constructor(readonly _selection: CdkSelection) {} diff --git a/src/cdk-experimental/selection/select-all.ts b/src/cdk-experimental/selection/select-all.ts index 3b5401969a7f..0b4e9e411556 100644 --- a/src/cdk-experimental/selection/select-all.ts +++ b/src/cdk-experimental/selection/select-all.ts @@ -8,7 +8,7 @@ import {Directive, Inject, isDevMode, OnDestroy, OnInit, Optional, Self} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {Observable, of as observableOf, ReplaySubject} from 'rxjs'; +import {Observable, of as observableOf, Subject} from 'rxjs'; import {switchMap, takeUntil} from 'rxjs/operators'; import {CdkSelection} from './selection'; @@ -48,7 +48,7 @@ export class CdkSelectAll implements OnDestroy, OnInit { /** * 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 , the parameter is required for the + * using with a native ``, the parameter is required for the * indeterminate state to work properly. */ toggle(event?: MouseEvent) { @@ -64,7 +64,7 @@ export class CdkSelectAll implements OnDestroy, OnInit { }); } - private readonly _destroyed = new ReplaySubject(1); + private readonly _destroyed = new Subject(); constructor( @Optional() private readonly _selection: CdkSelection, diff --git a/src/cdk-experimental/selection/selection-column.ts b/src/cdk-experimental/selection/selection-column.ts index 399d4f8530cb..1a25e3d8da47 100644 --- a/src/cdk-experimental/selection/selection-column.ts +++ b/src/cdk-experimental/selection/selection-column.ts @@ -60,7 +60,7 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { this.syncColumnDefName(); } - _name: string; + private _name: string; @ViewChild(CdkColumnDef, {static: true}) private readonly _columnDef: CdkColumnDef; @ViewChild(CdkCellDef, {static: true}) private readonly _cell: CdkCellDef; diff --git a/src/cdk-experimental/selection/selection-toggle.ts b/src/cdk-experimental/selection/selection-toggle.ts index af8af9609b90..f73cc22b6c20 100644 --- a/src/cdk-experimental/selection/selection-toggle.ts +++ b/src/cdk-experimental/selection/selection-toggle.ts @@ -18,8 +18,8 @@ import { Self } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {BehaviorSubject, Observable, of as observableOf, ReplaySubject, Subject} from 'rxjs'; -import {distinctUntilChanged, startWith, switchMap, takeUntil} from 'rxjs/operators'; +import {Observable, of as observableOf, Subject} from 'rxjs'; +import {distinctUntilChanged, switchMap, takeUntil} from 'rxjs/operators'; import {CdkSelection} from './selection'; @@ -39,16 +39,12 @@ import {CdkSelection} from './selection'; }) export class CdkSelectionToggle implements OnDestroy, OnInit { /** The value that is associated with the toggle */ - @Input('cdkSelectionToggleValue') private _value: T; + @Input('cdkSelectionToggleValue') value: T; /** The index of the value in the list. Required when used with `trackBy` */ @Input('cdkSelectionToggleIndex') - get index(): number|undefined { - return this._index; - } - set index(index: number|undefined) { - this._index = coerceNumberProperty(index); - } + get index(): number|undefined { return this._index; } + set index(index: number|undefined) { this._index = coerceNumberProperty(index); } private _index?: number; /** The checked state of the selection toggle */ @@ -59,10 +55,10 @@ export class CdkSelectionToggle implements OnDestroy, OnInit { /** Toggles the selection */ toggle() { - this._selection.toggleSelection(this._value, this._index); + this._selection.toggleSelection(this.value, this.index); } - private _destroyed = new ReplaySubject(1); + private _destroyed = new Subject(); constructor( @Optional() private _selection: CdkSelection, @@ -101,7 +97,7 @@ export class CdkSelectionToggle implements OnDestroy, OnInit { } private _isSelected(): boolean { - return this._selection.isSelected(this._value, this._index); + return this._selection.isSelected(this.value, this.index); } static ngAcceptInputType_index: NumberInput; diff --git a/src/cdk-experimental/selection/selection.ts b/src/cdk-experimental/selection/selection.ts index ca683bcf9cad..15e9cf1f5c61 100644 --- a/src/cdk-experimental/selection/selection.ts +++ b/src/cdk-experimental/selection/selection.ts @@ -19,7 +19,7 @@ import { Output, TrackByFunction } from '@angular/core'; -import {Observable, of as observableOf, ReplaySubject, Subscription} from 'rxjs'; +import {Observable, of as observableOf, Subject, Subscription} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {SelectableWithIndex, SelectionChange, SelectionSet} from './selection-set'; @@ -48,7 +48,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } private _dataSource: TableDataSource; - @Input('trackBy') private _trackByFn: TrackByFunction; + @Input('trackBy') trackByFn: TrackByFunction; /** Whether to support multiple selection */ @Input('cdkSelectionMultiple') @@ -69,7 +69,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV /** Subscription that listens for the data provided by the data source. */ private _renderChangeSubscription: Subscription|null; - private _destroyed = new ReplaySubject(1); + private _destroyed = new Subject(); private _selection: SelectionSet; @@ -115,7 +115,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } ngOnInit() { - this._selection = new SelectionSet(this._multiple, this._trackByFn); + this._selection = new SelectionSet(this._multiple, this.trackByFn); this._selection.changed.pipe(takeUntil(this._destroyed)).subscribe((change) => { this.updateSelectAllState(); this.change.emit(change); @@ -139,7 +139,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV /** Toggles selection for a given value. `index` is required if `trackBy` is used. */ toggleSelection(value: T, index?: number) { - if (this._trackByFn && index == null && isDevMode()) { + if (this.trackByFn && index == null && isDevMode()) { throw Error('CdkSelection: index required when trackBy is used'); } @@ -168,7 +168,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV /** Checks whether a value is selected. `index` is required if `trackBy` is used. */ isSelected(value: T, index?: number) { - if (this._trackByFn && index == null && isDevMode()) { + if (this.trackByFn && index == null && isDevMode()) { throw Error('CdkSelection: index required when trackBy is used'); } From 9794c6199a7e07a7c344e0d9040871f691595a23 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Tue, 18 Feb 2020 22:32:01 -0800 Subject: [PATCH 5/7] fix check-rollup-globals --- src/cdk-experimental/selection/public-api.ts | 1 + .../cdk-selection-column/cdk-selection-column-example.ts | 2 +- .../selection/cdk-selection-list/cdk-selection-list-example.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cdk-experimental/selection/public-api.ts b/src/cdk-experimental/selection/public-api.ts index 8cae343cd78d..423a022570cd 100644 --- a/src/cdk-experimental/selection/public-api.ts +++ b/src/cdk-experimental/selection/public-api.ts @@ -11,4 +11,5 @@ export * from './select-all'; export * from './selection-toggle'; export * from './selection-column'; export * from './row-selection'; +export * from './selection-set'; export * from './selection-module'; diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts index cb2146c54a33..3a17fe9fc6a0 100644 --- a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts @@ -1,4 +1,4 @@ -import {SelectionChange} from '@angular/cdk-experimental/selection/selection-set'; +import {SelectionChange} from '@angular/cdk-experimental/selection'; import {Component, OnDestroy} from '@angular/core'; import {ReplaySubject} from 'rxjs'; diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts index 3e4d6d45a897..d7bd2fb80af7 100644 --- a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts @@ -1,4 +1,4 @@ -import {SelectionChange} from '@angular/cdk-experimental/selection/selection-set'; +import {SelectionChange} from '@angular/cdk-experimental/selection'; import {Component, OnDestroy} from '@angular/core'; import {ReplaySubject} from 'rxjs'; From bea5c3489f1945d6cd071938c4f6d9a49cd9c799 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Wed, 19 Feb 2020 10:53:35 -0800 Subject: [PATCH 6/7] add `CODEOWNERS` entries for `cdk-experimental/selection` --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 96e3fea241bc..601245761f1d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -120,6 +120,7 @@ /src/cdk-experimental/dialog/** @jelbourn @crisbeto /src/cdk-experimental/popover-edit/** @kseamon @andrewseguin /src/cdk-experimental/scrolling/** @mmalerba +/src/cdk-experimental/selection/** @yifange @jelbourn # Docs examples & guides /guides/** @jelbourn @@ -196,6 +197,7 @@ /src/dev-app/typography/** @crisbeto /src/dev-app/virtual-scroll/** @mmalerba /src/dev-app/youtube-player/** @nathantate +/src/dev-app/selection/** @yifange @jelbourn # E2E app /src/e2e-app/* @jelbourn From 6f6cc11ebbe070f43736a472b70e94cb9d637cf2 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Wed, 19 Feb 2020 13:10:32 -0800 Subject: [PATCH 7/7] fix tslint --- .../selection/selection-column.ts | 20 +++++++++++-------- .../selection/selection-set.ts | 12 +++++------ src/cdk-experimental/selection/selection.ts | 12 +++++------ .../cdk-selection-column-example.ts | 6 +++--- .../cdk-selection-list-example.ts | 6 +++--- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/cdk-experimental/selection/selection-column.ts b/src/cdk-experimental/selection/selection-column.ts index 1a25e3d8da47..e840feb54d8f 100644 --- a/src/cdk-experimental/selection/selection-column.ts +++ b/src/cdk-experimental/selection/selection-column.ts @@ -15,6 +15,8 @@ import { OnInit, Optional, ViewChild, + ChangeDetectionStrategy, + ViewEncapsulation, } from '@angular/core'; import {CdkSelection} from './selection'; @@ -48,6 +50,8 @@ import {CdkSelection} from './selection'; `, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, }) export class CdkSelectionColumn implements OnInit, OnDestroy { /** Column name that should be used to reference this column. */ @@ -58,7 +62,7 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { set name(name: string) { this._name = name; - this.syncColumnDefName(); + this._syncColumnDefName(); } private _name: string; @@ -67,7 +71,7 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { @ViewChild(CdkHeaderCellDef, {static: true}) private readonly _headerCell: CdkHeaderCellDef; constructor( - @Optional() private table: CdkTable, + @Optional() private _table: CdkTable, @Optional() readonly selection: CdkSelection, ) {} @@ -76,12 +80,12 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { throw Error('CdkSelectionColumn: missing CdkSelection in the parent'); } - this.syncColumnDefName(); + this._syncColumnDefName(); - if (this.table) { + if (this._table) { this._columnDef.cell = this._cell; this._columnDef.headerCell = this._headerCell; - this.table.addColumnDef(this._columnDef); + this._table.addColumnDef(this._columnDef); } else { if (isDevMode()) { throw Error('CdkSelectionColumn: missing parent table'); @@ -90,12 +94,12 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { } ngOnDestroy() { - if (this.table) { - this.table.removeColumnDef(this._columnDef); + if (this._table) { + this._table.removeColumnDef(this._columnDef); } } - private syncColumnDefName() { + private _syncColumnDefName() { if (this._columnDef) { this._columnDef.name = this._name; } diff --git a/src/cdk-experimental/selection/selection-set.ts b/src/cdk-experimental/selection/selection-set.ts index 179b950b52f3..696da173f0a1 100644 --- a/src/cdk-experimental/selection/selection-set.ts +++ b/src/cdk-experimental/selection/selection-set.ts @@ -45,13 +45,13 @@ export interface SelectionChange { * expected to be set when calling `isSelected`, `select` and `deselect`. */ export class SelectionSet implements TrackBySelection { - private selectionMap = new Map>, SelectableWithIndex>(); + private _selectionMap = new Map>, SelectableWithIndex>(); changed = new Subject>(); constructor(private _multiple = false, private _trackByFn?: TrackByFunction) {} isSelected(value: SelectableWithIndex): boolean { - return this.selectionMap.has(this._getTrackedByValue(value)); + return this._selectionMap.has(this._getTrackedByValue(value)); } select(...selects: Array>) { @@ -62,7 +62,7 @@ export class SelectionSet implements TrackBySelection { const before = this._getCurrentSelection(); if (!this._multiple) { - this.selectionMap.clear(); + this._selectionMap.clear(); } const toSelect: Array> = []; @@ -102,11 +102,11 @@ export class SelectionSet implements TrackBySelection { } private _markSelected(key: T|ReturnType>, toSelect: SelectableWithIndex) { - this.selectionMap.set(key, toSelect); + this._selectionMap.set(key, toSelect); } private _markDeselected(key: T|ReturnType>) { - this.selectionMap.delete(key); + this._selectionMap.delete(key); } private _getTrackedByValue(select: SelectableWithIndex) { @@ -122,6 +122,6 @@ export class SelectionSet implements TrackBySelection { } private _getCurrentSelection(): Array> { - return Array.from(this.selectionMap.values()); + return Array.from(this._selectionMap.values()); } } diff --git a/src/cdk-experimental/selection/selection.ts b/src/cdk-experimental/selection/selection.ts index 15e9cf1f5c61..a82c8cf8bb5b 100644 --- a/src/cdk-experimental/selection/selection.ts +++ b/src/cdk-experimental/selection/selection.ts @@ -117,7 +117,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV ngOnInit() { this._selection = new SelectionSet(this._multiple, this.trackByFn); this._selection.changed.pipe(takeUntil(this._destroyed)).subscribe((change) => { - this.updateSelectAllState(); + this._updateSelectAllState(); this.change.emit(change); }); } @@ -160,9 +160,9 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } if (this.selectAllState === 'none') { - this.selectAll(); + this._selectAll(); } else { - this.clearAll(); + this._clearAll(); } } @@ -186,7 +186,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV this._data.some((value, index) => this._selection.isSelected({value, index})); } - private selectAll() { + private _selectAll() { const toSelect: Array> = []; this._data.forEach((value, index) => { toSelect.push({value, index}); @@ -195,7 +195,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV this._selection.select(...toSelect); } - private clearAll() { + private _clearAll() { const toDeselect: Array> = []; this._data.forEach((value, index) => { toDeselect.push({value, index}); @@ -204,7 +204,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV this._selection.deselect(...toDeselect); } - private updateSelectAllState() { + private _updateSelectAllState() { if (this.isAllSelected()) { this.selectAllState = 'all'; } else if (this.isPartialSelected()) { diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts index 3a17fe9fc6a0..6c5d8357d607 100644 --- a/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.ts @@ -11,15 +11,15 @@ import {ReplaySubject} from 'rxjs'; styleUrls: ['cdk-selection-column-example.css'], }) export class CdkSelectionColumnExample implements OnDestroy { - private readonly destroyed$ = new ReplaySubject(1); + private readonly _destroyed = new ReplaySubject(1); displayedColumns: string[] = ['select', 'position', 'name', 'weight', 'symbol']; dataSource = ELEMENT_DATA; selected: string[] = []; ngOnDestroy() { - this.destroyed$.next(); - this.destroyed$.complete(); + this._destroyed.next(); + this._destroyed.complete(); } selectionChanged(event: SelectionChange) { diff --git a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts index d7bd2fb80af7..5b0f3f65fe09 100644 --- a/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-list/cdk-selection-list-example.ts @@ -10,7 +10,7 @@ import {ReplaySubject} from 'rxjs'; templateUrl: 'cdk-selection-list-example.html', }) export class CdkSelectionListExample implements OnDestroy { - private readonly destroyed$ = new ReplaySubject(1); + private readonly _destroyed = new ReplaySubject(1); data = ELEMENT_NAMES; @@ -20,8 +20,8 @@ export class CdkSelectionListExample implements OnDestroy { selected4: string[] = []; ngOnDestroy() { - this.destroyed$.next(); - this.destroyed$.complete(); + this._destroyed.next(); + this._destroyed.complete(); } getCurrentSelected(event: SelectionChange) {