@@ -17,7 +17,6 @@ import {
17
17
InjectFlags ,
18
18
Input ,
19
19
OnDestroy ,
20
- Optional ,
21
20
Output ,
22
21
QueryList ,
23
22
} from '@angular/core' ;
@@ -211,13 +210,12 @@ export class CdkListbox<T = unknown>
211
210
212
211
/** The value selected in the listbox, represented as an array of option values. */
213
212
@Input ( 'cdkListboxValue' )
214
- get value ( ) : T [ ] {
215
- return this . _value ;
213
+ get value ( ) : readonly T [ ] {
214
+ return this . selectionModel ( ) . selected ;
216
215
}
217
- set value ( value : T [ ] ) {
216
+ set value ( value : readonly T [ ] ) {
218
217
this . _setSelection ( value ) ;
219
218
}
220
- private _value : T [ ] = [ ] ;
221
219
222
220
/**
223
221
* Whether the listbox allows multiple options to be selected. If the value switches from `true`
@@ -292,10 +290,10 @@ export class CdkListbox<T = unknown>
292
290
protected readonly changeDetectorRef = inject ( ChangeDetectorRef ) ;
293
291
294
292
/** Callback called when the listbox has been touched */
295
- private _onTouched : ( ) => { } ;
293
+ private _onTouched = ( ) => { } ;
296
294
297
295
/** Callback called when the listbox value changes */
298
- private _onChange : ( value : T [ ] ) => void = ( ) => { } ;
296
+ private _onChange : ( value : readonly T [ ] ) => void = ( ) => { } ;
299
297
300
298
/** Callback called when the form validator changes. */
301
299
private _onValidatorChange = ( ) => { } ;
@@ -334,12 +332,8 @@ export class CdkListbox<T = unknown>
334
332
* @return A validation error or null
335
333
*/
336
334
private _validateInvalidValues : ValidatorFn = ( control : AbstractControl ) => {
337
- const validValues = ( this . options ?? [ ] ) . map ( option => option . value ) ;
338
335
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 ) ;
343
337
if ( invalidValues . length ) {
344
338
return { 'cdkListboxInvalidValues' : { 'values' : invalidValues } } ;
345
339
}
@@ -370,6 +364,7 @@ export class CdkListbox<T = unknown>
370
364
this . _initKeyManager ( ) ;
371
365
this . _combobox ?. _registerContent ( this . id , 'listbox' ) ;
372
366
this . options . changes . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( ( ) => {
367
+ this . _updateInternalValue ( ) ;
373
368
this . _onValidatorChange ( ) ;
374
369
} ) ;
375
370
this . _optionClicked
@@ -435,7 +430,7 @@ export class CdkListbox<T = unknown>
435
430
* @param fn The callback to register
436
431
* @docs -private
437
432
*/
438
- registerOnChange ( fn : ( value : T [ ] ) => void ) : void {
433
+ registerOnChange ( fn : ( value : readonly T [ ] ) => void ) : void {
439
434
this . _onChange = fn ;
440
435
}
441
436
@@ -453,7 +448,7 @@ export class CdkListbox<T = unknown>
453
448
* @param value The new value of the listbox
454
449
* @docs -private
455
450
*/
456
- writeValue ( value : T [ ] ) : void {
451
+ writeValue ( value : readonly T [ ] ) : void {
457
452
this . _setSelection ( value ) ;
458
453
}
459
454
@@ -615,7 +610,7 @@ export class CdkListbox<T = unknown>
615
610
this . selectionModelSubject . next (
616
611
new SelectionModel (
617
612
this . multiple ,
618
- ! this . multiple && this . value . length > 1 ? [ ] : this . value ,
613
+ ! this . multiple && this . value . length > 1 ? [ ] : this . value . slice ( ) ,
619
614
true ,
620
615
this . _compareWith ,
621
616
) ,
@@ -634,25 +629,49 @@ export class CdkListbox<T = unknown>
634
629
* Set the selected values.
635
630
* @param value The list of new selected values.
636
631
*/
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
+ ) ;
639
636
}
640
637
641
638
/** Update the internal value of the listbox based on the selection model. */
642
639
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
+ } ) ;
653
654
this . changeDetectorRef . markForCheck ( ) ;
654
655
}
655
656
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
+
656
675
/**
657
676
* Handle the user clicking an option.
658
677
* @param option The option that was clicked.
@@ -703,15 +722,29 @@ export class CdkListbox<T = unknown>
703
722
* @param value The value to coerce
704
723
* @return An array
705
724
*/
706
- private _coerceValue ( value : T [ ] ) {
725
+ private _coerceValue ( value : readonly T [ ] ) {
707
726
return value == null ? [ ] : coerceArray ( value ) ;
708
727
}
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
+ }
709
742
}
710
743
711
744
/** Change event that is fired whenever the value of the listbox changes. */
712
745
export interface ListboxValueChangeEvent < T > {
713
746
/** The new value of the listbox. */
714
- readonly value : T [ ] ;
747
+ readonly value : readonly T [ ] ;
715
748
716
749
/** Reference to the listbox that emitted the event. */
717
750
readonly listbox : CdkListbox < T > ;
0 commit comments