Skip to content

Commit 801e83d

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

File tree

1 file changed

+62
-29
lines changed

1 file changed

+62
-29
lines changed

src/cdk-experimental/listbox/listbox.ts

Lines changed: 62 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`
@@ -292,10 +290,10 @@ export class CdkListbox<T = unknown>
292290
protected readonly changeDetectorRef = inject(ChangeDetectorRef);
293291

294292
/** Callback called when the listbox has been touched */
295-
private _onTouched: () => {};
293+
private _onTouched = () => {};
296294

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

300298
/** Callback called when the form validator changes. */
301299
private _onValidatorChange = () => {};
@@ -334,12 +332,8 @@ export class CdkListbox<T = unknown>
334332
* @return A validation error or null
335333
*/
336334
private _validateInvalidValues: ValidatorFn = (control: AbstractControl) => {
337-
const validValues = (this.options ?? []).map(option => option.value);
338335
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-
);
336+
const invalidValues = this._getValuesWithValidity(controlValue, false);
343337
if (invalidValues.length) {
344338
return {'cdkListboxInvalidValues': {'values': invalidValues}};
345339
}
@@ -370,6 +364,7 @@ export class CdkListbox<T = unknown>
370364
this._initKeyManager();
371365
this._combobox?._registerContent(this.id, 'listbox');
372366
this.options.changes.pipe(takeUntil(this.destroyed)).subscribe(() => {
367+
this._updateInternalValue();
373368
this._onValidatorChange();
374369
});
375370
this._optionClicked
@@ -435,7 +430,7 @@ export class CdkListbox<T = unknown>
435430
* @param fn The callback to register
436431
* @docs-private
437432
*/
438-
registerOnChange(fn: (value: T[]) => void): void {
433+
registerOnChange(fn: (value: readonly T[]) => void): void {
439434
this._onChange = fn;
440435
}
441436

@@ -453,7 +448,7 @@ export class CdkListbox<T = unknown>
453448
* @param value The new value of the listbox
454449
* @docs-private
455450
*/
456-
writeValue(value: T[]): void {
451+
writeValue(value: readonly T[]): void {
457452
this._setSelection(value);
458453
}
459454

@@ -615,7 +610,7 @@ export class CdkListbox<T = unknown>
615610
this.selectionModelSubject.next(
616611
new SelectionModel(
617612
this.multiple,
618-
!this.multiple && this.value.length > 1 ? [] : this.value,
613+
!this.multiple && this.value.length > 1 ? [] : this.value.slice(),
619614
true,
620615
this._compareWith,
621616
),
@@ -634,25 +629,49 @@ export class CdkListbox<T = unknown>
634629
* Set the selected values.
635630
* @param value The list of new selected values.
636631
*/
637-
private _setSelection(value: T[]) {
638-
this.selectionModel().setSelection(...this._coerceValue(value));
632+
private _setSelection(value: readonly T[]) {
633+
this.selectionModel().setSelection(
634+
...this._getValuesWithValidity(this._coerceValue(value), true),
635+
);
639636
}
640637

641638
/** Update the internal value of the listbox based on the selection model. */
642639
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-
}, []);
640+
const options = [...this.options];
641+
const indexCache = new Map<T, number>();
642+
// Check if we need to remove any values due to them becoming invalid
643+
// (e.g. if the option was removed from the DOM.)
644+
const selected = this.selectionModel().selected;
645+
const validSelected = this._getValuesWithValidity(selected, true);
646+
if (validSelected.length != selected.length) {
647+
this.selectionModel().setSelection(...validSelected);
648+
}
649+
this.selectionModel().sort((a: T, b: T) => {
650+
const aIndex = this._getIndexForValue(indexCache, options, a);
651+
const bIndex = this._getIndexForValue(indexCache, options, b);
652+
return aIndex - bIndex;
653+
});
653654
this.changeDetectorRef.markForCheck();
654655
}
655656

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

711744
/** Change event that is fired whenever the value of the listbox changes. */
712745
export interface ListboxValueChangeEvent<T> {
713746
/** The new value of the listbox. */
714-
readonly value: T[];
747+
readonly value: readonly T[];
715748

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

0 commit comments

Comments
 (0)