Skip to content

Commit

Permalink
fix(cdk/listbox): incorrectly validating preselected value (#25893)
Browse files Browse the repository at this point in the history
In #25856 the check that verifies the validity of the form control value was moved into `writeValue`. This works as expected for assignments after initialization, but it throws an error incorrectly if there is a preselected value, because `writeValue` will be called before the options are available.

These changes resolve the issue by checking that the options have been initialized before throwing the error.

(cherry picked from commit 166ed5e)
  • Loading branch information
crisbeto committed Oct 29, 2022
1 parent 8baae73 commit 992cafc
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 13 deletions.
36 changes: 36 additions & 0 deletions src/cdk/listbox/listbox.spec.ts
Expand Up @@ -883,6 +883,18 @@ describe('CdkOption and CdkListbox', () => {
fixture.detectChanges();
}).toThrowError('Listbox has selected values that do not match any of its options.');
});

it('should not throw on init with a preselected form control and a dynamic set of options', () => {
expect(() => {
setupComponent(ListboxWithPreselectedFormControl, [ReactiveFormsModule]);
}).not.toThrow();
});

it('should throw on init if the preselected value is invalid', () => {
expect(() => {
setupComponent(ListboxWithInvalidPreselectedFormControl, [ReactiveFormsModule]);
}).toThrowError('Listbox has selected values that do not match any of its options.');
});
});
});

Expand Down Expand Up @@ -955,6 +967,30 @@ class ListboxWithFormControl {
isActiveDescendant = false;
}

@Component({
template: `
<div cdkListbox [formControl]="formControl">
<div *ngFor="let option of options" [cdkOption]="option">{{option}}</div>
</div>
`,
})
class ListboxWithPreselectedFormControl {
options = ['a', 'b', 'c'];
formControl = new FormControl('c');
}

@Component({
template: `
<div cdkListbox [formControl]="formControl">
<div *ngFor="let option of options" [cdkOption]="option">{{option}}</div>
</div>
`,
})
class ListboxWithInvalidPreselectedFormControl {
options = ['a', 'b', 'c'];
formControl = new FormControl('d');
}

@Component({
template: `
<ul cdkListbox>
Expand Down
31 changes: 18 additions & 13 deletions src/cdk/listbox/listbox.ts
Expand Up @@ -422,6 +422,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
ngAfterContentInit() {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
this._verifyNoOptionValueCollisions();
this._verifyOptionValues();
}

this._initKeyManager();
Expand Down Expand Up @@ -561,19 +562,7 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
*/
writeValue(value: readonly T[]): void {
this._setSelection(value);

if (typeof ngDevMode === 'undefined' || ngDevMode) {
const selected = this.selectionModel.selected;
const invalidValues = this._getInvalidOptionValues(selected);

if (!this.multiple && selected.length > 1) {
throw Error('Listbox cannot have more than one selected value in multi-selection mode.');
}

if (invalidValues.length) {
throw Error('Listbox has selected values that do not match any of its options.');
}
}
this._verifyOptionValues();
}

/**
Expand Down Expand Up @@ -924,6 +913,22 @@ export class CdkListbox<T = unknown> implements AfterContentInit, OnDestroy, Con
});
}

/** Verifies that the option values are valid. */
private _verifyOptionValues() {
if (this.options && (typeof ngDevMode === 'undefined' || ngDevMode)) {
const selected = this.selectionModel.selected;
const invalidValues = this._getInvalidOptionValues(selected);

if (!this.multiple && selected.length > 1) {
throw Error('Listbox cannot have more than one selected value in multi-selection mode.');
}

if (invalidValues.length) {
throw Error('Listbox has selected values that do not match any of its options.');
}
}
}

/**
* Coerces a value into an array representing a listbox selection.
* @param value The value to coerce
Expand Down

0 comments on commit 992cafc

Please sign in to comment.