@@ -27,7 +27,16 @@ import {BooleanInput, coerceArray, coerceBooleanProperty} from '@angular/cdk/coe
2727import { SelectionModel } from '@angular/cdk/collections' ;
2828import { BehaviorSubject , combineLatest , defer , merge , Observable , Subject } from 'rxjs' ;
2929import { 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' ;
3140import { Directionality } from '@angular/cdk/bidi' ;
3241import { CdkCombobox } from '@angular/cdk-experimental/combobox' ;
3342
@@ -187,13 +196,19 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab
187196 useExisting : forwardRef ( ( ) => CdkListbox ) ,
188197 multi : true ,
189198 } ,
199+ {
200+ provide : NG_VALIDATORS ,
201+ useExisting : forwardRef ( ( ) => CdkListbox ) ,
202+ multi : true ,
203+ } ,
190204 ] ,
191205} )
192- export class CdkListbox < T = unknown > implements AfterContentInit , OnDestroy , ControlValueAccessor {
206+ export class CdkListbox < T = unknown >
207+ implements AfterContentInit , OnDestroy , ControlValueAccessor , Validator
208+ {
193209 /** The id of the option's host element. */
194210 @Input ( ) id = `cdk-listbox-${ nextId ++ } ` ;
195211
196- // TODO(mmalerba): Add forms validation support.
197212 /** The value selected in the listbox, represented as an array of option values. */
198213 @Input ( 'cdkListboxValue' )
199214 get value ( ) : T [ ] {
@@ -215,6 +230,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
215230 set multiple ( value : BooleanInput ) {
216231 this . _multiple = coerceBooleanProperty ( value ) ;
217232 this . _updateSelectionModel ( ) ;
233+ this . _onValidatorChange ( ) ;
218234 }
219235 private _multiple : boolean = false ;
220236
@@ -276,11 +292,14 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
276292 protected readonly changeDetectorRef = inject ( ChangeDetectorRef ) ;
277293
278294 /** Callback called when the listbox has been touched */
279- private _onTouched : ( ) => void = ( ) => { } ;
295+ private _onTouched : ( ) => { } ;
280296
281297 /** Callback called when the listbox value changes */
282298 private _onChange : ( value : T [ ] ) => void = ( ) => { } ;
283299
300+ /** Callback called when the form validator changes. */
301+ private _onValidatorChange = ( ) => { } ;
302+
284303 /** Emits when an option has been clicked. */
285304 private _optionClicked = defer ( ( ) =>
286305 ( this . options . changes as Observable < CdkOption < T > [ ] > ) . pipe (
@@ -295,6 +314,44 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
295314 // TODO(mmalerba): Should not depend on combobox
296315 private readonly _combobox = inject ( CdkCombobox , InjectFlags . Optional ) ;
297316
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+
298355 constructor ( ) {
299356 this . selectionModelSubject
300357 . pipe (
@@ -312,6 +369,9 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
312369 }
313370 this . _initKeyManager ( ) ;
314371 this . _combobox ?. _registerContent ( this . id , 'listbox' ) ;
372+ this . options . changes . pipe ( takeUntil ( this . destroyed ) ) . subscribe ( ( ) => {
373+ this . _onValidatorChange ( ) ;
374+ } ) ;
315375 this . _optionClicked
316376 . pipe (
317377 filter ( option => ! option . disabled ) ,
@@ -406,6 +466,23 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
406466 this . disabled = isDisabled ;
407467 }
408468
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+
409486 /** Focus the listbox's host element. */
410487 focus ( ) {
411488 this . element . focus ( ) ;
@@ -558,7 +635,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
558635 * @param value The list of new selected values.
559636 */
560637 private _setSelection ( value : T [ ] ) {
561- this . selectionModel ( ) . setSelection ( ...( value == null ? [ ] : coerceArray ( value ) ) ) ;
638+ this . selectionModel ( ) . setSelection ( ...this . _coerceValue ( value ) ) ;
562639 }
563640
564641 /** 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
620697 }
621698 } ) ;
622699 }
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+ }
623709}
624710
625711/** Change event that is fired whenever the value of the listbox changes. */
0 commit comments