@@ -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`
@@ -274,6 +272,7 @@ export class CdkListbox<T = unknown>
274
272
/** The child options in this listbox. */
275
273
@ContentChildren ( CdkOption , { descendants : true } ) protected options : QueryList < CdkOption < T > > ;
276
274
275
+ // TODO(mmalerba): Refactor SelectionModel so that its not necessary to create new instances
277
276
/** The selection model used by the listbox. */
278
277
protected selectionModelSubject = new BehaviorSubject (
279
278
new SelectionModel < T > ( this . multiple , [ ] , true , this . _compareWith ) ,
@@ -292,10 +291,10 @@ export class CdkListbox<T = unknown>
292
291
protected readonly changeDetectorRef = inject ( ChangeDetectorRef ) ;
293
292
294
293
/** Callback called when the listbox has been touched */
295
- private _onTouched : ( ) => { } ;
294
+ private _onTouched = ( ) => { } ;
296
295
297
296
/** Callback called when the listbox value changes */
298
- private _onChange : ( value : T [ ] ) => void = ( ) => { } ;
297
+ private _onChange : ( value : readonly T [ ] ) => void = ( ) => { } ;
299
298
300
299
/** Callback called when the form validator changes. */
301
300
private _onValidatorChange = ( ) => { } ;
@@ -334,12 +333,8 @@ export class CdkListbox<T = unknown>
334
333
* @return A validation error or null
335
334
*/
336
335
private _validateInvalidValues : ValidatorFn = ( control : AbstractControl ) => {
337
- const validValues = ( this . options ?? [ ] ) . map ( option => option . value ) ;
338
336
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 ) ;
343
338
if ( invalidValues . length ) {
344
339
return { 'cdkListboxInvalidValues' : { 'values' : invalidValues } } ;
345
340
}
@@ -370,6 +365,7 @@ export class CdkListbox<T = unknown>
370
365
this . _initKeyManager ( ) ;
371
366
this . _combobox ?. _registerContent ( this . id , 'listbox' ) ;
372
367
this . options . changes . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( ( ) => {
368
+ this . _updateInternalValue ( ) ;
373
369
this . _onValidatorChange ( ) ;
374
370
} ) ;
375
371
this . _optionClicked
@@ -435,7 +431,7 @@ export class CdkListbox<T = unknown>
435
431
* @param fn The callback to register
436
432
* @docs -private
437
433
*/
438
- registerOnChange ( fn : ( value : T [ ] ) => void ) : void {
434
+ registerOnChange ( fn : ( value : readonly T [ ] ) => void ) : void {
439
435
this . _onChange = fn ;
440
436
}
441
437
@@ -453,7 +449,7 @@ export class CdkListbox<T = unknown>
453
449
* @param value The new value of the listbox
454
450
* @docs -private
455
451
*/
456
- writeValue ( value : T [ ] ) : void {
452
+ writeValue ( value : readonly T [ ] ) : void {
457
453
this . _setSelection ( value ) ;
458
454
}
459
455
@@ -615,7 +611,7 @@ export class CdkListbox<T = unknown>
615
611
this . selectionModelSubject . next (
616
612
new SelectionModel (
617
613
this . multiple ,
618
- ! this . multiple && this . value . length > 1 ? [ ] : this . value ,
614
+ ! this . multiple && this . value . length > 1 ? [ ] : this . value . slice ( ) ,
619
615
true ,
620
616
this . _compareWith ,
621
617
) ,
@@ -634,25 +630,49 @@ export class CdkListbox<T = unknown>
634
630
* Set the selected values.
635
631
* @param value The list of new selected values.
636
632
*/
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
+ ) ;
639
637
}
640
638
641
639
/** Update the internal value of the listbox based on the selection model. */
642
640
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
+ } ) ;
653
655
this . changeDetectorRef . markForCheck ( ) ;
654
656
}
655
657
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
+
656
676
/**
657
677
* Handle the user clicking an option.
658
678
* @param option The option that was clicked.
@@ -703,15 +723,29 @@ export class CdkListbox<T = unknown>
703
723
* @param value The value to coerce
704
724
* @return An array
705
725
*/
706
- private _coerceValue ( value : T [ ] ) {
726
+ private _coerceValue ( value : readonly T [ ] ) {
707
727
return value == null ? [ ] : coerceArray ( value ) ;
708
728
}
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
+ }
709
743
}
710
744
711
745
/** Change event that is fired whenever the value of the listbox changes. */
712
746
export interface ListboxValueChangeEvent < T > {
713
747
/** The new value of the listbox. */
714
- readonly value : T [ ] ;
748
+ readonly value : readonly T [ ] ;
715
749
716
750
/** Reference to the listbox that emitted the event. */
717
751
readonly listbox : CdkListbox < T > ;
0 commit comments