Skip to content

Commit 2aed840

Browse files
committed
fix(cdk-experimental/listbox): address feedback
1 parent 35b0cee commit 2aed840

File tree

1 file changed

+63
-29
lines changed

1 file changed

+63
-29
lines changed

src/cdk-experimental/listbox/listbox.ts

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
InjectFlags,
1818
Input,
1919
OnDestroy,
20-
Optional,
2120
Output,
2221
QueryList,
2322
} from '@angular/core';
@@ -211,13 +210,12 @@ export class CdkListbox<T = unknown>
211210

212211
/** The value selected in the listbox, represented as an array of option values. */
213212
@Input('cdkListboxValue')
214-
get value(): T[] {
215-
return this._value;
213+
get value(): readonly T[] {
214+
return this.selectionModel().selected;
216215
}
217-
set value(value: T[]) {
216+
set value(value: readonly T[]) {
218217
this._setSelection(value);
219218
}
220-
private _value: T[] = [];
221219

222220
/**
223221
* Whether the listbox allows multiple options to be selected. If the value switches from `true`
@@ -274,6 +272,7 @@ export class CdkListbox<T = unknown>
274272
/** The child options in this listbox. */
275273
@ContentChildren(CdkOption, {descendants: true}) protected options: QueryList<CdkOption<T>>;
276274

275+
// TODO(mmalerba): Refactor SelectionModel so that its not necessary to create new instances
277276
/** The selection model used by the listbox. */
278277
protected selectionModelSubject = new BehaviorSubject(
279278
new SelectionModel<T>(this.multiple, [], true, this._compareWith),
@@ -292,10 +291,10 @@ export class CdkListbox<T = unknown>
292291
protected readonly changeDetectorRef = inject(ChangeDetectorRef);
293292

294293
/** Callback called when the listbox has been touched */
295-
private _onTouched: () => {};
294+
private _onTouched = () => {};
296295

297296
/** Callback called when the listbox value changes */
298-
private _onChange: (value: T[]) => void = () => {};
297+
private _onChange: (value: readonly T[]) => void = () => {};
299298

300299
/** Callback called when the form validator changes. */
301300
private _onValidatorChange = () => {};
@@ -334,12 +333,8 @@ export class CdkListbox<T = unknown>
334333
* @return A validation error or null
335334
*/
336335
private _validateInvalidValues: ValidatorFn = (control: AbstractControl) => {
337-
const validValues = (this.options ?? []).map(option => option.value);
338336
const controlValue = this._coerceValue(control.value);
339-
const isEqual = this.compareWith ?? Object.is;
340-
const invalidValues = controlValue.filter(
341-
value => !validValues.some(validValue => isEqual(value, validValue)),
342-
);
337+
const invalidValues = this._getValuesWithValidity(controlValue, false);
343338
if (invalidValues.length) {
344339
return {'cdkListboxInvalidValues': {'values': invalidValues}};
345340
}
@@ -370,6 +365,7 @@ export class CdkListbox<T = unknown>
370365
this._initKeyManager();
371366
this._combobox?._registerContent(this.id, 'listbox');
372367
this.options.changes.pipe(takeUntil(this.destroyed)).subscribe(() => {
368+
this._updateInternalValue();
373369
this._onValidatorChange();
374370
});
375371
this._optionClicked
@@ -435,7 +431,7 @@ export class CdkListbox<T = unknown>
435431
* @param fn The callback to register
436432
* @docs-private
437433
*/
438-
registerOnChange(fn: (value: T[]) => void): void {
434+
registerOnChange(fn: (value: readonly T[]) => void): void {
439435
this._onChange = fn;
440436
}
441437

@@ -453,7 +449,7 @@ export class CdkListbox<T = unknown>
453449
* @param value The new value of the listbox
454450
* @docs-private
455451
*/
456-
writeValue(value: T[]): void {
452+
writeValue(value: readonly T[]): void {
457453
this._setSelection(value);
458454
}
459455

@@ -615,7 +611,7 @@ export class CdkListbox<T = unknown>
615611
this.selectionModelSubject.next(
616612
new SelectionModel(
617613
this.multiple,
618-
!this.multiple && this.value.length > 1 ? [] : this.value,
614+
!this.multiple && this.value.length > 1 ? [] : this.value.slice(),
619615
true,
620616
this._compareWith,
621617
),
@@ -634,25 +630,49 @@ export class CdkListbox<T = unknown>
634630
* Set the selected values.
635631
* @param value The list of new selected values.
636632
*/
637-
private _setSelection(value: T[]) {
638-
this.selectionModel().setSelection(...this._coerceValue(value));
633+
private _setSelection(value: readonly T[]) {
634+
this.selectionModel().setSelection(
635+
...this._getValuesWithValidity(this._coerceValue(value), true),
636+
);
639637
}
640638

641639
/** Update the internal value of the listbox based on the selection model. */
642640
private _updateInternalValue() {
643-
const selectionSet = new Set(this.selectionModel().selected);
644-
// Reduce the options list to just the selected values, maintaining their order,
645-
// but removing any duplicate values.
646-
this._value = (this.options ?? []).reduce<T[]>((result, option) => {
647-
if (selectionSet.has(option.value)) {
648-
result.push(option.value);
649-
selectionSet.delete(option.value);
650-
}
651-
return result;
652-
}, []);
641+
const options = [...this.options];
642+
const indexCache = new Map<T, number>();
643+
// Check if we need to remove any values due to them becoming invalid
644+
// (e.g. if the option was removed from the DOM.)
645+
const selected = this.selectionModel().selected;
646+
const validSelected = this._getValuesWithValidity(selected, true);
647+
if (validSelected.length != selected.length) {
648+
this.selectionModel().setSelection(...validSelected);
649+
}
650+
this.selectionModel().sort((a: T, b: T) => {
651+
const aIndex = this._getIndexForValue(indexCache, options, a);
652+
const bIndex = this._getIndexForValue(indexCache, options, b);
653+
return aIndex - bIndex;
654+
});
653655
this.changeDetectorRef.markForCheck();
654656
}
655657

658+
/**
659+
* Gets the index of the given value in the given list of options.
660+
* @param cache The cache of indices found so far
661+
* @param options The list of options to search in
662+
* @param value The value to find
663+
* @return The index of the value in the options list
664+
*/
665+
private _getIndexForValue(cache: Map<T, number>, options: CdkOption<T>[], value: T) {
666+
const isEqual = this.compareWith || Object.is;
667+
if (!cache.has(value)) {
668+
cache.set(
669+
value,
670+
options.findIndex(option => isEqual(value, option.value)),
671+
);
672+
}
673+
return cache.get(value)!;
674+
}
675+
656676
/**
657677
* Handle the user clicking an option.
658678
* @param option The option that was clicked.
@@ -703,15 +723,29 @@ export class CdkListbox<T = unknown>
703723
* @param value The value to coerce
704724
* @return An array
705725
*/
706-
private _coerceValue(value: T[]) {
726+
private _coerceValue(value: readonly T[]) {
707727
return value == null ? [] : coerceArray(value);
708728
}
729+
730+
/**
731+
* Get the sublist of values with the given validity.
732+
* @param values The list of values
733+
* @param valid Whether to get valid values
734+
* @return The sublist of values with the requested validity
735+
*/
736+
private _getValuesWithValidity(values: readonly T[], valid: boolean) {
737+
const isEqual = this.compareWith || Object.is;
738+
const validValues = (this.options || []).map(option => option.value);
739+
return values.filter(
740+
value => valid === validValues.some(validValue => isEqual(value, validValue)),
741+
);
742+
}
709743
}
710744

711745
/** Change event that is fired whenever the value of the listbox changes. */
712746
export interface ListboxValueChangeEvent<T> {
713747
/** The new value of the listbox. */
714-
readonly value: T[];
748+
readonly value: readonly T[];
715749

716750
/** Reference to the listbox that emitted the event. */
717751
readonly listbox: CdkListbox<T>;

0 commit comments

Comments
 (0)