-
Notifications
You must be signed in to change notification settings - Fork 6.8k
wip: cdk selection #11770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wip: cdk selection #11770
Changes from all commits
b45d46f
2bd4b3a
f48d574
c515ae8
ce1561b
1a3f875
59629aa
913c434
b9e7261
7888b82
81fd942
4749ee2
f348bb5
3fd4e4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package(default_visibility=["//visibility:public"]) | ||
load("@angular//:index.bzl", "ng_module") | ||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library", "ts_web_test") | ||
load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary") | ||
|
||
|
||
ng_module( | ||
name = "selection", | ||
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]), | ||
module_name = "@angular/cdk-experimental/selection", | ||
assets = [] + glob(["**/*.html"]), | ||
deps = [ | ||
"//src/cdk/coercion", | ||
"//src/cdk/collections", | ||
"@rxjs", | ||
], | ||
tsconfig = "//src/cdk-experimental:tsconfig-build.json", | ||
) | ||
|
||
|
||
ts_library( | ||
name = "selection_test_sources", | ||
testonly = 1, | ||
srcs = glob(["**/*.spec.ts"]), | ||
deps = [ | ||
":selection", | ||
"//src/cdk/collections", | ||
"//src/cdk/testing", | ||
"@rxjs", | ||
], | ||
tsconfig = "//src/cdk-experimental:tsconfig-build.json", | ||
) | ||
|
||
ts_web_test( | ||
name = "unit_tests", | ||
bootstrap = [ | ||
"//:web_test_bootstrap_scripts", | ||
], | ||
tags = ["manual"], | ||
|
||
# Do not sort | ||
deps = [ | ||
"//:tslib_bundle", | ||
"//:angular_bundles", | ||
"//:angular_test_bundles", | ||
"//test:angular_test_init", | ||
":selection_test_sources", | ||
], | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* @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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/** | ||
* @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-module'; | ||
export * from './selection'; | ||
export * from './selection-toggle'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* @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 {NgModule} from '@angular/core'; | ||
import {CdkSelection} from './selection'; | ||
import {CdkSelectionToggle} from './selection-toggle'; | ||
|
||
|
||
@NgModule({ | ||
declarations: [CdkSelection, CdkSelectionToggle], | ||
exports: [CdkSelection, CdkSelectionToggle] | ||
}) | ||
export class CdkSelectionModule {} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/** | ||
* @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, Input, OnInit, OnDestroy, ElementRef, ChangeDetectorRef} from '@angular/core'; | ||
import {CdkSelection} from './selection'; | ||
import {Subject} from 'rxjs'; | ||
import {takeUntil} from 'rxjs/operators'; | ||
import {coerceBooleanProperty} from '@angular/cdk/coercion'; | ||
|
||
|
||
@Directive({ | ||
selector: '[cdkSelectionToggle]', | ||
host: { | ||
'class': 'cdk-selection-toggle', | ||
'tabindex': '-1', | ||
'[class.cdk-selection-selected]': 'selected', | ||
'(mousedown)': '_setModifiers($event)', | ||
'(click)': 'toggle()', | ||
// Right click should select item. | ||
'(contextmenu)': 'toggle()', | ||
'(keydown.enter)': 'toggle()', | ||
'(keydown.space)': 'toggle()', | ||
'(keydown.shift)': '_disableTextSelection()', | ||
'(blur)': '_enableTextSelection()', | ||
} | ||
}) | ||
export class CdkSelectionToggle<T> implements OnInit, OnDestroy { | ||
|
||
/** The value(s) that represent the selection of this directive. */ | ||
@Input('cdkSelectionToggle') model: T | T[]; | ||
|
||
/** Whether the selection is disabled or not. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't need the "or not" at the end of the comment (here and other boolean descriptions) |
||
@Input() | ||
get disabled(): boolean { return this._disabled; } | ||
set disabled(val) { this._disabled = coerceBooleanProperty(val); } | ||
private _disabled: boolean; | ||
|
||
/** Whether the toggle is selected. */ | ||
selected: boolean; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, similar to the sort you set the active states on the parent. This is just a internal state. Perhaps I should underscore it? |
||
|
||
/** The modifier that was invoked. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "that was invoked" when?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my original implementation I listened for this separately because in cases like a checkbox I would listen to the change event rather than click and this information would not be available on that change event. I figured as we carry this forward to other implementations like the checkbox having this at the root would simplify those scenarios. The modifier is public so the selection directive parent can read it out w/o having to pass it. Let me know your thoughts here. |
||
modifier: 'shift' | 'meta' | null; | ||
|
||
private _destroyed = new Subject(); | ||
|
||
constructor( | ||
/** @docs-private */ | ||
public elementRef: ElementRef, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, its used in the parent directive to identify the element order. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ack; it needs a user-facing JsDoc, then |
||
private _selection: CdkSelection<T>, | ||
private _changeDetectorRef: ChangeDetectorRef | ||
) {} | ||
|
||
ngOnInit() { | ||
this._selection.selectionChange | ||
.pipe(takeUntil(this._destroyed)) | ||
.subscribe(() => this._updateSelected()); | ||
|
||
this._updateSelected(); | ||
this._selection.registerToggle(this); | ||
} | ||
|
||
ngOnDestroy() { | ||
this._destroyed.next(); | ||
this._destroyed.complete(); | ||
|
||
this._selection.deregisterToggle(this); | ||
} | ||
|
||
/** Toggle the selection. */ | ||
toggle() { | ||
if (!this.disabled) { | ||
this._selection.toggle(this); | ||
} | ||
} | ||
|
||
/** Set the modifiers for the mouse event. */ | ||
_setModifiers(event) { | ||
if (event.metaKey || event.ctrlKey) { | ||
this.modifier = 'meta'; | ||
} else if (event.shiftKey) { | ||
this.modifier = 'shift'; | ||
} | ||
|
||
// Clear the modifier if we don't use it | ||
setTimeout(() => this.modifier = null, 200); | ||
} | ||
|
||
/** Update the state of the selection based on the selection model. */ | ||
_updateSelected() { | ||
if (Array.isArray(this.model)) { | ||
let has = true; | ||
for (const model of this.model) { | ||
if (!this._selection._selectionModel.isSelected(model)) { | ||
has = false; | ||
break; | ||
} | ||
} | ||
this.selected = has; | ||
} else { | ||
this.selected = this._selection._selectionModel.isSelected(this.model); | ||
} | ||
|
||
this._changeDetectorRef.markForCheck(); | ||
} | ||
|
||
/** Shift key was captured to set user selection. */ | ||
_disableTextSelection() { | ||
this._selection.setTextSelection('none'); | ||
} | ||
|
||
/** Element was blurred, lets reset user selection. */ | ||
_enableTextSelection() { | ||
setTimeout(() => this._selection.setTextSelection('initial'), 200); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
The `selection` package handles managing selection state on components | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, I'll do an editing pass on this after it's submitted since it's easier than doing back and forth review of longer copy. |
||
with support for single and multi select with keyboard modifiers. | ||
|
||
Components such as buttons, checkboxes, etc can all be decorated with | ||
the `cdkSelectionToggle` directive. Once decorated, the directive | ||
will listen for mouse and keyboard events and coordinate the | ||
selection options with the parent `cdkSelection` directive. | ||
|
||
In the example below, the div container of the buttons is decorated with `cdkSelection`. To populate | ||
default selections pass an array of the selections to this option like: | ||
`[cdkSelection]="['yellow']"`. Nested beneath the selection directive is a button decorated | ||
with the `cdkSelectionToggle` directive with an argument of the item's value property. | ||
|
||
```html | ||
<ul cdkSelection cdkSelectionMode="single"> | ||
<li *ngFor="let item of items"> | ||
<button [cdkSelectionToggle]="item.value"> | ||
{{item.label}} | ||
</button> | ||
</li> | ||
</ul> | ||
``` | ||
|
||
### Modes | ||
The selection directive has 2 different modes for selection: | ||
|
||
- Single: Only one item can be selected at a time. | ||
- Multiple: Multiple items can be selected. | ||
|
||
### Key Modifier | ||
When multi-selection mode is enabled anytime a user clicks a toggle element it will be selected. | ||
The `requireModifier` property will only allow multi-selection when using key modifiers. For example, | ||
CTRL/Command + Click would select multiples and SHIFT + Click would select a range. | ||
|
||
### Deselectable | ||
The ability to deselect a selection that has been made can be prevented using the `cdkSelectionDeselectable`. | ||
This means that after a selection (or many selections) have been made users can deselect | ||
all but one of the selections. This concept can be related to radio buttons selection strategy. | ||
|
||
### Tracking | ||
When using complex objects with the selection toggle, custom tracking strategies | ||
can ensure correct identification of the values. The `cdkSelectionTrackBy` accepts | ||
a function that will be invoked with the values of the toggle. This can be used to | ||
return a identifying attribute for the object. | ||
|
||
```TS | ||
@Component({ | ||
template: ` | ||
<ul cdkSelection cdkSelectionMode="multiple" [cdkSelectionTrackBy]=""trackBy> | ||
<li *ngFor="let item of items"> | ||
<button [cdkSelectionToggle]="item"> | ||
{{item.label}} | ||
</button> | ||
</li> | ||
</ul> | ||
` | ||
}) | ||
export class MyComponent { | ||
trackBy(model) { | ||
return model.value; | ||
} | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this is merged into the cdk for real, I think it would make sense to add it to the existing
collections
subpackage