Skip to content

Commit 77ffdf9

Browse files
authored
fix(material/autocomplete): don't assign to model value while typing when requireSelection is enabled (#27572)
Follow-up to #27423 based on the feedback. Usually `mat-autocomplete` assigns to the model as the user is typing which may not be desired when `requireSelection` is enabled, because at the end of the selection either an option value will set or it'll be reset. These changes add a condition so that the value isn't assigned while typing and `requireSelection` is enabled.
1 parent b13c6aa commit 77ffdf9

File tree

7 files changed

+42
-38
lines changed

7 files changed

+42
-38
lines changed

src/components-examples/material/autocomplete/autocomplete-require-selection/autocomplete-require-selection-example.css

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
min-width: 150px;
33
max-width: 500px;
44
width: 100%;
5+
margin-top: 16px;
56
}
67

78
.example-full-width {

src/components-examples/material/autocomplete/autocomplete-require-selection/autocomplete-require-selection-example.html

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
Control value: {{myControl.value || 'empty'}}
2+
13
<form class="example-form">
24
<mat-form-field class="example-full-width">
35
<mat-label>Number</mat-label>
4-
<input type="text"
6+
<input #input
7+
type="text"
58
placeholder="Pick one"
6-
aria-label="Number"
79
matInput
810
[formControl]="myControl"
9-
[matAutocomplete]="auto">
11+
[matAutocomplete]="auto"
12+
(input)="filter()"
13+
(focus)="filter()">
1014
<mat-autocomplete requireSelection #auto="matAutocomplete">
11-
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
15+
<mat-option *ngFor="let option of filteredOptions" [value]="option">
1216
{{option}}
1317
</mat-option>
1418
</mat-autocomplete>
1519
</mat-form-field>
1620
</form>
17-
18-
Control value: {{myControl.value}}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import {Component, OnInit} from '@angular/core';
1+
import {Component, ElementRef, ViewChild} from '@angular/core';
22
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
3-
import {Observable} from 'rxjs';
4-
import {map, startWith} from 'rxjs/operators';
53
import {NgFor, AsyncPipe} from '@angular/common';
64
import {MatAutocompleteModule} from '@angular/material/autocomplete';
75
import {MatInputModule} from '@angular/material/input';
86
import {MatFormFieldModule} from '@angular/material/form-field';
97

108
/**
11-
* @title Require an autocomplete option to be selected.
9+
* @title Require an autocomplete option to be selected
1210
*/
1311
@Component({
1412
selector: 'autocomplete-require-selection-example',
@@ -25,21 +23,18 @@ import {MatFormFieldModule} from '@angular/material/form-field';
2523
AsyncPipe,
2624
],
2725
})
28-
export class AutocompleteRequireSelectionExample implements OnInit {
26+
export class AutocompleteRequireSelectionExample {
27+
@ViewChild('input') input: ElementRef<HTMLInputElement>;
2928
myControl = new FormControl('');
30-
options: string[] = ['One', 'Two', 'Three', 'Three', 'Four'];
31-
filteredOptions: Observable<string[]>;
29+
options: string[] = ['One', 'Two', 'Three', 'Four', 'Five'];
30+
filteredOptions: string[];
3231

33-
ngOnInit() {
34-
this.filteredOptions = this.myControl.valueChanges.pipe(
35-
startWith(''),
36-
map(value => this._filter(value || '')),
37-
);
32+
constructor() {
33+
this.filteredOptions = this.options.slice();
3834
}
3935

40-
private _filter(value: string): string[] {
41-
const filterValue = value.toLowerCase();
42-
43-
return this.options.filter(option => option.toLowerCase().includes(filterValue));
36+
filter(): void {
37+
const filterValue = this.input.nativeElement.value.toLowerCase();
38+
this.filteredOptions = this.options.filter(o => o.toLowerCase().includes(filterValue));
4439
}
4540
}

src/dev-app/autocomplete/autocomplete-demo.html

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
Space above cards: <input type="number" [formControl]="topHeightCtrl">
22
<div [style.height.px]="topHeightCtrl.value"></div>
33
<div class="demo-autocomplete">
4-
<mat-card *ngIf="(reactiveStates | async) as tempStates">
5-
Reactive length: {{ tempStates?.length }}
4+
<mat-card>
5+
Reactive length: {{ reactiveStates.length }}
66
<div>Reactive value: {{ stateCtrl.value | json }}</div>
77
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>
88

99
<mat-form-field [color]="reactiveStatesTheme">
1010
<mat-label>State</mat-label>
11-
<input matInput [matAutocomplete]="reactiveAuto" [formControl]="stateCtrl">
11+
<input
12+
#reactiveInput
13+
matInput
14+
[matAutocomplete]="reactiveAuto"
15+
[formControl]="stateCtrl"
16+
(input)="reactiveStates = filterStates(reactiveInput.value)"
17+
(focus)="reactiveStates = filterStates(reactiveInput.value)">
1218
</mat-form-field>
1319
<mat-autocomplete #reactiveAuto="matAutocomplete"
1420
[displayWith]="displayFn"
1521
[hideSingleSelectionIndicator]="reactiveHideSingleSelectionIndicator"
1622
[autoActiveFirstOption]="reactiveAutoActiveFirstOption"
1723
[requireSelection]="reactiveRequireSelection">
18-
<mat-option *ngFor="let state of tempStates; let index = index" [value]="state"
24+
<mat-option *ngFor="let state of reactiveStates; let index = index" [value]="state"
1925
[disabled]="reactiveIsStateDisabled(state.index)">
2026
<span>{{ state.name }}</span>
2127
<span class="demo-secondary-text"> ({{ state.code }}) </span>

src/dev-app/autocomplete/autocomplete-demo.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import {MatButtonModule} from '@angular/material/button';
1414
import {MatCardModule} from '@angular/material/card';
1515
import {MatCheckboxModule} from '@angular/material/checkbox';
1616
import {MatInputModule} from '@angular/material/input';
17-
import {Observable} from 'rxjs';
18-
import {map, startWith} from 'rxjs/operators';
1917
import {ThemePalette} from '@angular/material/core';
2018
import {MatDialog, MatDialogModule, MatDialogRef} from '@angular/material/dialog';
2119

@@ -50,12 +48,12 @@ type DisableStateOption = 'none' | 'first-middle-last' | 'all';
5048
],
5149
})
5250
export class AutocompleteDemo {
53-
stateCtrl = new FormControl({code: 'CA', name: 'California'});
51+
stateCtrl = new FormControl();
5452
currentState = '';
5553
currentGroupedState = '';
5654
topHeightCtrl = new FormControl(0);
5755

58-
reactiveStates: Observable<State[]>;
56+
reactiveStates: State[];
5957
tdStates: State[];
6058

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

140138
constructor() {
141-
this.tdStates = this.states;
142-
this.reactiveStates = this.stateCtrl.valueChanges.pipe(
143-
startWith(this.stateCtrl.value),
144-
map(val => this.displayFn(val)),
145-
map(name => this.filterStates(name)),
146-
);
139+
this.tdStates = this.states.slice();
140+
this.reactiveStates = this.states.slice();
147141

148142
this.filteredGroupedStates = this.groupedStates = this.states.reduce<StateGroup[]>(
149143
(groups, state) => {

src/material/autocomplete/autocomplete-trigger.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,13 @@ export abstract class _MatAutocompleteTriggerBase
470470
if (this._previousValue !== value) {
471471
this._previousValue = value;
472472
this._pendingAutoselectedOption = null;
473-
this._onChange(value);
473+
474+
// If selection is required we don't write to the CVA while the user is typing.
475+
// At the end of the selection either the user will have picked something
476+
// or we'll reset the value back to null.
477+
if (!this.autocomplete || !this.autocomplete.requireSelection) {
478+
this._onChange(value);
479+
}
474480

475481
if (!value) {
476482
this._clearPreviousSelectedOption(null, false);

src/material/autocomplete/autocomplete.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2622,7 +2622,7 @@ describe('MDC-based MatAutocomplete', () => {
26222622
tick();
26232623

26242624
expect(input.value).toBe('Cali');
2625-
expect(stateCtrl.value).toBe('Cali');
2625+
expect(stateCtrl.value).toEqual({code: 'CA', name: 'California'});
26262626
expect(spy).not.toHaveBeenCalled();
26272627

26282628
dispatchFakeEvent(document, 'click');

0 commit comments

Comments
 (0)