@@ -27,7 +27,16 @@ import {BooleanInput, coerceArray, coerceBooleanProperty} from '@angular/cdk/coe
27
27
import { SelectionModel } from '@angular/cdk/collections' ;
28
28
import { BehaviorSubject , combineLatest , defer , merge , Observable , Subject } from 'rxjs' ;
29
29
import { filter , mapTo , startWith , switchMap , take , takeUntil } from 'rxjs/operators' ;
30
- import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ;
30
+ import {
31
+ AbstractControl ,
32
+ ControlValueAccessor ,
33
+ NG_VALIDATORS ,
34
+ NG_VALUE_ACCESSOR ,
35
+ ValidationErrors ,
36
+ Validator ,
37
+ ValidatorFn ,
38
+ Validators ,
39
+ } from '@angular/forms' ;
31
40
import { Directionality } from '@angular/cdk/bidi' ;
32
41
import { CdkCombobox } from '@angular/cdk-experimental/combobox' ;
33
42
@@ -187,13 +196,19 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab
187
196
useExisting : forwardRef ( ( ) => CdkListbox ) ,
188
197
multi : true ,
189
198
} ,
199
+ {
200
+ provide : NG_VALIDATORS ,
201
+ useExisting : forwardRef ( ( ) => CdkListbox ) ,
202
+ multi : true ,
203
+ } ,
190
204
] ,
191
205
} )
192
- export class CdkListbox < T = unknown > implements AfterContentInit , OnDestroy , ControlValueAccessor {
206
+ export class CdkListbox < T = unknown >
207
+ implements AfterContentInit , OnDestroy , ControlValueAccessor , Validator
208
+ {
193
209
/** The id of the option's host element. */
194
210
@Input ( ) id = `cdk-listbox-${ nextId ++ } ` ;
195
211
196
- // TODO(mmalerba): Add forms validation support.
197
212
/** The value selected in the listbox, represented as an array of option values. */
198
213
@Input ( 'cdkListboxValue' )
199
214
get value ( ) : T [ ] {
@@ -215,6 +230,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
215
230
set multiple ( value : BooleanInput ) {
216
231
this . _multiple = coerceBooleanProperty ( value ) ;
217
232
this . _updateSelectionModel ( ) ;
233
+ this . _onValidatorChange ( ) ;
218
234
}
219
235
private _multiple : boolean = false ;
220
236
@@ -276,11 +292,14 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
276
292
protected readonly changeDetectorRef = inject ( ChangeDetectorRef ) ;
277
293
278
294
/** Callback called when the listbox has been touched */
279
- private _onTouched : ( ) => void = ( ) => { } ;
295
+ private _onTouched : ( ) => { } ;
280
296
281
297
/** Callback called when the listbox value changes */
282
298
private _onChange : ( value : T [ ] ) => void = ( ) => { } ;
283
299
300
+ /** Callback called when the form validator changes. */
301
+ private _onValidatorChange = ( ) => { } ;
302
+
284
303
/** Emits when an option has been clicked. */
285
304
private _optionClicked = defer ( ( ) =>
286
305
( this . options . changes as Observable < CdkOption < T > [ ] > ) . pipe (
@@ -295,6 +314,44 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
295
314
// TODO(mmalerba): Should not depend on combobox
296
315
private readonly _combobox = inject ( CdkCombobox , InjectFlags . Optional ) ;
297
316
317
+ /**
318
+ * Validator that produces an error if multiple values are selected in a single selection
319
+ * listbox.
320
+ * @param control The control to validate
321
+ * @return A validation error or null
322
+ */
323
+ private _validateMultipleValues : ValidatorFn = ( control : AbstractControl ) => {
324
+ const controlValue = this . _coerceValue ( control . value ) ;
325
+ if ( ! this . multiple && controlValue . length > 1 ) {
326
+ return { 'cdkListboxMultipleValues' : true } ;
327
+ }
328
+ return null ;
329
+ } ;
330
+
331
+ /**
332
+ * Validator that produces an error if any selected values are not valid options for this listbox.
333
+ * @param control The control to validate
334
+ * @return A validation error or null
335
+ */
336
+ private _validateInvalidValues : ValidatorFn = ( control : AbstractControl ) => {
337
+ const validValues = ( this . options ?? [ ] ) . map ( option => option . value ) ;
338
+ 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
+ ) ;
343
+ if ( invalidValues . length ) {
344
+ return { 'cdkListboxInvalidValues' : { 'values' : invalidValues } } ;
345
+ }
346
+ return null ;
347
+ } ;
348
+
349
+ /** The combined set of validators for this listbox. */
350
+ private _validators = Validators . compose ( [
351
+ this . _validateMultipleValues ,
352
+ this . _validateInvalidValues ,
353
+ ] ) ! ;
354
+
298
355
constructor ( ) {
299
356
this . selectionModelSubject
300
357
. pipe (
@@ -312,6 +369,9 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
312
369
}
313
370
this . _initKeyManager ( ) ;
314
371
this . _combobox ?. _registerContent ( this . id , 'listbox' ) ;
372
+ this . options . changes . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( ( ) => {
373
+ this . _onValidatorChange ( ) ;
374
+ } ) ;
315
375
this . _optionClicked
316
376
. pipe (
317
377
filter ( option => ! option . disabled ) ,
@@ -406,6 +466,23 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
406
466
this . disabled = isDisabled ;
407
467
}
408
468
469
+ /**
470
+ * Validate the given control
471
+ * @docs -private
472
+ */
473
+ validate ( control : AbstractControl < any , any > ) : ValidationErrors | null {
474
+ return this . _validators ( control ) ;
475
+ }
476
+
477
+ /**
478
+ * Registers a callback to be called when the form validator changes.
479
+ * @param fn The callback to call
480
+ * @docs -private
481
+ */
482
+ registerOnValidatorChange ( fn : ( ) => void ) {
483
+ this . _onValidatorChange = fn ;
484
+ }
485
+
409
486
/** Focus the listbox's host element. */
410
487
focus ( ) {
411
488
this . element . focus ( ) ;
@@ -558,7 +635,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
558
635
* @param value The list of new selected values.
559
636
*/
560
637
private _setSelection ( value : T [ ] ) {
561
- this . selectionModel ( ) . setSelection ( ...( value == null ? [ ] : coerceArray ( value ) ) ) ;
638
+ this . selectionModel ( ) . setSelection ( ...this . _coerceValue ( value ) ) ;
562
639
}
563
640
564
641
/** Update the internal value of the listbox based on the selection model. */
@@ -620,6 +697,15 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
620
697
}
621
698
} ) ;
622
699
}
700
+
701
+ /**
702
+ * Coerces a value into an array representing a listbox selection.
703
+ * @param value The value to coerce
704
+ * @return An array
705
+ */
706
+ private _coerceValue ( value : T [ ] ) {
707
+ return value == null ? [ ] : coerceArray ( value ) ;
708
+ }
623
709
}
624
710
625
711
/** Change event that is fired whenever the value of the listbox changes. */
0 commit comments