Skip to content

Commit 3d5ad3f

Browse files
authored
fix(cdk/listbox): remove incorrect usage of validator (#25856)
Currently the CDK listbox is providing itself as a validator so that it can validate that programmatically-assigned values are valid. This is incorrect, because validators are meant to check user-assigned values and the checks that are currently being performed can't happen purely through the UI. These changes remove the `Validator` implementation and replace the checks with runtime errors.
1 parent 68d81a4 commit 3d5ad3f

File tree

3 files changed

+26
-105
lines changed

3 files changed

+26
-105
lines changed

src/cdk/listbox/listbox.spec.ts

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -852,44 +852,28 @@ describe('CdkOption and CdkListbox', () => {
852852
subscription.unsubscribe();
853853
});
854854

855-
it('should have FormControl error when multiple values selected in single-select listbox', () => {
855+
it('should throw when multiple values selected in single-select listbox', () => {
856856
const {testComponent, fixture} = setupComponent(ListboxWithFormControl, [
857857
ReactiveFormsModule,
858858
]);
859-
testComponent.formControl.setValue(['orange', 'banana']);
860-
fixture.detectChanges();
861859

862-
expect(testComponent.formControl.hasError('cdkListboxUnexpectedMultipleValues')).toBeTrue();
863-
expect(testComponent.formControl.hasError('cdkListboxUnexpectedOptionValues')).toBeFalse();
860+
expect(() => {
861+
testComponent.formControl.setValue(['orange', 'banana']);
862+
fixture.detectChanges();
863+
}).toThrowError('Listbox cannot have more than one selected value in multi-selection mode.');
864864
});
865865

866-
it('should have FormControl error when non-option value selected', () => {
866+
it('should throw when an invalid value is selected', () => {
867867
const {testComponent, fixture} = setupComponent(ListboxWithFormControl, [
868868
ReactiveFormsModule,
869869
]);
870870
testComponent.isMultiselectable = true;
871-
testComponent.formControl.setValue(['orange', 'dragonfruit', 'mango']);
872871
fixture.detectChanges();
873872

874-
expect(testComponent.formControl.hasError('cdkListboxUnexpectedOptionValues')).toBeTrue();
875-
expect(testComponent.formControl.hasError('cdkListboxUnexpectedMultipleValues')).toBeFalse();
876-
expect(testComponent.formControl.errors?.['cdkListboxUnexpectedOptionValues']).toEqual({
877-
'values': ['dragonfruit', 'mango'],
878-
});
879-
});
880-
881-
it('should have multiple FormControl errors when multiple non-option values selected in single-select listbox', () => {
882-
const {testComponent, fixture} = setupComponent(ListboxWithFormControl, [
883-
ReactiveFormsModule,
884-
]);
885-
testComponent.formControl.setValue(['dragonfruit', 'mango']);
886-
fixture.detectChanges();
887-
888-
expect(testComponent.formControl.hasError('cdkListboxUnexpectedOptionValues')).toBeTrue();
889-
expect(testComponent.formControl.hasError('cdkListboxUnexpectedMultipleValues')).toBeTrue();
890-
expect(testComponent.formControl.errors?.['cdkListboxUnexpectedOptionValues']).toEqual({
891-
'values': ['dragonfruit', 'mango'],
892-
});
873+
expect(() => {
874+
testComponent.formControl.setValue(['orange', 'dragonfruit', 'mango']);
875+
fixture.detectChanges();
876+
}).toThrowError('Listbox has selected values that do not match any of its options.');
893877
});
894878
});
895879
});

src/cdk/listbox/listbox.ts

Lines changed: 15 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,7 @@ import {BooleanInput, coerceArray, coerceBooleanProperty} from '@angular/cdk/coe
3636
import {SelectionModel} from '@angular/cdk/collections';
3737
import {defer, merge, Observable, Subject} from 'rxjs';
3838
import {filter, map, startWith, switchMap, takeUntil} from 'rxjs/operators';
39-
import {
40-
AbstractControl,
41-
ControlValueAccessor,
42-
NG_VALIDATORS,
43-
NG_VALUE_ACCESSOR,
44-
ValidationErrors,
45-
Validator,
46-
ValidatorFn,
47-
Validators,
48-
} from '@angular/forms';
39+
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
4940
import {Directionality} from '@angular/cdk/bidi';
5041

5142
/** The next id to use for creating unique DOM IDs. */
@@ -256,16 +247,9 @@ export class CdkOption<T = unknown> implements ListKeyManagerOption, Highlightab
256247
useExisting: forwardRef(() => CdkListbox),
257248
multi: true,
258249
},
259-
{
260-
provide: NG_VALIDATORS,
261-
useExisting: forwardRef(() => CdkListbox),
262-
multi: true,
263-
},
264250
],
265251
})
266-
export class CdkListbox<T = unknown>
267-
implements AfterContentInit, OnDestroy, ControlValueAccessor, Validator
268-
{
252+
export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, ControlValueAccessor {
269253
/** The id of the option's host element. */
270254
@Input()
271255
get id() {
@@ -416,9 +400,6 @@ export class CdkListbox<T = unknown>
416400
/** Callback called when the listbox value changes */
417401
private _onChange: (value: readonly T[]) => void = () => {};
418402

419-
/** Callback called when the form validator changes. */
420-
private _onValidatorChange = () => {};
421-
422403
/** Emits when an option has been clicked. */
423404
private _optionClicked = defer(() =>
424405
(this.options.changes as Observable<CdkOption<T>[]>).pipe(
@@ -438,40 +419,6 @@ export class CdkListbox<T = unknown>
438419
/** A predicate that does not skip any options. */
439420
private readonly _skipNonePredicate = () => false;
440421

441-
/**
442-
* Validator that produces an error if multiple values are selected in a single selection
443-
* listbox.
444-
* @param control The control to validate
445-
* @return A validation error or null
446-
*/
447-
private _validateUnexpectedMultipleValues: ValidatorFn = (control: AbstractControl) => {
448-
const controlValue = this._coerceValue(control.value);
449-
if (!this.multiple && controlValue.length > 1) {
450-
return {'cdkListboxUnexpectedMultipleValues': true};
451-
}
452-
return null;
453-
};
454-
455-
/**
456-
* Validator that produces an error if any selected values are not valid options for this listbox.
457-
* @param control The control to validate
458-
* @return A validation error or null
459-
*/
460-
private _validateUnexpectedOptionValues: ValidatorFn = (control: AbstractControl) => {
461-
const controlValue = this._coerceValue(control.value);
462-
const invalidValues = this._getInvalidOptionValues(controlValue);
463-
if (invalidValues.length) {
464-
return {'cdkListboxUnexpectedOptionValues': {'values': invalidValues}};
465-
}
466-
return null;
467-
};
468-
469-
/** The combined set of validators for this listbox. */
470-
private _validators = Validators.compose([
471-
this._validateUnexpectedMultipleValues,
472-
this._validateUnexpectedOptionValues,
473-
])!;
474-
475422
ngAfterContentInit() {
476423
if (typeof ngDevMode === 'undefined' || ngDevMode) {
477424
this._verifyNoOptionValueCollisions();
@@ -614,6 +561,19 @@ export class CdkListbox<T = unknown>
614561
*/
615562
writeValue(value: readonly T[]): void {
616563
this._setSelection(value);
564+
565+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
566+
const selected = this.selectionModel.selected;
567+
const invalidValues = this._getInvalidOptionValues(selected);
568+
569+
if (!this.multiple && selected.length > 1) {
570+
throw Error('Listbox cannot have more than one selected value in multi-selection mode.');
571+
}
572+
573+
if (invalidValues.length) {
574+
throw Error('Listbox has selected values that do not match any of its options.');
575+
}
576+
}
617577
}
618578

619579
/**
@@ -625,23 +585,6 @@ export class CdkListbox<T = unknown>
625585
this.disabled = isDisabled;
626586
}
627587

628-
/**
629-
* Validate the given control
630-
* @docs-private
631-
*/
632-
validate(control: AbstractControl<any, any>): ValidationErrors | null {
633-
return this._validators(control);
634-
}
635-
636-
/**
637-
* Registers a callback to be called when the form validator changes.
638-
* @param fn The callback to call
639-
* @docs-private
640-
*/
641-
registerOnValidatorChange(fn: () => void) {
642-
this._onValidatorChange = fn;
643-
}
644-
645588
/** Focus the listbox's host element. */
646589
focus() {
647590
this.element.focus();
@@ -901,7 +844,6 @@ export class CdkListbox<T = unknown>
901844
const selected = this.selectionModel.selected;
902845
this._invalid =
903846
(!this.multiple && selected.length > 1) || !!this._getInvalidOptionValues(selected).length;
904-
this._onValidatorChange();
905847
this.changeDetectorRef.markForCheck();
906848
}
907849

tools/public_api_guard/cdk/listbox.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
55
```ts
66

7-
import { AbstractControl } from '@angular/forms';
87
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
98
import { AfterContentInit } from '@angular/core';
109
import { BooleanInput } from '@angular/cdk/coercion';
@@ -17,11 +16,9 @@ import { OnDestroy } from '@angular/core';
1716
import { QueryList } from '@angular/core';
1817
import { SelectionModel } from '@angular/cdk/collections';
1918
import { Subject } from 'rxjs';
20-
import { ValidationErrors } from '@angular/forms';
21-
import { Validator } from '@angular/forms';
2219

2320
// @public (undocumented)
24-
export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, ControlValueAccessor, Validator {
21+
export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, ControlValueAccessor {
2522
protected readonly changeDetectorRef: ChangeDetectorRef;
2623
get compareWith(): undefined | ((o1: T, o2: T) => boolean);
2724
set compareWith(fn: undefined | ((o1: T, o2: T) => boolean));
@@ -59,7 +56,6 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
5956
set orientation(value: 'horizontal' | 'vertical');
6057
registerOnChange(fn: (value: readonly T[]) => void): void;
6158
registerOnTouched(fn: () => {}): void;
62-
registerOnValidatorChange(fn: () => void): void;
6359
select(option: CdkOption<T>): void;
6460
protected selectionModel: ListboxSelectionModel<T>;
6561
selectValue(value: T): void;
@@ -72,7 +68,6 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
7268
protected triggerRange(trigger: CdkOption<T> | null, from: number, to: number, on: boolean): void;
7369
get useActiveDescendant(): boolean;
7470
set useActiveDescendant(shouldUseActiveDescendant: BooleanInput);
75-
validate(control: AbstractControl<any, any>): ValidationErrors | null;
7671
get value(): readonly T[];
7772
set value(value: readonly T[]);
7873
readonly valueChange: Subject<ListboxValueChangeEvent<T>>;

0 commit comments

Comments
 (0)