-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Cdk listbox control accessor #20071
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
Cdk listbox control accessor #20071
Changes from 15 commits
8c11c3a
7525a2c
26af9c9
67f3aad
61513bb
29b8513
12f1e80
bc8e583
fe81e8c
4414737
3bdfa83
cbf7c2d
75c0dfa
1d88375
047077a
5802c7d
732d7c6
516354a
eb2405d
3b07bcc
bf04326
4c4a24c
c0aaac6
4ccfbd6
d00df03
d567cd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -21,6 +21,7 @@ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; | |||||
import {SelectionChange, SelectionModel} from '@angular/cdk/collections'; | ||||||
import {defer, merge, Observable, Subject} from 'rxjs'; | ||||||
import {startWith, switchMap, takeUntil} from 'rxjs/operators'; | ||||||
import {ControlValueAccessor} from '@angular/forms'; | ||||||
|
||||||
let nextId = 0; | ||||||
|
||||||
|
@@ -67,6 +68,9 @@ export class CdkOption implements ListKeyManagerOption, Highlightable { | |||||
this._disabled = coerceBooleanProperty(value); | ||||||
} | ||||||
|
||||||
/** The form value of the option. */ | ||||||
@Input() value: any; | ||||||
|
||||||
@Output() readonly selectionChange: EventEmitter<OptionSelectionChangeEvent> = | ||||||
new EventEmitter<OptionSelectionChangeEvent>(); | ||||||
|
||||||
|
@@ -178,12 +182,18 @@ export class CdkOption implements ListKeyManagerOption, Highlightable { | |||||
'[attr.aria-activedescendant]': '_getAriaActiveDescendant()' | ||||||
} | ||||||
}) | ||||||
export class CdkListbox implements AfterContentInit, OnDestroy, OnInit { | ||||||
export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, ControlValueAccessor { | ||||||
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.
Suggested change
And all references to |
||||||
|
||||||
_listKeyManager: ActiveDescendantKeyManager<CdkOption>; | ||||||
_selectionModel: SelectionModel<CdkOption>; | ||||||
_tabIndex = 0; | ||||||
|
||||||
/** `View -> model callback called when select has been touched` */ | ||||||
_onTouched: () => void = () => {}; | ||||||
|
||||||
/** `View -> model callback called when value changes` */ | ||||||
_onChange: (value: any) => void = () => {}; | ||||||
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.
Suggested change
|
||||||
|
||||||
readonly optionSelectionChanges: Observable<OptionSelectionChangeEvent> = defer(() => { | ||||||
const options = this._options; | ||||||
|
||||||
|
@@ -197,7 +207,6 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit { | |||||
private _multiple: boolean = false; | ||||||
private _useActiveDescendant: boolean = true; | ||||||
private _activeOption: CdkOption; | ||||||
|
||||||
private readonly _destroyed = new Subject<void>(); | ||||||
|
||||||
@ContentChildren(CdkOption, {descendants: true}) _options: QueryList<CdkOption>; | ||||||
|
@@ -235,6 +244,8 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit { | |||||
this._useActiveDescendant = coerceBooleanProperty(shouldUseActiveDescendant); | ||||||
} | ||||||
|
||||||
@Input() compareWith: (o1: any, o2: any) => boolean = (a1, a2) => a1 === a2; | ||||||
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.
Suggested change
|
||||||
|
||||||
ngOnInit() { | ||||||
this._selectionModel = new SelectionModel<CdkOption>(this.multiple); | ||||||
} | ||||||
|
@@ -371,27 +382,33 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit { | |||||
/** Selects the given option if the option and listbox aren't disabled. */ | ||||||
select(option: CdkOption) { | ||||||
if (!this.disabled && !option.disabled) { | ||||||
const wasSelected = option.selected; | ||||||
option.select(); | ||||||
|
||||||
if (!wasSelected) { | ||||||
this._emitChangeEvent(option); | ||||||
this._updateSelectionModel(option); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/** Deselects the given option if the option and listbox aren't disabled. */ | ||||||
deselect(option: CdkOption) { | ||||||
if (!this.disabled && !option.disabled) { | ||||||
const wasSelected = option.selected; | ||||||
option.deselect(); | ||||||
|
||||||
if (wasSelected) { | ||||||
this._emitChangeEvent(option); | ||||||
this._updateSelectionModel(option); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/** Sets the selected state of all options to be the given value. */ | ||||||
setAllSelected(isSelected: boolean) { | ||||||
for (const option of this._options.toArray()) { | ||||||
const wasSelected = option.selected; | ||||||
isSelected ? this.select(option) : this.deselect(option); | ||||||
|
||||||
if (wasSelected !== isSelected) { | ||||||
this._emitChangeEvent(option); | ||||||
this._updateSelectionModel(option); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -400,6 +417,51 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit { | |||||
this._listKeyManager.updateActiveItem(option); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Saves a callback function to be invoked when the select's value | ||||||
* changes from user input. Required to implement ControlValueAccessor. | ||||||
*/ | ||||||
registerOnChange(fn: (value: any) => void): void { | ||||||
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.
Suggested change
|
||||||
this._onChange = fn; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Saves a callback function to be invoked when the select is blurred | ||||||
* by the user. Required to implement ControlValueAccessor. | ||||||
*/ | ||||||
registerOnTouched(fn: () => {}): void { | ||||||
this._onTouched = fn; | ||||||
} | ||||||
|
||||||
/** Sets the select's value. Required to implement ControlValueAccessor. */ | ||||||
writeValue(value: any): void { | ||||||
if (this._options) { | ||||||
this._setSelectionByValue(value); | ||||||
} | ||||||
} | ||||||
|
||||||
/** Disables the select. Required to implement ControlValueAccessor. */ | ||||||
setDisabledState(isDisabled: boolean) { | ||||||
this.disabled = isDisabled; | ||||||
} | ||||||
|
||||||
/** Selects an option that has the corresponding given value. */ | ||||||
private _setSelectionByValue(values: any | any[]) { | ||||||
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.
Suggested change
|
||||||
for (const option of this._options.toArray()) { | ||||||
this.deselect(option); | ||||||
} | ||||||
|
||||||
for (const value of values) { | ||||||
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. Won't this break down if 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. I neglected to test that out. I'll test out different methods and find a way to handle it. Thanks. |
||||||
const correspondingOption = this._options.find((option: CdkOption) => { | ||||||
return option.value != null && this.compareWith(option.value, value); | ||||||
}); | ||||||
|
||||||
if (correspondingOption) { | ||||||
jelbourn marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
this.select(correspondingOption); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
static ngAcceptInputType_disabled: BooleanInput; | ||||||
static ngAcceptInputType_multiple: BooleanInput; | ||||||
static ngAcceptInputType_useActiveDescendant: BooleanInput; | ||||||
|
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.
Maybe the option should be generic so people can type the value if they want to? E.g.
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.
Hmm this seems like a good idea. Do you know if there's a reason why MatOption doesn't do this?
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.
+1,
MatOption
didn't do this because we didn't know any better back when we wrote it. I would, though, useunknown
instead ofany
for the default