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