-
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
Conversation
First things first, really great work on the CDK selection feature @amcdnl 👏 I was thinking whether it made sense to add also a drag-to-select feature to the CDK as part of this WIP. I have recently (a few weeks back) implemented a library called ngx-drag-to-select for this and people seem to like it and they also start using it in their apps. It also covers mobile use-cases and should provide you with everything you need to implement it in many different scenarios. Here's a demo page. I tried to focus heavily on performance and also ran some performance audits. The results were pretty good (~60 FPS). @jelbourn What do you think about integrating this into the Angular CDK as part of the selection feature? I would be totally open to completely overhaul the source code and re-write it if necessary so that the source code satisfies the Angular coding guideline / style. There also still some room for improvement, especially when it comes to the API and how to use it. Any thoughts? |
@jelbourn - Updated API per the feedback. |
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.
First pass
export class CdkSelection<T> implements OnInit { | ||
|
||
/** List of all active selections. */ | ||
@Input('cdkSelection') selections: T[] = []; |
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.
selectedItems
?
Should this be a Set
instead of an array?
- A large set of items might get expensive if you're doing a lot of array manipulation
- I'm guessing the items aren't ordered
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.
re: set - I suppose it could be but from a public API perspective that feels odd. I totally see the value of doing that internally though. Also, the API of the selection model class is a array so since this is more just directive bindings for that I was trying to honor that api. Let me know your thoughts.
* - Multiple: Multiple items can be selected. | ||
* - Modifier Multiple: Multiple items can be selected using modifier keys such as ctrl or shift. | ||
*/ | ||
@Input('cdkSelectionStrategy') strategy: 'single' | 'multiple' | 'modifierMultiple' = 'single'; |
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.
Could potentially bake this into the selector, e.g. cdkMultiSelection
Might also want to just do single
and multiple
with the modifier thing as a separate option, something like requireModifierKey
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.
Yup - already done - 2bd4b3a#diff-06a4dd49126d9cf2343782d2b7f01cacR28
@Input('cdkSelectionTrackBy') trackBy: (model: T) => any; | ||
|
||
/** Whether the selections can be cleared after selection. */ | ||
@Input('cdkSelectionClearable') |
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.
"Clear" makes me thing it will do the whole thing, maybe "deselect" or "toggle"
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.
private _clearable: boolean = true; | ||
|
||
/** The max number of selections that can be selected */ | ||
@Input('cdkSelectionMaxSelections') |
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.
cdkSelectionMaxSelected
?
} | ||
|
||
/** Returns whether the control is multi select or not. */ | ||
getIsMultiple() { |
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.
Just isMultiple()
|
||
/** Deregister a control from the set. */ | ||
deregister(ctrl) { | ||
const idx = this._set.indexOf(ctrl); |
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.
idx
-> toggleIndex
} | ||
|
||
/** Deregister a control from the set. */ | ||
deregister(ctrl) { |
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.
ctrl
-> toggle
(here and elsewhere)
} | ||
|
||
/** Set the user selection for the element. */ | ||
setTextSelection(type: string) { |
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.
I don't quite follow why this is here
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 I do SHIFT + CLICK I want to set the text selection on the entire parent directive area to a specified state.
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.
There should be a comment with the explanation
} | ||
|
||
/** Update the selections given the arguments. */ | ||
private _setSelections(ctrl: any) { |
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.
This function looks like it could use some comments
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.
How about Set the selection state given a toggle directive and its activated options.
?
} | ||
|
||
/** Get the index of the given element in the dom tree relative to the other toggles. */ | ||
private _getIndex(sortedSet: any[], sourceCtrl: any) { |
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.
_getToggleIndex
?
@jelbourn - Updated per feedback. |
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.
Second pass
/** The modifier that was invoked. */ | ||
modifier: 'shift' | 'meta' | null; | ||
|
||
private _destroy = new Subject(); |
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.
_destroy
-> _destroyed
private _destroy = new Subject(); | ||
|
||
constructor( | ||
public elementRef: ElementRef, |
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.
Ack; it needs a user-facing JsDoc, then
} | ||
|
||
/** Invoke toggle on the parent selection directive. */ | ||
_onToggle() { |
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.
_onToggle
-> _toggle()
?
@@ -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 comment
The 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.
|
||
constructor(public elementRef: ElementRef) {} | ||
|
||
clickITem(id: string, modifier?: 'meta' | 'shift') { |
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.
clickItem
|
||
/** Set the selection state given a toggle directive and its activated options. */ | ||
private _setSelections(toggle) { | ||
const ctrls = this._getSortedSet(); |
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.
ctrls
-> either toggles
or toggleControls
(here and in all other places). I personally like toggleControls
as being the most explicit.
/** Set the selection state given a toggle directive and its activated options. */ | ||
private _setSelections(toggle) { | ||
const ctrls = this._getSortedSet(); | ||
const index = this._getToggleIndex(ctrls, toggle); |
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.
Could index
be more specific?
const { modifier, model } = toggle; | ||
const selections = this._selectionModel.selected; | ||
|
||
if (this.mode === 'multiple' && modifier === 'shift') { |
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.
Could the body of this method be broken up into two other methods: _setMultipleSelections
and _setsingleSelections
? Right now it feels like it has a lot going on.
|
||
if (Array.isArray(toggle.model)) { | ||
if (this.mode === 'multiple' && | ||
(this.maxSelections === undefined || |
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.
Could this condition be broken into a function?
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.
Kinda feels like overkill IMO.
} | ||
|
||
/** Get the set of controls sorted by DOM order. */ | ||
private _getSortedSet(): any[] { |
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.
_getSortedSet
doesn't seem accurate since it's not returning a Set
What do you think about making CTRL + A do select all? Also the re: implementation on select list - All of the mixins don't have any arguments in the constructor. For this I need to inject a variety of things into the injector so I don't think we can do it cleanly. I think it would be a better candidate for inheritance or encapsulated composition. Thoughts? |
|
'[class.cdk-selection-selected]': 'selected', | ||
'(mousedown)': '_setModifiers($event)', | ||
'(click)': 'toggle()', | ||
/** Right click should select item. */ |
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.
Should be a //
style comment ( /** */
is only for JsDoc)
Hi @amcdnl! This PR has merge conflicts due to recent upstream merges. |
Thats fine. So we would be adding a
I was playing with making this a mixin but the complexity exploded. I think it would be better as a higher order component. |
src/cdk/collections/selection.ts
Outdated
@@ -34,12 +34,13 @@ export class SelectionModel<T> { | |||
} | |||
|
|||
/** Event emitted when the value has changed. */ | |||
onChange: Subject<SelectionChange<T>> | null = this._emitChanges ? new Subject() : null; | |||
change: Subject<SelectionChange<T>> | null = this._emitChanges ? new Subject() : null; |
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.
Is this a breaking change?
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.
Actually, I've already made this change in this PR and deprecated it - #8286 - I'm going to revert this back to the original name and we can consolidate on that merge :)
@@ -39,7 +39,8 @@ export class SelectionModel<T> { | |||
constructor( | |||
private _multiple = false, | |||
initiallySelectedValues?: T[], | |||
private _emitChanges = true) { | |||
private _emitChanges = true, | |||
private _trackBy?: (model) => any) { |
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.
Looks like there aren't any unit tests that cover this new trackBy
behavior; this would be necessary since it's part of the main library rather than cdk-experimental
Hi @amcdnl! This PR has merge conflicts due to recent upstream merges. |
1 similar comment
Hi @amcdnl! This PR has merge conflicts due to recent upstream merges. |
@amcdnl this is great feature which is not yet merged. Can You make updates with changes from review? |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
WIP of CDK Selection Directive - Addresses #11309
Note: This PR includes 2 changes to Selection Model.
onChange
event tochange
to honor standardtrackBy