@@ -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. */
712745export 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