Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(material/autocomplete): don't assign to model value while typing when requireSelection is enabled #27572

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
min-width: 150px;
max-width: 500px;
width: 100%;
margin-top: 16px;
}

.example-full-width {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
Control value: {{myControl.value || 'empty'}}

<form class="example-form">
<mat-form-field class="example-full-width">
<mat-label>Number</mat-label>
<input type="text"
<input #input
type="text"
placeholder="Pick one"
aria-label="Number"
matInput
[formControl]="myControl"
[matAutocomplete]="auto">
[matAutocomplete]="auto"
(input)="filter()"
(focus)="filter()">
<mat-autocomplete requireSelection #auto="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
<mat-option *ngFor="let option of filteredOptions" [value]="option">
{{option}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>

Control value: {{myControl.value}}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import {Component, OnInit} from '@angular/core';
import {Component, ElementRef, ViewChild} from '@angular/core';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Observable} from 'rxjs';
import {map, startWith} from 'rxjs/operators';
import {NgFor, AsyncPipe} from '@angular/common';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';

/**
* @title Require an autocomplete option to be selected.
* @title Require an autocomplete option to be selected
*/
@Component({
selector: 'autocomplete-require-selection-example',
Expand All @@ -25,21 +23,18 @@ import {MatFormFieldModule} from '@angular/material/form-field';
AsyncPipe,
],
})
export class AutocompleteRequireSelectionExample implements OnInit {
export class AutocompleteRequireSelectionExample {
@ViewChild('input') input: ElementRef<HTMLInputElement>;
myControl = new FormControl('');
options: string[] = ['One', 'Two', 'Three', 'Three', 'Four'];
filteredOptions: Observable<string[]>;
options: string[] = ['One', 'Two', 'Three', 'Four', 'Five'];
filteredOptions: string[];

ngOnInit() {
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value || '')),
);
constructor() {
this.filteredOptions = this.options.slice();
}

private _filter(value: string): string[] {
const filterValue = value.toLowerCase();

return this.options.filter(option => option.toLowerCase().includes(filterValue));
filter(): void {
const filterValue = this.input.nativeElement.value.toLowerCase();
this.filteredOptions = this.options.filter(o => o.toLowerCase().includes(filterValue));
}
}
14 changes: 10 additions & 4 deletions src/dev-app/autocomplete/autocomplete-demo.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
Space above cards: <input type="number" [formControl]="topHeightCtrl">
<div [style.height.px]="topHeightCtrl.value"></div>
<div class="demo-autocomplete">
<mat-card *ngIf="(reactiveStates | async) as tempStates">
Reactive length: {{ tempStates?.length }}
<mat-card>
Reactive length: {{ reactiveStates.length }}
<div>Reactive value: {{ stateCtrl.value | json }}</div>
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>

<mat-form-field [color]="reactiveStatesTheme">
<mat-label>State</mat-label>
<input matInput [matAutocomplete]="reactiveAuto" [formControl]="stateCtrl">
<input
#reactiveInput
matInput
[matAutocomplete]="reactiveAuto"
[formControl]="stateCtrl"
(input)="reactiveStates = filterStates(reactiveInput.value)"
(focus)="reactiveStates = filterStates(reactiveInput.value)">
</mat-form-field>
<mat-autocomplete #reactiveAuto="matAutocomplete"
[displayWith]="displayFn"
[hideSingleSelectionIndicator]="reactiveHideSingleSelectionIndicator"
[autoActiveFirstOption]="reactiveAutoActiveFirstOption"
[requireSelection]="reactiveRequireSelection">
<mat-option *ngFor="let state of tempStates; let index = index" [value]="state"
<mat-option *ngFor="let state of reactiveStates; let index = index" [value]="state"
[disabled]="reactiveIsStateDisabled(state.index)">
<span>{{ state.name }}</span>
<span class="demo-secondary-text"> ({{ state.code }}) </span>
Expand Down
14 changes: 4 additions & 10 deletions src/dev-app/autocomplete/autocomplete-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatInputModule} from '@angular/material/input';
import {Observable} from 'rxjs';
import {map, startWith} from 'rxjs/operators';
import {ThemePalette} from '@angular/material/core';
import {MatDialog, MatDialogModule, MatDialogRef} from '@angular/material/dialog';

Expand Down Expand Up @@ -50,12 +48,12 @@ type DisableStateOption = 'none' | 'first-middle-last' | 'all';
],
})
export class AutocompleteDemo {
stateCtrl = new FormControl({code: 'CA', name: 'California'});
stateCtrl = new FormControl();
currentState = '';
currentGroupedState = '';
topHeightCtrl = new FormControl(0);

reactiveStates: Observable<State[]>;
reactiveStates: State[];
tdStates: State[];

tdDisabled = false;
Expand Down Expand Up @@ -138,12 +136,8 @@ export class AutocompleteDemo {
].map((state, index) => ({...state, index}));

constructor() {
this.tdStates = this.states;
this.reactiveStates = this.stateCtrl.valueChanges.pipe(
startWith(this.stateCtrl.value),
map(val => this.displayFn(val)),
map(name => this.filterStates(name)),
);
this.tdStates = this.states.slice();
this.reactiveStates = this.states.slice();

this.filteredGroupedStates = this.groupedStates = this.states.reduce<StateGroup[]>(
(groups, state) => {
Expand Down
8 changes: 7 additions & 1 deletion src/material/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,13 @@ export abstract class _MatAutocompleteTriggerBase
if (this._previousValue !== value) {
this._previousValue = value;
this._pendingAutoselectedOption = null;
this._onChange(value);

// If selection is required we don't write to the CVA while the user is typing.
// At the end of the selection either the user will have picked something
// or we'll reset the value back to null.
if (!this.autocomplete || !this.autocomplete.requireSelection) {
this._onChange(value);
}

if (!value) {
this._clearPreviousSelectedOption(null, false);
Expand Down
2 changes: 1 addition & 1 deletion src/material/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2622,7 +2622,7 @@ describe('MDC-based MatAutocomplete', () => {
tick();

expect(input.value).toBe('Cali');
expect(stateCtrl.value).toBe('Cali');
expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});
expect(spy).not.toHaveBeenCalled();

dispatchFakeEvent(document, 'click');
Expand Down
Loading