From 2fd34212d9517effaa81b500d64de35bc3aa99fb Mon Sep 17 00:00:00 2001 From: Karan Mistry Date: Fri, 27 Jun 2025 15:33:43 +0530 Subject: [PATCH] fix(material/chips): update form control immediately when chips is removed Currently, when we removed chips, the value is updated in form control only when we blur. This fix will update the value of form control immediately when removed Fixes #30566 --- goldens/material/chips/index.api.md | 1 + src/material/chips/chip-grid.spec.ts | 70 ++++++++++++++++++++++++++++ src/material/chips/chip-grid.ts | 15 ++++++ 3 files changed, 86 insertions(+) diff --git a/goldens/material/chips/index.api.md b/goldens/material/chips/index.api.md index fdcc60891262..c24de34c1dc0 100644 --- a/goldens/material/chips/index.api.md +++ b/goldens/material/chips/index.api.md @@ -187,6 +187,7 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi protected _allowFocusEscape(): void; _blur(): void; readonly change: EventEmitter; + _change(): void; get chipBlurChanges(): Observable; protected _chipInput: MatChipTextControl; // (undocumented) diff --git a/src/material/chips/chip-grid.spec.ts b/src/material/chips/chip-grid.spec.ts index 385dca10c968..52a49810c181 100644 --- a/src/material/chips/chip-grid.spec.ts +++ b/src/material/chips/chip-grid.spec.ts @@ -1026,6 +1026,29 @@ describe('MatChipGrid', () => { })); }); + it('should update the form control immediately when remove button is clicked', fakeAsync(() => { + const fixture = createComponent(ChipGridWithRemoveAndFormControl, undefined, [ + NoopAnimationsModule, + ]); + + const component = fixture.componentRef.instance; + + flush(); + const trailingActions = chipGridNativeElement.querySelectorAll( + '.mdc-evolution-chip__action--secondary', + ); + const chip = chips.get(2)!; + chip.focus(); + fixture.detectChanges(); + + trailingActions[2].click(); + fixture.detectChanges(); + flush(); + + expect(component.formControl.value?.length).toBe(3); + expect(component.formControl.value?.indexOf('tutorial')).toBe(-1); + })); + function createComponent( component: Type, direction: Direction = 'ltr', @@ -1234,3 +1257,50 @@ class ChipGridWithRemove { this.chips.splice(event.chip.value, 1); } } + +@Component({ + template: ` + + + @for (keyword of keywords(); track keyword) { + + {{keyword}} + Remove + + } + + + + `, + imports: [ + MatChipGrid, + MatChipRow, + MatChipInput, + MatFormField, + MatChipRemove, + ReactiveFormsModule, + ], +}) +class ChipGridWithRemoveAndFormControl { + readonly keywords = signal(['angular', 'how-to', 'tutorial', 'accessibility']); + readonly formControl = new FormControl([...this.keywords()]); + + constructor() { + this.formControl.setValidators(Validators.required); + } + + removeKeyword(keyword: string) { + this.keywords.update(keywords => { + const index = keywords.indexOf(keyword); + if (index < 0) { + return keywords; + } + + keywords.splice(index, 1); + return [...keywords]; + }); + } +} diff --git a/src/material/chips/chip-grid.ts b/src/material/chips/chip-grid.ts index 5dfe91e9233e..dfeca04874fc 100644 --- a/src/material/chips/chip-grid.ts +++ b/src/material/chips/chip-grid.ts @@ -280,6 +280,11 @@ export class MatChipGrid this.stateChanges.next(); }); + this.chipRemovedChanges.pipe(takeUntil(this._destroyed)).subscribe(() => { + this._change(); + this.stateChanges.next(); + }); + merge(this.chipFocusChanges, this._chips.changes) .pipe(takeUntil(this._destroyed)) .subscribe(() => this.stateChanges.next()); @@ -423,6 +428,16 @@ export class MatChipGrid } } + /** When called, propagates the changes and update the immediately */ + _change() { + if (!this.disabled) { + // Timeout is needed to wait for the focus() event trigger on chip input. + setTimeout(() => { + this._propagateChanges(); + }); + } + } + /** * Removes the `tabindex` from the chip grid and resets it back afterwards, allowing the * user to tab out of it. This prevents the grid from capturing focus and redirecting