Skip to content

Commit ef7e114

Browse files
committed
fix(material/chips): chips form control updating value immediately
Currently, when we have chips with form control, the value is updated only when its focused out. This fix will update the value of form control immediately Fixes #28065
1 parent 9d12aaa commit ef7e114

File tree

4 files changed

+105
-5
lines changed

4 files changed

+105
-5
lines changed

goldens/material/chips/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi
187187
protected _allowFocusEscape(): void;
188188
_blur(): void;
189189
readonly change: EventEmitter<MatChipGridChange>;
190+
_change(): void;
190191
get chipBlurChanges(): Observable<MatChipEvent>;
191192
protected _chipInput: MatChipTextControl;
192193
// (undocumented)

src/material/chips/chip-grid.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,44 @@ describe('MatChipGrid', () => {
10261026
}));
10271027
});
10281028

1029+
describe('chip with form control', () => {
1030+
let fixture: ComponentFixture<ChipsFormControlUpdate>;
1031+
let component: ChipsFormControlUpdate;
1032+
let nativeInput: HTMLInputElement;
1033+
let nativeButton: HTMLButtonElement;
1034+
1035+
beforeEach(() => {
1036+
fixture = createComponent(ChipsFormControlUpdate);
1037+
component = fixture.componentInstance;
1038+
nativeInput = fixture.nativeElement.querySelector('input');
1039+
nativeButton = fixture.nativeElement.querySelector('button[id="save"]');
1040+
});
1041+
1042+
it('should update the form control value when pressed enter', fakeAsync(() => {
1043+
nativeInput.value = 'hello';
1044+
nativeInput.focus();
1045+
fixture.detectChanges();
1046+
1047+
dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER);
1048+
fixture.detectChanges();
1049+
flush();
1050+
1051+
expect(component.keywordChipControl.value).not.toBeNull();
1052+
expect(component.keywordChipControl.value.length).toBe(1);
1053+
expect(nativeButton.disabled).toBeFalsy();
1054+
1055+
nativeInput.value = 'how are you ?';
1056+
nativeInput.focus();
1057+
fixture.detectChanges();
1058+
1059+
dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER);
1060+
fixture.detectChanges();
1061+
flush();
1062+
1063+
expect(component.keywordChipControl.value.length).toBe(2);
1064+
}));
1065+
});
1066+
10291067
function createComponent<T>(
10301068
component: Type<T>,
10311069
direction: Direction = 'ltr',
@@ -1234,3 +1272,37 @@ class ChipGridWithRemove {
12341272
this.chips.splice(event.chip.value, 1);
12351273
}
12361274
}
1275+
1276+
@Component({
1277+
template: `
1278+
<mat-form-field>
1279+
<mat-label>Keywords</mat-label>
1280+
<mat-chip-grid #chipGrid [formControl]="keywordChipControl">
1281+
@for (keyword of keywords; track keyword) {
1282+
<mat-chip-row>{{keyword}}</mat-chip-row>
1283+
}
1284+
</mat-chip-grid>
1285+
<input placeholder="New keyword..." [matChipInputFor]="chipGrid" (matChipInputTokenEnd)="add($event)">
1286+
</mat-form-field>
1287+
<button id="save" [disabled]="!keywordChipControl.valid">Save</button>
1288+
<button >Cancel</button>`,
1289+
standalone: false,
1290+
})
1291+
class ChipsFormControlUpdate {
1292+
keywords = new Array<string>();
1293+
keywordChipControl = new FormControl();
1294+
1295+
constructor() {
1296+
this.keywordChipControl.setValidators(Validators.required);
1297+
}
1298+
1299+
add(event: MatChipInputEvent): void {
1300+
const value = (event.value || '').trim();
1301+
1302+
if (value) {
1303+
this.keywords.push(value);
1304+
}
1305+
1306+
event.chipInput.clear();
1307+
}
1308+
}

src/material/chips/chip-grid.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,11 @@ export class MatChipGrid
280280
this.stateChanges.next();
281281
});
282282

283+
this.chipRemovedChanges.pipe(takeUntil(this._destroyed)).subscribe(() => {
284+
this._change();
285+
this.stateChanges.next();
286+
});
287+
283288
merge(this.chipFocusChanges, this._chips.changes)
284289
.pipe(takeUntil(this._destroyed))
285290
.subscribe(() => this.stateChanges.next());
@@ -423,6 +428,16 @@ export class MatChipGrid
423428
}
424429
}
425430

431+
/** When called, propagates the changes and update the immediately */
432+
_change() {
433+
if (!this.disabled) {
434+
// Timeout is needed to wait for the focus() event trigger on chip input.
435+
setTimeout(() => {
436+
this._propagateChanges();
437+
});
438+
}
439+
}
440+
426441
/**
427442
* Removes the `tabindex` from the chip grid and resets it back afterwards, allowing the
428443
* user to tab out of it. This prevents the grid from capturing focus and redirecting
@@ -493,11 +508,18 @@ export class MatChipGrid
493508
/** Emits change event to set the model value. */
494509
private _propagateChanges(): void {
495510
const valueToEmit = this._chips.length ? this._chips.toArray().map(chip => chip.value) : [];
496-
this._value = valueToEmit;
497-
this.change.emit(new MatChipGridChange(this, valueToEmit));
498-
this.valueChange.emit(valueToEmit);
499-
this._onChange(valueToEmit);
500-
this._changeDetectorRef.markForCheck();
511+
512+
if (
513+
!this._value ||
514+
this._value.length !== valueToEmit.length ||
515+
!valueToEmit.every(item => this._value.includes(item))
516+
) {
517+
this._value = valueToEmit;
518+
this.change.emit(new MatChipGridChange(this, valueToEmit));
519+
this.valueChange.emit(valueToEmit);
520+
this._onChange(valueToEmit);
521+
this._changeDetectorRef.markForCheck();
522+
}
501523
}
502524

503525
/** Mark the field as touched */

src/material/chips/chip-input.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
193193
/** Checks to see if the (chipEnd) event needs to be emitted. */
194194
_emitChipEnd(event?: KeyboardEvent) {
195195
if (!event || (this._isSeparatorKey(event) && !event.repeat)) {
196+
const trimmedValue = this.inputElement.value?.trim();
197+
if (!this.empty && trimmedValue) {
198+
this._chipGrid._change();
199+
this._chipGrid.stateChanges.next();
200+
}
196201
this.chipEnd.emit({
197202
input: this.inputElement,
198203
value: this.inputElement.value,

0 commit comments

Comments
 (0)