From 99c7e2c1984329c4e6b3094140220bdc3e240142 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Thu, 20 Feb 2020 09:18:40 -0800 Subject: [PATCH 1/4] feat(cdk-experimental/selection): add selection state to a list of items (#18424) --- .github/CODEOWNERS | 2 + src/cdk-experimental/config.bzl | 1 + src/cdk-experimental/selection/BUILD.bazel | 20 ++ src/cdk-experimental/selection/index.ts | 10 + src/cdk-experimental/selection/public-api.ts | 15 ++ .../selection/row-selection.ts | 39 +++ src/cdk-experimental/selection/select-all.ts | 106 +++++++++ .../selection/selection-column.ts | 107 +++++++++ .../selection/selection-module.ts | 40 ++++ .../selection/selection-set.ts | 127 ++++++++++ .../selection/selection-toggle.ts | 104 ++++++++ src/cdk-experimental/selection/selection.ts | 223 ++++++++++++++++++ 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 | 4 +- src/dev-app/selection/BUILD.bazel | 14 ++ .../selection/selection-demo-module.ts | 25 ++ src/dev-app/selection/selection-demo.ts | 21 ++ 29 files changed, 1131 insertions(+), 11 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/.github/CODEOWNERS b/.github/CODEOWNERS index 4170974ada74..e40a61849031 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -132,6 +132,7 @@ /src/cdk-experimental/popover-edit/** @kseamon @andrewseguin /src/cdk-experimental/scrolling/** @mmalerba /src/cdk-experimental/listbox/** @nielsr98 @jelbourn +/src/cdk-experimental/selection/** @yifange @jelbourn # Docs examples & guides /guides/** @jelbourn @@ -215,6 +216,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 diff --git a/src/cdk-experimental/config.bzl b/src/cdk-experimental/config.bzl index ca3ffb5f404f..e69d1970a744 100644 --- a/src/cdk-experimental/config.bzl +++ b/src/cdk-experimental/config.bzl @@ -7,6 +7,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [ "listbox", "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..65e8c1f30348 --- /dev/null +++ b/src/cdk-experimental/selection/BUILD.bazel @@ -0,0 +1,20 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_module") + +ng_module( + name = "selection", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + module_name = "@angular/cdk-experimental/selection", + deps = [ + "//src/cdk/coercion", + "//src/cdk/collections", + "//src/cdk/table", + "@npm//@angular/core", + "@npm//@angular/forms", + "@npm//rxjs", + ], +) 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..423a022570cd --- /dev/null +++ b/src/cdk-experimental/selection/public-api.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './selection'; +export * from './select-all'; +export * from './selection-toggle'; +export * from './selection-column'; +export * from './row-selection'; +export * from './selection-set'; +export * from './selection-module'; diff --git a/src/cdk-experimental/selection/row-selection.ts b/src/cdk-experimental/selection/row-selection.ts new file mode 100644 index 000000000000..005b1fb2d82e --- /dev/null +++ b/src/cdk-experimental/selection/row-selection.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion'; +import {Directive, Input} from '@angular/core'; + +import {CdkSelection} from './selection'; + +/** + * Applies `cdk-selected` class and `aria-selected` to an element. + * + * Must be used within a parent `CdkSelection` directive. + * Must be provided with the value. The index is required if `trackBy` is used on the `CdkSelection` + * directive. + */ +@Directive({ + selector: '[cdkRowSelection]', + host: { + '[class.cdk-selected]': '_selection.isSelected(this.value, this.index)', + '[attr.aria-selected]': '_selection.isSelected(this.value, this.index)', + }, +}) +export class CdkRowSelection { + @Input('cdkRowSelectionValue') value: T; + + @Input('cdkRowSelectionIndex') + get index(): number|undefined { return this._index; } + set index(index: number|undefined) { this._index = coerceNumberProperty(index); } + private _index?: number; + + constructor(readonly _selection: CdkSelection) {} + + static ngAcceptInputType_index: NumberInput; +} diff --git a/src/cdk-experimental/selection/select-all.ts b/src/cdk-experimental/selection/select-all.ts new file mode 100644 index 000000000000..0b4e9e411556 --- /dev/null +++ b/src/cdk-experimental/selection/select-all.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, Inject, isDevMode, OnDestroy, OnInit, Optional, Self} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {Observable, of as observableOf, Subject} from 'rxjs'; +import {switchMap, takeUntil} from 'rxjs/operators'; + +import {CdkSelection} from './selection'; + +/** + * Makes the element a select-all toggle. + * + * Must be used within a parent `CdkSelection` directive. It toggles the selection states + * of all the selection toggles connected with the `CdkSelection` directive. + * If the element implements `ControlValueAccessor`, e.g. `MatCheckbox`, the directive + * automatically connects it with the select-all state provided by the `CdkSelection` directive. If + * not, use `checked$` to get the checked state, `indeterminate$` to get the indeterminate state, + * and `toggle()` to change the selection state. + */ +@Directive({ + selector: '[cdkSelectAll]', + exportAs: 'cdkSelectAll', +}) +export class CdkSelectAll implements OnDestroy, OnInit { + /** + * The checked state of the toggle. + * Resolves to `true` if all the values are selected, `false` if no value is selected. + */ + readonly checked: Observable = this._selection.change.pipe( + switchMap(() => observableOf(this._selection.isAllSelected())), + ); + + /** + * The indeterminate state of the toggle. + * Resolves to `true` if part (not all) of the values are selected, `false` if all values or no + * value at all are selected. + */ + readonly indeterminate: Observable = this._selection.change.pipe( + switchMap(() => observableOf(this._selection.isPartialSelected())), + ); + + /** + * Toggles the select-all state. + * @param event The click event if the toggle is triggered by a (mouse or keyboard) click. If + * using with a native ``, 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() { + this._assertValidParentSelection(); + this._configureControlValueAccessor(); + } + + private _configureControlValueAccessor() { + if (this._controlValueAccessor && this._controlValueAccessor.length) { + this._controlValueAccessor[0].registerOnChange((e: unknown) => { + if (e === true || e === false) { + this.toggle(); + } + }); + this.checked.pipe(takeUntil(this._destroyed)).subscribe((state) => { + this._controlValueAccessor[0].writeValue(state); + }); + } + } + + private _assertValidParentSelection() { + if (!this._selection && isDevMode()) { + throw Error('CdkSelectAll: missing CdkSelection in the parent'); + } + + if (!this._selection.multiple && isDevMode()) { + throw Error('CdkSelectAll: CdkSelection must have cdkSelectionMultiple set to true'); + } + } + + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + } +} diff --git a/src/cdk-experimental/selection/selection-column.ts b/src/cdk-experimental/selection/selection-column.ts new file mode 100644 index 000000000000..e840feb54d8f --- /dev/null +++ b/src/cdk-experimental/selection/selection-column.ts @@ -0,0 +1,107 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef, CdkTable} from '@angular/cdk/table'; +import { + Component, + Input, + isDevMode, + OnDestroy, + OnInit, + Optional, + ViewChild, + ChangeDetectionStrategy, + ViewEncapsulation, +} from '@angular/core'; + +import {CdkSelection} from './selection'; + +/** + * Column that adds row selecting checkboxes and a select-all checkbox if `cdkSelectionMultiple` is + * `true`. + * + * Must be used within a parent `CdkSelection` directive. + */ +@Component({ + selector: 'cdk-selection-column', + template: ` + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class CdkSelectionColumn implements OnInit, OnDestroy { + /** Column name that should be used to reference this column. */ + @Input('cdkSelectionColumnName') + get name(): string { + return this._name; + } + set name(name: string) { + this._name = name; + + this._syncColumnDefName(); + } + private _name: string; + + @ViewChild(CdkColumnDef, {static: true}) private readonly _columnDef: CdkColumnDef; + @ViewChild(CdkCellDef, {static: true}) private readonly _cell: CdkCellDef; + @ViewChild(CdkHeaderCellDef, {static: true}) private readonly _headerCell: CdkHeaderCellDef; + + constructor( + @Optional() private _table: CdkTable, + @Optional() readonly selection: CdkSelection, + ) {} + + ngOnInit() { + if (!this.selection && isDevMode()) { + throw Error('CdkSelectionColumn: missing CdkSelection in the parent'); + } + + this._syncColumnDefName(); + + if (this._table) { + this._columnDef.cell = this._cell; + this._columnDef.headerCell = this._headerCell; + this._table.addColumnDef(this._columnDef); + } else { + if (isDevMode()) { + throw Error('CdkSelectionColumn: missing parent table'); + } + } + } + + ngOnDestroy() { + if (this._table) { + this._table.removeColumnDef(this._columnDef); + } + } + + private _syncColumnDefName() { + if (this._columnDef) { + this._columnDef.name = this._name; + } + } +} 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..696da173f0a1 --- /dev/null +++ b/src/cdk-experimental/selection/selection-set.ts @@ -0,0 +1,127 @@ +/** + * @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 {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>; +} + +/** + * 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 && isDevMode()) { + throw 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 && isDevMode()) { + throw 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 && isDevMode()) { + throw 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..f73cc22b6c20 --- /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 {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 {Observable, 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('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); } + private _index?: number; + + /** The checked state of the selection toggle */ + 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(); + + constructor( + @Optional() private _selection: CdkSelection, + @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) private _controlValueAccessors: + ControlValueAccessor[], + ) {} + + ngOnInit() { + 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') { + this.toggle(); + } + }); + + this.checked.pipe(takeUntil(this._destroyed)).subscribe((state) => { + this._controlValueAccessors[0].writeValue(state); + }); + } + } + + 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 new file mode 100644 index 000000000000..a82c8cf8bb5b --- /dev/null +++ b/src/cdk-experimental/selection/selection.ts @@ -0,0 +1,223 @@ +/** + * @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 {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {CollectionViewer, DataSource, isDataSource, ListRange} from '@angular/cdk/collections'; +import { + AfterContentChecked, + Directive, + EventEmitter, + Input, + isDevMode, + OnDestroy, + OnInit, + Output, + TrackByFunction +} from '@angular/core'; +import {Observable, of as observableOf, Subject, 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('trackBy') trackByFn: TrackByFunction; + + /** Whether to support multiple selection */ + @Input('cdkSelectionMultiple') + get multiple(): boolean { + return this._multiple; + } + set multiple(multiple: boolean) { + this._multiple = coerceBooleanProperty(multiple); + } + private _multiple: boolean; + + /** Emits when selection changes. */ + @Output('cdkSelectionChange') change = 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 Subject(); + + 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); + } + + 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 && isDevMode()) { + throw 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.change.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.trackByFn && index == null && isDevMode()) { + throw 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 && isDevMode()) { + throw 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.trackByFn && index == null && isDevMode()) { + throw 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'; + + static ngAcceptInputType_multiple: BooleanInput; +} + +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 ca0de2c1c25f..108299832433 100644 --- a/src/components-examples/BUILD.bazel +++ b/src/components-examples/BUILD.bazel @@ -59,6 +59,7 @@ ALL_EXAMPLES = [ "//src/components-examples/cdk/a11y", "//src/components-examples/cdk/overlay", "//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..99b8307a1da4 --- /dev/null +++ b/src/components-examples/cdk-experimental/selection/cdk-selection-column/cdk-selection-column-example.css @@ -0,0 +1,7 @@ +.example-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..ad8231d5c0cf --- /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..6c5d8357d607 --- /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'; +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..8877ac3e1b25 --- /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..5b0f3f65fe09 --- /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'; +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 886792ef85ee..457ef09ac4de 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -75,6 +75,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 87493fc13353..28fa3119c19a 100644 --- a/src/dev-app/dev-app/dev-app-layout.ts +++ b/src/dev-app/dev-app/dev-app-layout.ts @@ -8,8 +8,9 @@ import {Directionality} from '@angular/cdk/bidi'; 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'; import {DOCUMENT} from '@angular/common'; const isDarkThemeKey = 'ANGULAR_COMPONENTS_DEV_APP_DARK_THEME'; @@ -65,6 +66,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 a0a71b07246a..e7f189985dcc 100644 --- a/src/dev-app/dev-app/routes.ts +++ b/src/dev-app/dev-app/routes.ts @@ -52,10 +52,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'}, @@ -148,6 +145,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 7ebe754afa41..954c156a2d37 100644 --- a/src/dev-app/example/example-module.ts +++ b/src/dev-app/example/example-module.ts @@ -7,11 +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 {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 53eb14cbee8cae91227711e3107f28440c53a426 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Tue, 31 Mar 2020 09:06:12 -0700 Subject: [PATCH 2/4] feat(material-experimental/selection): add mat APIs for cdk-experimental/selection (#18620) --- .github/CODEOWNERS | 1 + src/components-examples/BUILD.bazel | 1 + .../selection/BUILD.bazel | 29 +++++ .../material-experimental/selection/index.ts | 34 ++++++ .../mat-selection-column-example.css | 3 + .../mat-selection-column-example.html | 30 ++++++ .../mat-selection-column-example.ts | 58 ++++++++++ .../mat-selection-list-example.html | 45 ++++++++ .../mat-selection-list-example.ts | 53 +++++++++ src/dev-app/example/example-list.ts | 2 - src/dev-app/selection/BUILD.bazel | 1 + .../selection/selection-demo-module.ts | 3 + src/dev-app/selection/selection-demo.ts | 6 ++ src/material-experimental/config.bzl | 1 + .../selection/BUILD.bazel | 35 ++++++ .../selection/_selection.scss | 3 + src/material-experimental/selection/index.ts | 10 ++ .../selection/public-api.ts | 14 +++ .../selection/row-selection.ts | 34 ++++++ .../selection/select-all.ts | 29 +++++ .../selection/selection-column.scss | 5 + .../selection/selection-column.ts | 101 ++++++++++++++++++ .../selection/selection-module.ts | 43 ++++++++ .../selection/selection-toggle.ts | 33 ++++++ .../selection/selection.ts | 35 ++++++ 25 files changed, 607 insertions(+), 2 deletions(-) create mode 100644 src/components-examples/material-experimental/selection/BUILD.bazel create mode 100644 src/components-examples/material-experimental/selection/index.ts create mode 100644 src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.css create mode 100644 src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.html create mode 100644 src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.ts create mode 100644 src/components-examples/material-experimental/selection/mat-selection-list/mat-selection-list-example.html create mode 100644 src/components-examples/material-experimental/selection/mat-selection-list/mat-selection-list-example.ts create mode 100644 src/material-experimental/selection/BUILD.bazel create mode 100644 src/material-experimental/selection/_selection.scss create mode 100644 src/material-experimental/selection/index.ts create mode 100644 src/material-experimental/selection/public-api.ts create mode 100644 src/material-experimental/selection/row-selection.ts create mode 100644 src/material-experimental/selection/select-all.ts create mode 100644 src/material-experimental/selection/selection-column.scss create mode 100644 src/material-experimental/selection/selection-column.ts create mode 100644 src/material-experimental/selection/selection-module.ts create mode 100644 src/material-experimental/selection/selection-toggle.ts create mode 100644 src/material-experimental/selection/selection.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e40a61849031..9ea4895af446 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -122,6 +122,7 @@ /src/material-experimental/mdc-typography/** @mmalerba /src/material-experimental/menubar/** @jelbourn @andy9775 /src/material-experimental/popover-edit/** @kseamon @andrewseguin +/src/material-experimental/selection/** @yifange @jelbourn # CDK experimental package /src/cdk-experimental/* @jelbourn diff --git a/src/components-examples/BUILD.bazel b/src/components-examples/BUILD.bazel index 108299832433..d980eb93b8ea 100644 --- a/src/components-examples/BUILD.bazel +++ b/src/components-examples/BUILD.bazel @@ -47,6 +47,7 @@ ALL_EXAMPLES = [ "//src/components-examples/material-experimental/popover-edit", "//src/components-examples/material-experimental/mdc-card", "//src/components-examples/material-experimental/mdc-form-field", + "//src/components-examples/material-experimental/selection", "//src/components-examples/cdk/tree", "//src/components-examples/cdk/text-field", "//src/components-examples/cdk/table", diff --git a/src/components-examples/material-experimental/selection/BUILD.bazel b/src/components-examples/material-experimental/selection/BUILD.bazel new file mode 100644 index 000000000000..cfdbac096a3f --- /dev/null +++ b/src/components-examples/material-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/material-experimental/selection", + deps = [ + "//src/cdk/collections", + "//src/cdk/table", + "//src/material-experimental/selection", + "//src/material/checkbox", + "@npm//@angular/forms", + ], +) + +filegroup( + name = "source-files", + srcs = glob([ + "**/*.html", + "**/*.css", + "**/*.ts", + ]), +) diff --git a/src/components-examples/material-experimental/selection/index.ts b/src/components-examples/material-experimental/selection/index.ts new file mode 100644 index 000000000000..2e8fdc1e0cbe --- /dev/null +++ b/src/components-examples/material-experimental/selection/index.ts @@ -0,0 +1,34 @@ +import {MatSelectionModule} from '@angular/material-experimental/selection'; +import {MatTableModule} from '@angular/material/table'; +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCheckboxModule} from '@angular/material/checkbox'; + +import {MatSelectionColumnExample} from './mat-selection-column/mat-selection-column-example'; +import {MatSelectionListExample} from './mat-selection-list/mat-selection-list-example'; + +export { + MatSelectionListExample, + MatSelectionColumnExample, +}; + +const EXAMPLES = [ + MatSelectionListExample, + MatSelectionColumnExample, +]; + +@NgModule({ + imports: [ + MatSelectionModule, + MatTableModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + MatCheckboxModule, + ], + declarations: EXAMPLES, + exports: EXAMPLES, +}) +export class MatSelectionExamplesModule { +} diff --git a/src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.css b/src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.css new file mode 100644 index 000000000000..cedd44731369 --- /dev/null +++ b/src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.css @@ -0,0 +1,3 @@ +.example-table { + width: 100%; +} diff --git a/src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.html b/src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.html new file mode 100644 index 000000000000..b528c55fba87 --- /dev/null +++ b/src/components-examples/material-experimental/selection/mat-selection-column/mat-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/material-experimental/selection/mat-selection-column/mat-selection-column-example.ts b/src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.ts new file mode 100644 index 000000000000..72b8eed687e4 --- /dev/null +++ b/src/components-examples/material-experimental/selection/mat-selection-column/mat-selection-column-example.ts @@ -0,0 +1,58 @@ +import {Component, OnDestroy} from '@angular/core'; +import {SelectionChange} from '@angular/material-experimental/selection'; +import {ReplaySubject} from 'rxjs'; + +/** + * @title Table that uses `matSelectionColumn` which allows users to select rows. + */ +@Component({ + selector: 'mat-selection-column-example', + templateUrl: 'mat-selection-column-example.html', + styleUrls: ['mat-selection-column-example.css'], +}) +export class MatSelectionColumnExample 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/material-experimental/selection/mat-selection-list/mat-selection-list-example.html b/src/components-examples/material-experimental/selection/mat-selection-list/mat-selection-list-example.html new file mode 100644 index 000000000000..bec0b35b1cc5 --- /dev/null +++ b/src/components-examples/material-experimental/selection/mat-selection-list/mat-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/material-experimental/selection/mat-selection-list/mat-selection-list-example.ts b/src/components-examples/material-experimental/selection/mat-selection-list/mat-selection-list-example.ts new file mode 100644 index 000000000000..c9af7c0b300e --- /dev/null +++ b/src/components-examples/material-experimental/selection/mat-selection-list/mat-selection-list-example.ts @@ -0,0 +1,53 @@ +import {SelectionChange} from '@angular/cdk-experimental/selection'; +import {Component, OnDestroy} from '@angular/core'; +import {ReplaySubject} from 'rxjs'; + +/** + * @title Mat Selection on a simple list. + */ +@Component({ + selector: 'mat-selection-list-example', + templateUrl: 'mat-selection-list-example.html', +}) +export class MatSelectionListExample 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/dev-app/example/example-list.ts b/src/dev-app/example/example-list.ts index d71d8863f2f9..05d5a43a366c 100644 --- a/src/dev-app/example/example-list.ts +++ b/src/dev-app/example/example-list.ts @@ -10,8 +10,6 @@ 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({ selector: 'material-example-list', diff --git a/src/dev-app/selection/BUILD.bazel b/src/dev-app/selection/BUILD.bazel index e3b3567853f5..264a3815636a 100644 --- a/src/dev-app/selection/BUILD.bazel +++ b/src/dev-app/selection/BUILD.bazel @@ -7,6 +7,7 @@ ng_module( srcs = glob(["**/*.ts"]), deps = [ "//src/components-examples/cdk-experimental/selection", + "//src/components-examples/material-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 index 6c47adfd3d2e..8fe8841bab70 100644 --- a/src/dev-app/selection/selection-demo-module.ts +++ b/src/dev-app/selection/selection-demo-module.ts @@ -7,6 +7,8 @@ */ import {CdkSelectionExamplesModule} from '@angular/components-examples/cdk-experimental/selection'; +import {MatSelectionExamplesModule} from +'@angular/components-examples/material-experimental/selection'; import {NgModule} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {RouterModule} from '@angular/router'; @@ -16,6 +18,7 @@ import {SelectionDemo} from './selection-demo'; @NgModule({ imports: [ CdkSelectionExamplesModule, + MatSelectionExamplesModule, FormsModule, RouterModule.forChild([{path: '', component: SelectionDemo}]), ], diff --git a/src/dev-app/selection/selection-demo.ts b/src/dev-app/selection/selection-demo.ts index f2666fb1753b..a7d8f1c4fa4b 100644 --- a/src/dev-app/selection/selection-demo.ts +++ b/src/dev-app/selection/selection-demo.ts @@ -15,6 +15,12 @@ import {Component} from '@angular/core';

CDK selection column and CDK row selection with CDK table

+ +

Mat selection with a list

+ + +

Mat selection column and Mat row selection with Mat table

+ `, }) export class SelectionDemo { diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl index 13796e01be13..184c9b157e88 100644 --- a/src/material-experimental/config.bzl +++ b/src/material-experimental/config.bzl @@ -34,6 +34,7 @@ entryPoints = [ "mdc-tabs", "menubar", "popover-edit", + "selection", ] # List of all non-testing entry-points of the Angular material-experimental package. diff --git a/src/material-experimental/selection/BUILD.bazel b/src/material-experimental/selection/BUILD.bazel new file mode 100644 index 000000000000..2289e9776998 --- /dev/null +++ b/src/material-experimental/selection/BUILD.bazel @@ -0,0 +1,35 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_module", "sass_binary", "sass_library") + +ng_module( + name = "selection", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + assets = [":selection_column_scss"], + module_name = "@angular/material-experimental/selection", + deps = [ + "//src/cdk-experimental/selection", + "//src/material/checkbox", + "//src/material/table", + "@npm//@angular/core", + ], +) + +sass_library( + name = "selection_scss_lib", + srcs = glob(["**/_*.scss"]), + deps = [], +) + +sass_binary( + name = "selection_column_scss", + src = "selection-column.scss", + include_paths = [ + "external/npm/node_modules", + ], + deps = [ + ], +) diff --git a/src/material-experimental/selection/_selection.scss b/src/material-experimental/selection/_selection.scss new file mode 100644 index 000000000000..14f7219a909f --- /dev/null +++ b/src/material-experimental/selection/_selection.scss @@ -0,0 +1,3 @@ +@mixin mat-selection-theme($theme) {} + +@mixin mat-selection-typography($config) {} diff --git a/src/material-experimental/selection/index.ts b/src/material-experimental/selection/index.ts new file mode 100644 index 000000000000..e1fc5bfc0361 --- /dev/null +++ b/src/material-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/material-experimental/selection/public-api.ts b/src/material-experimental/selection/public-api.ts new file mode 100644 index 000000000000..8cae343cd78d --- /dev/null +++ b/src/material-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/material-experimental/selection/row-selection.ts b/src/material-experimental/selection/row-selection.ts new file mode 100644 index 000000000000..518fb70e899c --- /dev/null +++ b/src/material-experimental/selection/row-selection.ts @@ -0,0 +1,34 @@ +/** + * @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 {CdkRowSelection} from '@angular/cdk-experimental/selection'; +import {Input, Directive} from '@angular/core'; + + +/** + * Applies `mat-selected` class and `aria-selected` to an element. + * + * Must be used within a parent `MatSelection` directive. + * Must be provided with the value. The index is required if `trackBy` is used on the `CdkSelection` + * directive. + */ +@Directive({ + selector: '[matRowSelection]', + host: { + '[class.mat-selected]': '_selection.isSelected(this.value, this.index)', + '[attr.aria-selected]': '_selection.isSelected(this.value, this.index)', + }, + providers: [{provide: CdkRowSelection, useExisting: MatRowSelection}] +}) +export class MatRowSelection extends CdkRowSelection { + /** The value that is associated with the row */ + @Input('matRowSelectionValue') value: T; + + /** The index of the value in the list. Required when used with `trackBy` */ + @Input('matRowSelectionIndex') index: number|undefined; +} diff --git a/src/material-experimental/selection/select-all.ts b/src/material-experimental/selection/select-all.ts new file mode 100644 index 000000000000..2501bb9dfce4 --- /dev/null +++ b/src/material-experimental/selection/select-all.ts @@ -0,0 +1,29 @@ +/** + * @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 {CdkSelectAll} from '@angular/cdk-experimental/selection'; +import {Directive} from '@angular/core'; + + +/** + * Makes the element a select-all toggle. + * + * Must be used within a parent `MatSelection` directive. It toggles the selection states + * of all the selection toggles connected with the `MatSelection` directive. + * If the element implements `ControlValueAccessor`, e.g. `MatCheckbox`, the directive + * automatically connects it with the select-all state provided by the `MatSelection` 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: '[matSelectAll]', + exportAs: 'matSelectAll', + providers: [{provide: CdkSelectAll, useExisting: MatSelectAll}] +}) +export class MatSelectAll extends CdkSelectAll { +} diff --git a/src/material-experimental/selection/selection-column.scss b/src/material-experimental/selection/selection-column.scss new file mode 100644 index 000000000000..81d4f297d7a7 --- /dev/null +++ b/src/material-experimental/selection/selection-column.scss @@ -0,0 +1,5 @@ +th.mat-selection-column-header, +td.mat-selection-column-cell { + text-align: center; + width: 48px; +} diff --git a/src/material-experimental/selection/selection-column.ts b/src/material-experimental/selection/selection-column.ts new file mode 100644 index 000000000000..074f0b091180 --- /dev/null +++ b/src/material-experimental/selection/selection-column.ts @@ -0,0 +1,101 @@ +/** + * @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 {MatCellDef, MatColumnDef, MatHeaderCellDef, MatTable} from '@angular/material/table'; +import { + Component, + Input, + isDevMode, + OnDestroy, + OnInit, + Optional, + ViewChild, + ChangeDetectionStrategy, + ViewEncapsulation, +} from '@angular/core'; + +import {MatSelection} from './selection'; + +/** + * Column that adds row selecting checkboxes and a select-all checkbox if `matSelectionMultiple` is + * `true`. + * + * Must be used within a parent `MatSelection` directive. + */ +@Component({ + selector: 'mat-selection-column', + template: ` + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['selection-column.css'], + encapsulation: ViewEncapsulation.None, +}) +export class MatSelectionColumn implements OnInit, OnDestroy { + /** Column name that should be used to reference this column. */ + @Input() + get name(): string { + return this._name; + } + set name(name: string) { + this._name = name; + + this._syncColumnDefName(); + } + private _name: string; + + @ViewChild(MatColumnDef, {static: true}) private readonly _columnDef: MatColumnDef; + @ViewChild(MatCellDef, {static: true}) private readonly _cell: MatCellDef; + @ViewChild(MatHeaderCellDef, {static: true}) private readonly _headerCell: MatHeaderCellDef; + + constructor( + @Optional() private _table: MatTable, + @Optional() readonly selection: MatSelection, + ) {} + + ngOnInit() { + if (!this.selection && isDevMode()) { + throw Error('MatSelectionColumn: missing MatSelection in the parent'); + } + + this._syncColumnDefName(); + + if (this._table) { + this._columnDef.cell = this._cell; + this._columnDef.headerCell = this._headerCell; + this._table.addColumnDef(this._columnDef); + } else if (isDevMode()) { + throw Error('MatSelectionColumn: 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/material-experimental/selection/selection-module.ts b/src/material-experimental/selection/selection-module.ts new file mode 100644 index 000000000000..f8e90a2d0466 --- /dev/null +++ b/src/material-experimental/selection/selection-module.ts @@ -0,0 +1,43 @@ +/** + * @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 + */ + +// TODO(yifange): Move the table-specific code to a separate module from the other selection +// behaviors once we move it out of experiemental. +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {MatTableModule} from '@angular/material/table'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSelectAll} from './select-all'; +import {MatSelection} from './selection'; +import {MatSelectionToggle} from './selection-toggle'; +import {MatSelectionColumn} from './selection-column'; +import {MatRowSelection} from './row-selection'; + +@NgModule({ + imports: [ + CommonModule, + MatTableModule, + MatCheckboxModule, + ], + exports: [ + MatSelectAll, + MatSelection, + MatSelectionToggle, + MatSelectionColumn, + MatRowSelection, + ], + declarations: [ + MatSelectAll, + MatSelection, + MatSelectionToggle, + MatSelectionColumn, + MatRowSelection, + ], +}) +export class MatSelectionModule { +} diff --git a/src/material-experimental/selection/selection-toggle.ts b/src/material-experimental/selection/selection-toggle.ts new file mode 100644 index 000000000000..32e36dd11d48 --- /dev/null +++ b/src/material-experimental/selection/selection-toggle.ts @@ -0,0 +1,33 @@ +/** + * @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 {CdkSelectionToggle} from '@angular/cdk-experimental/selection'; +import {Directive, Input} from '@angular/core'; + +/** + * Makes the element a selection toggle. + * + * Must be used within a parent `MatSelection` directive. + * Must be provided with the value. If `trackBy` is used on `MatSelection`, 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 `MatSelection` directive. If + * not, use `checked$` to get the checked state of the value, and `toggle()` to change the selection + * state. + */ +@Directive({ + selector: '[matSelectionToggle]', + exportAs: 'matSelectionToggle', + providers: [{provide: CdkSelectionToggle, useExisting: MatSelectionToggle}] +}) +export class MatSelectionToggle extends CdkSelectionToggle { + /** The value that is associated with the toggle */ + @Input('matSelectionToggleValue') value: T; + + /** The index of the value in the list. Required when used with `trackBy` */ + @Input('matSelectionToggleIndex') index: number|undefined; +} diff --git a/src/material-experimental/selection/selection.ts b/src/material-experimental/selection/selection.ts new file mode 100644 index 000000000000..62ab56468ec3 --- /dev/null +++ b/src/material-experimental/selection/selection.ts @@ -0,0 +1,35 @@ +/** + * @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 {CdkSelection, SelectionChange} from '@angular/cdk-experimental/selection'; +import {Directive, Input, Output, EventEmitter} from '@angular/core'; + + +/** + * 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 `matSelectionToggle`, `matSelectAll`, + * `matRowSelection` and `matSelectionColumn` are applied. + */ +@Directive({ + selector: '[matSelection]', + exportAs: 'matSelection', + providers: [{provide: CdkSelection, useExisting: MatSelection}] +}) +export class MatSelection extends CdkSelection { + /** Whether to support multiple selection */ + @Input('matSelectionMultiple') multiple: boolean; + + /** Emits when selection changes. */ + @Output('matSelectionChange') change = new EventEmitter>(); +} + +/** + * Represents the change in the selection set. + */ +export {SelectionChange} from '@angular/cdk-experimental/selection'; From ee91cbe583de4cbf8510cbd34d344d510239c950 Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Tue, 21 Jul 2020 14:49:54 -0700 Subject: [PATCH 3/4] CdkSelection: Add unit tests (#19945) --- src/cdk-experimental/selection/BUILD.bazel | 20 +- .../selection/selection.spec.ts | 691 ++++++++++++++++++ 2 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 src/cdk-experimental/selection/selection.spec.ts diff --git a/src/cdk-experimental/selection/BUILD.bazel b/src/cdk-experimental/selection/BUILD.bazel index 65e8c1f30348..15dd9584f72f 100644 --- a/src/cdk-experimental/selection/BUILD.bazel +++ b/src/cdk-experimental/selection/BUILD.bazel @@ -1,6 +1,6 @@ package(default_visibility = ["//visibility:public"]) -load("//tools:defaults.bzl", "ng_module") +load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite") ng_module( name = "selection", @@ -18,3 +18,21 @@ ng_module( "@npm//rxjs", ], ) + +ng_test_library( + name = "unit_test_sources", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["**/*.e2e.spec.ts"], + ), + deps = [ + ":selection", + "//src/cdk/table", + "//src/cdk/testing/private", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_test_sources"], +) diff --git a/src/cdk-experimental/selection/selection.spec.ts b/src/cdk-experimental/selection/selection.spec.ts new file mode 100644 index 000000000000..2008cb443f38 --- /dev/null +++ b/src/cdk-experimental/selection/selection.spec.ts @@ -0,0 +1,691 @@ +import {CdkTableModule} from '@angular/cdk/table'; +import { + ChangeDetectorRef, + Component, + ElementRef, + ViewChild, +} from '@angular/core'; +import { + async, + ComponentFixture, + fakeAsync, + flush, + TestBed, +} from '@angular/core/testing'; + +import {CdkSelection} from './selection'; +import {CdkSelectionModule} from './selection-module'; +import {SelectionChange} from './selection-set'; + +describe('CdkSelection', () => { + let fixture: ComponentFixture; + let component: ListWithMultiSelection; + + beforeEach(async(() => { + TestBed + .configureTestingModule({ + imports: [CdkSelectionModule], + declarations: [ + ListWithMultiSelection, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ListWithMultiSelection); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('cdkSelection', () => { + it('should allow toggling selection', () => { + expect(component.cdkSelection.isSelected('apple', 0)).toBeFalsy(); + + component.cdkSelection.toggleSelection('apple', 0); + + expect(component.cdkSelection.isSelected('apple', 0)).toBeTruthy(); + }); + + it('should allow selecting all', () => { + expect(component.cdkSelection.isAllSelected()).toBeFalsy(); + + component.cdkSelection.toggleSelectAll(); + + expect(component.cdkSelection.isAllSelected()).toBeTruthy(); + }); + + it('should detect partial selection', () => { + expect(component.cdkSelection.isPartialSelected()).toBeFalsy(); + + component.cdkSelection.toggleSelectAll(); + + expect(component.cdkSelection.isPartialSelected()).toBeFalsy(); + + component.cdkSelection.toggleSelectAll(); + component.cdkSelection.toggleSelection('apple', 0); + + expect(component.cdkSelection.isPartialSelected()).toBeTruthy(); + }); + + it('should clear selection when partial selected and toggling select-all', () => { + component.cdkSelection.toggleSelection('apple', 0); + component.cdkSelection.toggleSelectAll(); + + expect(component.cdkSelection.isPartialSelected()).toBeFalsy(); + expect(component.cdkSelection.isAllSelected()).toBeFalsy(); + }); + }); + + describe('cdkSelectAll', () => { + it('should select all items when not all selected', fakeAsync(() => { + expect(component.cdkSelection.isAllSelected()).toBeFalsy(); + expect(component.getSelectAll().textContent.trim()).toBe('unchecked'); + + component.clickSelectAll(); + + expect(component.cdkSelection.isAllSelected()).toBeTruthy(); + expect(component.getSelectAll().textContent.trim()).toBe('checked'); + })); + + it('should de-select all items when all selected', fakeAsync(() => { + // Select all items. + component.clickSelectAll(); + + expect(component.cdkSelection.isAllSelected()).toBeTruthy(); + expect(component.getSelectAll().textContent.trim()).toBe('checked'); + + component.clickSelectAll(); + + expect(component.cdkSelection.isAllSelected()).toBeFalsy(); + expect(component.getSelectAll().textContent.trim()).toBe('unchecked'); + })); + + it('should de-select all items when partially selected', fakeAsync(() => { + // make the 1st item selected. + component.clickSelectionToggle(0); + + expect(component.cdkSelection.isPartialSelected()).toBeTruthy(); + expect(component.getSelectAll().textContent.trim()).toBe('indeterminate'); + + component.clickSelectAll(); + + expect(component.cdkSelection.isAllSelected()).toBeFalsy(); + expect(component.cdkSelection.isPartialSelected()).toBeFalsy(); + expect(component.getSelectAll().textContent.trim()).toBe('unchecked'); + })); + + it('should respond to selection toggle clicks', fakeAsync(() => { + // Start with no selection. + expect(component.cdkSelection.isAllSelected()).toBeFalsy(); + expect(component.getSelectAll().textContent.trim()).toBe('unchecked'); + + // Select the 1st item. + component.clickSelectionToggle(0); + + // Partially selected. + expect(component.cdkSelection.isAllSelected()).toBeFalsy(); + expect(component.cdkSelection.isPartialSelected()).toBeTruthy(); + expect(component.getSelectAll().textContent.trim()).toBe('indeterminate'); + + // Select the all the other items. + component.clickSelectionToggle(1); + component.clickSelectionToggle(2); + component.clickSelectionToggle(3); + + // Select-all shows all selected. + expect(component.cdkSelection.isAllSelected()).toBeTruthy(); + expect(component.cdkSelection.isPartialSelected()).toBeFalsy(); + expect(component.getSelectAll().textContent.trim()).toBe('checked'); + })); + + it('should emit the correct selection change events', fakeAsync(() => { + component.clickSelectAll(); + + expect(component.selectionChange!.before).toEqual([]); + expect(component.selectionChange!.after).toEqual([ + {value: 'apple', index: 0}, + {value: 'banana', index: 1}, + {value: 'cherry', index: 2}, + {value: 'durian', index: 3}, + ]); + + component.clickSelectAll(); + + expect(component.selectionChange!.before).toEqual([ + {value: 'apple', index: 0}, + {value: 'banana', index: 1}, + {value: 'cherry', index: 2}, + {value: 'durian', index: 3}, + ]); + expect(component.selectionChange!.after).toEqual([]); + })); + }); + + describe('cdkSelectionToggle', () => { + it('should respond to select-all toggle click', fakeAsync(() => { + // All items not unchecked. + expect(component.getSelectionToggle(0).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('unchecked'); + + component.clickSelectAll(); + + // Everything selected. + expect(component.getSelectionToggle(0).textContent.trim()).toBe('checked'); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('checked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('checked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('checked'); + + component.clickSelectAll(); + + // Everything unselected. + expect(component.getSelectionToggle(0).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('unchecked'); + })); + + it('should select unselected item when clicked', fakeAsync(() => { + expect(component.cdkSelection.isSelected('apple', 0)).toBeFalsy(); + expect(component.getSelectionToggle(0).textContent.trim()).toBe('unchecked'); + + component.clickSelectionToggle(0); + + expect(component.cdkSelection.isSelected('apple', 0)).toBeTruthy(); + expect(component.getSelectionToggle(0).textContent.trim()).toBe('checked'); + + // And all the others are not affected. + expect(component.cdkSelection.isSelected('banana', 1)).toBeFalsy(); + expect(component.cdkSelection.isSelected('cherry', 2)).toBeFalsy(); + expect(component.cdkSelection.isSelected('durian', 3)).toBeFalsy(); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('unchecked'); + })); + + it('should de-selected selected item when clicked', fakeAsync(() => { + // Make all items selected. + component.clickSelectAll(); + + component.clickSelectionToggle(1); + + expect(component.cdkSelection.isSelected('banana', 1)).toBeFalsy(); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('unchecked'); + + // And all the others are not affected. + expect(component.cdkSelection.isSelected('apple', 0)).toBeTruthy(); + expect(component.cdkSelection.isSelected('cherry', 2)).toBeTruthy(); + expect(component.cdkSelection.isSelected('durian', 3)).toBeTruthy(); + expect(component.getSelectionToggle(0).textContent.trim()).toBe('checked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('checked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('checked'); + })); + + it('should emit the correct selection change events', fakeAsync(() => { + component.clickSelectionToggle(1); + + expect(component.selectionChange!.before).toEqual([]); + expect(component.selectionChange!.after).toEqual([ + {value: 'banana', index: 1}, + ]); + + component.clickSelectionToggle(2); + + expect(component.selectionChange!.before).toEqual([ + {value: 'banana', index: 1}, + ]); + expect(component.selectionChange!.after).toEqual([ + {value: 'banana', index: 1}, + {value: 'cherry', index: 2}, + ]); + + component.clickSelectionToggle(2); + + expect(component.selectionChange!.before).toEqual([ + {value: 'banana', index: 1}, + {value: 'cherry', index: 2}, + ]); + expect(component.selectionChange!.after).toEqual([ + {value: 'banana', index: 1}, + ]); + })); + }); +}); + +describe('CdkSelection with multiple = false', () => { + let fixture: ComponentFixture; + let component: ListWithSingleSelection; + + beforeEach(async(() => { + TestBed + .configureTestingModule({ + imports: [CdkSelectionModule], + declarations: [ + ListWithSingleSelection, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ListWithSingleSelection); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should uncheck the previous selection when selecting new item', fakeAsync(() => { + // Everything start as unchecked. + expect(component.getSelectionToggle(0).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('unchecked'); + + component.clickSelectionToggle(0); + + expect(component.getSelectionToggle(0).textContent.trim()).toBe('checked'); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('unchecked'); + + component.clickSelectionToggle(1); + + // Should uncheck the previous selection while selecting the new value. + expect(component.getSelectionToggle(0).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('checked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('unchecked'); + + component.clickSelectionToggle(1); + + // Selecting a selected value should still uncheck it. + expect(component.getSelectionToggle(0).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(1).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(2).textContent.trim()).toBe('unchecked'); + expect(component.getSelectionToggle(3).textContent.trim()).toBe('unchecked'); + })); + + it('should emit the correct selection change events', fakeAsync(() => { + component.clickSelectionToggle(1); + + expect(component.selectionChange!.before).toEqual([]); + expect(component.selectionChange!.after).toEqual([ + {value: 'banana', index: 1}, + ]); + + component.clickSelectionToggle(2); + + expect(component.selectionChange!.before).toEqual([ + {value: 'banana', index: 1}, + ]); + expect(component.selectionChange!.after).toEqual([ + {value: 'cherry', index: 2}, + ]); + + component.clickSelectionToggle(2); + + expect(component.selectionChange!.before).toEqual([ + {value: 'cherry', index: 2}, + ]); + expect(component.selectionChange!.after).toEqual([]); + })); +}); + +describe('cdkSelectionColumn', () => { + let fixture: ComponentFixture; + let component: MultiSelectTableWithSelectionColumn; + + beforeEach(async(() => { + TestBed + .configureTestingModule({ + imports: [ + CdkSelectionModule, + CdkTableModule, + ], + declarations: [ + MultiSelectTableWithSelectionColumn, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MultiSelectTableWithSelectionColumn); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should show check boxes', () => { + const checkboxes = + component.elementRef.nativeElement.querySelectorAll('input[type="checkbox"]'); + // Select-all toggle + each toggle per row. + expect(checkboxes.length).toBe(5); + }); + + it('should allow select all', fakeAsync(() => { + expect(component.getSelectAll().checked).toBe(false); + expect(component.getSelectionToggle(0).checked).toBe(false); + expect(component.getSelectionToggle(1).checked).toBe(false); + expect(component.getSelectionToggle(2).checked).toBe(false); + expect(component.getSelectionToggle(3).checked).toBe(false); + + component.clickSelectAll(); + + expect(component.getSelectAll().checked).toBe(true); + expect(component.getSelectionToggle(0).checked).toBe(true); + expect(component.getSelectionToggle(1).checked).toBe(true); + expect(component.getSelectionToggle(2).checked).toBe(true); + expect(component.getSelectionToggle(3).checked).toBe(true); + })); + + it('should allow toggle rows', fakeAsync(() => { + expect(component.getSelectAll().checked).toBe(false); + expect(component.getSelectAll().indeterminate).toBe(false); + expect(component.getSelectionToggle(0).checked).toBe(false); + + component.clickSelectionToggle(0); + + expect(component.getSelectAll().checked).toBe(false); + expect(component.getSelectAll().indeterminate).toBe(true); + expect(component.getSelectionToggle(0).checked).toBe(true); + + component.clickSelectionToggle(1); + component.clickSelectionToggle(2); + component.clickSelectionToggle(3); + + expect(component.getSelectAll().checked).toBe(true); + expect(component.getSelectAll().indeterminate).toBe(false); + expect(component.getSelectionToggle(1).checked).toBe(true); + expect(component.getSelectionToggle(2).checked).toBe(true); + expect(component.getSelectionToggle(3).checked).toBe(true); + })); + + describe('cdkRowSelection', () => { + it('should set .cdk-selected on selected rows', fakeAsync(() => { + expect(component.getRow(0).classList.contains('cdk-selected')).toBeFalsy(); + expect(component.getRow(1).classList.contains('cdk-selected')).toBeFalsy(); + expect(component.getRow(2).classList.contains('cdk-selected')).toBeFalsy(); + expect(component.getRow(3).classList.contains('cdk-selected')).toBeFalsy(); + + component.clickSelectionToggle(0); + + expect(component.getRow(0).classList.contains('cdk-selected')).toBeTruthy(); + + component.clickSelectionToggle(0); + + expect(component.getRow(0).classList.contains('cdk-selected')).toBeFalsy(); + })); + + it('should set aria-selected on selected rows', fakeAsync(() => { + expect(component.getRow(0).getAttribute('aria-selected')).toBe('false'); + expect(component.getRow(1).getAttribute('aria-selected')).toBe('false'); + expect(component.getRow(2).getAttribute('aria-selected')).toBe('false'); + expect(component.getRow(3).getAttribute('aria-selected')).toBe('false'); + + component.clickSelectionToggle(0); + + expect(component.getRow(0).getAttribute('aria-selected')).toBe('true'); + + component.clickSelectionToggle(0); + + expect(component.getRow(0).getAttribute('aria-selected')).toBe('false'); + })); + }); +}); + +describe('cdkSelectionColumn with multiple = false', () => { + let fixture: ComponentFixture; + let component: SingleSelectTableWithSelectionColumn; + + beforeEach(async(() => { + TestBed + .configureTestingModule({ + imports: [ + CdkSelectionModule, + CdkTableModule, + ], + declarations: [ + SingleSelectTableWithSelectionColumn, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SingleSelectTableWithSelectionColumn); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should not show select all', () => { + expect(component.elementRef.nativeElement.querySelector('input[cdkselectall]')).toBe(null); + }); + + it('should allow selecting one single row', fakeAsync(() => { + expect(component.getSelectionToggle(0).checked).toBe(false); + expect(component.getSelectionToggle(1).checked).toBe(false); + expect(component.getSelectionToggle(2).checked).toBe(false); + expect(component.getSelectionToggle(3).checked).toBe(false); + + component.clickSelectionToggle(0); + + expect(component.getSelectionToggle(0).checked).toBe(true); + + component.clickSelectionToggle(1); + + expect(component.getSelectionToggle(0).checked).toBe(false); + expect(component.getSelectionToggle(1).checked).toBe(true); + + component.clickSelectionToggle(1); + expect(component.getSelectionToggle(1).checked).toBe(false); + })); +}); + +@Component({ + template: ` +
    + +
  • + + {{item}} +
  • +
` +}) +class ListWithMultiSelection { + @ViewChild(CdkSelection) cdkSelection: CdkSelection; + + data = ['apple', 'banana', 'cherry', 'durian']; + + selectionChange?: SelectionChange; + + constructor(private readonly _elementRef: ElementRef, private readonly _cdr: ChangeDetectorRef) {} + + selectAllState(indeterminateState: boolean|null, checkedState: boolean|null): string { + if (indeterminateState) { + return 'indeterminate'; + } else if (checkedState) { + return 'checked'; + } else { + return 'unchecked'; + } + } + + clickSelectAll() { + this.getSelectAll().click(); + flush(); + this._cdr.detectChanges(); + } + + clickSelectionToggle(index: number) { + const toggle = this.getSelectionToggle(index); + if (!toggle) { + return; + } + + toggle.click(); + flush(); + this._cdr.detectChanges(); + } + + getSelectAll() { + return this._elementRef.nativeElement.querySelector('[cdkselectall]'); + } + + getSelectionToggle(index: number) { + return this._elementRef.nativeElement.querySelectorAll('[cdkselectiontoggle]')[index]; + } +} + +@Component({ + template: ` +
    +
  • + + {{item}} +
  • +
` +}) +class ListWithSingleSelection { + @ViewChild(CdkSelection) cdkSelection: CdkSelection; + + data = ['apple', 'banana', 'cherry', 'durian']; + selectionChange?: SelectionChange; + + clickSelectionToggle(index: number) { + const toggle = this.getSelectionToggle(index); + if (!toggle) { + return; + } + + toggle.click(); + flush(); + this._cdr.detectChanges(); + } + + constructor(private readonly _elementRef: ElementRef, private readonly _cdr: ChangeDetectorRef) {} + + getSelectionToggle(index: number) { + return this._elementRef.nativeElement.querySelectorAll('[cdkselectiontoggle]')[index]; + } +} + +@Component({ + template: ` + + + + + + + + + +
Name{{element}}
+ ` +}) +class MultiSelectTableWithSelectionColumn { + @ViewChild(CdkSelection) cdkSelection: CdkSelection; + + columns = ['select', 'name']; + data = ['apple', 'banana', 'cherry', 'durian']; + + selectAllState(indeterminateState: boolean|null, checkedState: boolean|null): string { + if (indeterminateState) { + return 'indeterminate'; + } else if (checkedState) { + return 'checked'; + } else { + return 'unchecked'; + } + } + + clickSelectAll() { + this.getSelectAll().click(); + flush(); + this._cdr.detectChanges(); + } + + clickSelectionToggle(index: number) { + const toggle = this.getSelectionToggle(index); + if (!toggle) { + return; + } + + toggle.click(); + flush(); + this._cdr.detectChanges(); + } + + constructor(readonly elementRef: ElementRef, private readonly _cdr: ChangeDetectorRef) {} + + getSelectAll(): HTMLInputElement { + return this.elementRef.nativeElement.querySelector('input[cdkselectall]'); + } + + getSelectionToggle(index: number): HTMLInputElement { + return this.elementRef.nativeElement.querySelectorAll('input[cdkselectiontoggle]')[index]; + } + + getRow(index: number): HTMLElement { + return this.elementRef.nativeElement.querySelectorAll('tr[cdkrowselection]')[index]; + } +} + +@Component({ + template: ` + + + + + + + + + +
Name{{element}}
+ ` +}) +class SingleSelectTableWithSelectionColumn { + @ViewChild(CdkSelection) cdkSelection: CdkSelection; + + columns = ['select', 'name']; + data = ['apple', 'banana', 'cherry', 'durian']; + + clickSelectionToggle(index: number) { + const toggle = this.getSelectionToggle(index); + if (!toggle) { + return; + } + + toggle.click(); + flush(); + this._cdr.detectChanges(); + } + + constructor(readonly elementRef: ElementRef, private readonly _cdr: ChangeDetectorRef) {} + + getSelectionToggle(index: number): HTMLInputElement { + return this.elementRef.nativeElement.querySelectorAll('input[cdkselectiontoggle]')[index]; + } + + getRow(index: number): HTMLElement { + return this.elementRef.nativeElement.querySelectorAll('tr[cdkrowselection]')[index]; + } +} From 3441c2de12ea631e386dc53d70fc2b2857cbe51d Mon Sep 17 00:00:00 2001 From: Yifan Ge Date: Thu, 6 Aug 2020 17:07:27 -0700 Subject: [PATCH 4/4] feat(cdk-experiment/selection): Merge to master --- src/cdk-experimental/selection/BUILD.bazel | 4 ++-- src/cdk-experimental/selection/select-all.ts | 2 +- .../selection/selection-column.ts | 5 +++-- .../selection/selection-set.ts | 18 +++++++++--------- .../selection/selection-toggle.ts | 2 +- src/cdk-experimental/selection/selection.ts | 4 ++-- .../cdk-experimental/selection/BUILD.bazel | 4 ++-- .../selection/BUILD.bazel | 4 ++-- src/dev-app/example/example-module.ts | 1 - src/dev-app/selection/BUILD.bazel | 4 ++-- .../selection/BUILD.bazel | 4 ++-- .../selection/_selection.scss | 9 +++++++-- .../selection/selection-column.ts | 5 +++-- 13 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/cdk-experimental/selection/BUILD.bazel b/src/cdk-experimental/selection/BUILD.bazel index 15dd9584f72f..69d04d366acb 100644 --- a/src/cdk-experimental/selection/BUILD.bazel +++ b/src/cdk-experimental/selection/BUILD.bazel @@ -1,7 +1,7 @@ -package(default_visibility = ["//visibility:public"]) - load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite") +package(default_visibility = ["//visibility:public"]) + ng_module( name = "selection", srcs = glob( diff --git a/src/cdk-experimental/selection/select-all.ts b/src/cdk-experimental/selection/select-all.ts index 0b4e9e411556..000587cdc1fa 100644 --- a/src/cdk-experimental/selection/select-all.ts +++ b/src/cdk-experimental/selection/select-all.ts @@ -67,7 +67,7 @@ export class CdkSelectAll implements OnDestroy, OnInit { private readonly _destroyed = new Subject(); constructor( - @Optional() private readonly _selection: CdkSelection, + @Optional() @Inject(CdkSelection) private readonly _selection: CdkSelection, @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) private readonly _controlValueAccessor: ControlValueAccessor[]) {} diff --git a/src/cdk-experimental/selection/selection-column.ts b/src/cdk-experimental/selection/selection-column.ts index e840feb54d8f..0e5a0bbd8426 100644 --- a/src/cdk-experimental/selection/selection-column.ts +++ b/src/cdk-experimental/selection/selection-column.ts @@ -17,6 +17,7 @@ import { ViewChild, ChangeDetectionStrategy, ViewEncapsulation, + Inject, } from '@angular/core'; import {CdkSelection} from './selection'; @@ -71,8 +72,8 @@ export class CdkSelectionColumn implements OnInit, OnDestroy { @ViewChild(CdkHeaderCellDef, {static: true}) private readonly _headerCell: CdkHeaderCellDef; constructor( - @Optional() private _table: CdkTable, - @Optional() readonly selection: CdkSelection, + @Optional() @Inject(CdkTable) private _table: CdkTable, + @Optional() @Inject(CdkSelection) readonly selection: CdkSelection, ) {} ngOnInit() { diff --git a/src/cdk-experimental/selection/selection-set.ts b/src/cdk-experimental/selection/selection-set.ts index 696da173f0a1..59ab89d5f1ad 100644 --- a/src/cdk-experimental/selection/selection-set.ts +++ b/src/cdk-experimental/selection/selection-set.ts @@ -15,8 +15,8 @@ import {Subject} from 'rxjs'; */ interface TrackBySelection { isSelected(value: SelectableWithIndex): boolean; - select(...values: Array>): void; - deselect(...values: Array>): void; + select(...values: SelectableWithIndex[]): void; + deselect(...values: SelectableWithIndex[]): void; changed: Subject>; } @@ -33,8 +33,8 @@ export interface SelectableWithIndex { * Represents the change in the selection set. */ export interface SelectionChange { - before: Array>; - after: Array>; + before: SelectableWithIndex[]; + after: SelectableWithIndex[]; } /** @@ -54,7 +54,7 @@ export class SelectionSet implements TrackBySelection { return this._selectionMap.has(this._getTrackedByValue(value)); } - select(...selects: Array>) { + select(...selects: SelectableWithIndex[]) { if (!this._multiple && selects.length > 1 && isDevMode()) { throw Error('SelectionSet: not multiple selection'); } @@ -65,7 +65,7 @@ export class SelectionSet implements TrackBySelection { this._selectionMap.clear(); } - const toSelect: Array> = []; + const toSelect: SelectableWithIndex[] = []; for (const select of selects) { if (this.isSelected(select)) { continue; @@ -80,13 +80,13 @@ export class SelectionSet implements TrackBySelection { this.changed.next({before, after}); } - deselect(...selects: Array>) { + deselect(...selects: SelectableWithIndex[]) { if (!this._multiple && selects.length > 1 && isDevMode()) { throw Error('SelectionSet: not multiple selection'); } const before = this._getCurrentSelection(); - const toDeselect: Array> = []; + const toDeselect: SelectableWithIndex[] = []; for (const select of selects) { if (!this.isSelected(select)) { @@ -121,7 +121,7 @@ export class SelectionSet implements TrackBySelection { return this._trackByFn(select.index!, select.value); } - private _getCurrentSelection(): Array> { + private _getCurrentSelection(): SelectableWithIndex[] { return Array.from(this._selectionMap.values()); } } diff --git a/src/cdk-experimental/selection/selection-toggle.ts b/src/cdk-experimental/selection/selection-toggle.ts index f73cc22b6c20..0d2220af0dea 100644 --- a/src/cdk-experimental/selection/selection-toggle.ts +++ b/src/cdk-experimental/selection/selection-toggle.ts @@ -61,7 +61,7 @@ export class CdkSelectionToggle implements OnDestroy, OnInit { private _destroyed = new Subject(); constructor( - @Optional() private _selection: CdkSelection, + @Optional() @Inject(CdkSelection) private _selection: CdkSelection, @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) private _controlValueAccessors: ControlValueAccessor[], ) {} diff --git a/src/cdk-experimental/selection/selection.ts b/src/cdk-experimental/selection/selection.ts index a82c8cf8bb5b..77734ab2d684 100644 --- a/src/cdk-experimental/selection/selection.ts +++ b/src/cdk-experimental/selection/selection.ts @@ -187,7 +187,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } private _selectAll() { - const toSelect: Array> = []; + const toSelect: SelectableWithIndex[] = []; this._data.forEach((value, index) => { toSelect.push({value, index}); }); @@ -196,7 +196,7 @@ export class CdkSelection implements OnInit, AfterContentChecked, CollectionV } private _clearAll() { - const toDeselect: Array> = []; + const toDeselect: SelectableWithIndex[] = []; this._data.forEach((value, index) => { toDeselect.push({value, index}); }); diff --git a/src/components-examples/cdk-experimental/selection/BUILD.bazel b/src/components-examples/cdk-experimental/selection/BUILD.bazel index 7915e6cf928d..b2c9bfc97298 100644 --- a/src/components-examples/cdk-experimental/selection/BUILD.bazel +++ b/src/components-examples/cdk-experimental/selection/BUILD.bazel @@ -1,7 +1,7 @@ -package(default_visibility = ["//visibility:public"]) - load("//tools:defaults.bzl", "ng_module") +package(default_visibility = ["//visibility:public"]) + ng_module( name = "selection", srcs = glob(["**/*.ts"]), diff --git a/src/components-examples/material-experimental/selection/BUILD.bazel b/src/components-examples/material-experimental/selection/BUILD.bazel index cfdbac096a3f..6b5cc62377a9 100644 --- a/src/components-examples/material-experimental/selection/BUILD.bazel +++ b/src/components-examples/material-experimental/selection/BUILD.bazel @@ -1,7 +1,7 @@ -package(default_visibility = ["//visibility:public"]) - load("//tools:defaults.bzl", "ng_module") +package(default_visibility = ["//visibility:public"]) + ng_module( name = "selection", srcs = glob(["**/*.ts"]), diff --git a/src/dev-app/example/example-module.ts b/src/dev-app/example/example-module.ts index 954c156a2d37..790bd8c3ebb2 100644 --- a/src/dev-app/example/example-module.ts +++ b/src/dev-app/example/example-module.ts @@ -7,7 +7,6 @@ */ import {CommonModule} from '@angular/common'; -import {ExampleModule as DocsExampleModule} from '@angular/components-examples'; import {NgModule} from '@angular/core'; import {MatExpansionModule} from '@angular/material/expansion'; diff --git a/src/dev-app/selection/BUILD.bazel b/src/dev-app/selection/BUILD.bazel index 264a3815636a..ac897a67e329 100644 --- a/src/dev-app/selection/BUILD.bazel +++ b/src/dev-app/selection/BUILD.bazel @@ -1,7 +1,7 @@ -package(default_visibility = ["//visibility:public"]) - load("//tools:defaults.bzl", "ng_module") +package(default_visibility = ["//visibility:public"]) + ng_module( name = "selection", srcs = glob(["**/*.ts"]), diff --git a/src/material-experimental/selection/BUILD.bazel b/src/material-experimental/selection/BUILD.bazel index 2289e9776998..94cd13789e70 100644 --- a/src/material-experimental/selection/BUILD.bazel +++ b/src/material-experimental/selection/BUILD.bazel @@ -1,7 +1,7 @@ -package(default_visibility = ["//visibility:public"]) - load("//tools:defaults.bzl", "ng_module", "sass_binary", "sass_library") +package(default_visibility = ["//visibility:public"]) + ng_module( name = "selection", srcs = glob( diff --git a/src/material-experimental/selection/_selection.scss b/src/material-experimental/selection/_selection.scss index 14f7219a909f..d2abf6b4aca8 100644 --- a/src/material-experimental/selection/_selection.scss +++ b/src/material-experimental/selection/_selection.scss @@ -1,3 +1,8 @@ -@mixin mat-selection-theme($theme) {} +@import '../../material/core/theming/check-duplicate-styles'; -@mixin mat-selection-typography($config) {} +@mixin mat-selection-theme($theme-or-color-config) { + $theme: _mat-legacy-get-theme($theme-or-color-config); + @include _mat-check-duplicate-theme-styles($theme, 'mat-selection'); +} + +@mixin mat-selection-typography($config-or-theme) {} diff --git a/src/material-experimental/selection/selection-column.ts b/src/material-experimental/selection/selection-column.ts index 074f0b091180..8fba868c49cd 100644 --- a/src/material-experimental/selection/selection-column.ts +++ b/src/material-experimental/selection/selection-column.ts @@ -17,6 +17,7 @@ import { ViewChild, ChangeDetectionStrategy, ViewEncapsulation, + Inject, } from '@angular/core'; import {MatSelection} from './selection'; @@ -67,8 +68,8 @@ export class MatSelectionColumn implements OnInit, OnDestroy { @ViewChild(MatHeaderCellDef, {static: true}) private readonly _headerCell: MatHeaderCellDef; constructor( - @Optional() private _table: MatTable, - @Optional() readonly selection: MatSelection, + @Optional() @Inject(MatTable) private _table: MatTable, + @Optional() @Inject(MatSelection) readonly selection: MatSelection, ) {} ngOnInit() {