From c474eed635d52201bd78167de1675028970ce4d2 Mon Sep 17 00:00:00 2001 From: Karan Mistry Date: Fri, 27 Jun 2025 15:25:12 +0530 Subject: [PATCH] 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 --- goldens/material/chips/index.api.md | 1 + src/material/chips/chip-grid.spec.ts | 79 ++++++++++++++++++++++++++++ src/material/chips/chip-grid.ts | 32 +++++++++-- src/material/chips/chip-input.ts | 5 ++ 4 files changed, 112 insertions(+), 5 deletions(-) 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..5ff94dc7c826 100644 --- a/src/material/chips/chip-grid.spec.ts +++ b/src/material/chips/chip-grid.spec.ts @@ -1026,6 +1026,44 @@ describe('MatChipGrid', () => { })); }); + describe('chip with form control', () => { + let fixture: ComponentFixture; + let component: ChipsFormControlUpdate; + let nativeInput: HTMLInputElement; + let nativeButton: HTMLButtonElement; + + beforeEach(() => { + fixture = createComponent(ChipsFormControlUpdate); + component = fixture.componentInstance; + nativeInput = fixture.nativeElement.querySelector('input'); + nativeButton = fixture.nativeElement.querySelector('button[id="save"]'); + }); + + it('should update the form control value when pressed enter', fakeAsync(() => { + nativeInput.value = 'hello'; + nativeInput.focus(); + fixture.detectChanges(); + + dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER); + fixture.detectChanges(); + flush(); + + expect(component.keywordChipControl.value).not.toBeNull(); + expect(component.keywordChipControl.value.length).toBe(1); + expect(nativeButton.disabled).toBeFalsy(); + + nativeInput.value = 'how are you ?'; + nativeInput.focus(); + fixture.detectChanges(); + + dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER); + fixture.detectChanges(); + flush(); + + expect(component.keywordChipControl.value.length).toBe(2); + })); + }); + function createComponent( component: Type, direction: Direction = 'ltr', @@ -1234,3 +1272,44 @@ class ChipGridWithRemove { this.chips.splice(event.chip.value, 1); } } + +@Component({ + template: ` + + Keywords + + @for (keyword of keywords; track keyword) { + {{keyword}} + } + + + + + `, + imports: [ + MatChipGrid, + MatChipRow, + MatChipInput, + MatFormField, + MatChipRemove, + ReactiveFormsModule, + ], +}) +class ChipsFormControlUpdate { + keywords = new Array(); + keywordChipControl = new FormControl(); + + constructor() { + this.keywordChipControl.setValidators(Validators.required); + } + + add(event: MatChipInputEvent): void { + const value = (event.value || '').trim(); + + if (value) { + this.keywords.push(value); + } + + event.chipInput.clear(); + } +} diff --git a/src/material/chips/chip-grid.ts b/src/material/chips/chip-grid.ts index 5dfe91e9233e..28243c3d966b 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 @@ -493,11 +508,18 @@ export class MatChipGrid /** Emits change event to set the model value. */ private _propagateChanges(): void { const valueToEmit = this._chips.length ? this._chips.toArray().map(chip => chip.value) : []; - this._value = valueToEmit; - this.change.emit(new MatChipGridChange(this, valueToEmit)); - this.valueChange.emit(valueToEmit); - this._onChange(valueToEmit); - this._changeDetectorRef.markForCheck(); + + if ( + !this._value || + this._value.length !== valueToEmit.length || + !valueToEmit.every(item => this._value.includes(item)) + ) { + this._value = valueToEmit; + this.change.emit(new MatChipGridChange(this, valueToEmit)); + this.valueChange.emit(valueToEmit); + this._onChange(valueToEmit); + this._changeDetectorRef.markForCheck(); + } } /** Mark the field as touched */ diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts index db6134d4d9fc..15a6ce364c0d 100644 --- a/src/material/chips/chip-input.ts +++ b/src/material/chips/chip-input.ts @@ -193,6 +193,11 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy { /** Checks to see if the (chipEnd) event needs to be emitted. */ _emitChipEnd(event?: KeyboardEvent) { if (!event || (this._isSeparatorKey(event) && !event.repeat)) { + const trimmedValue = this.inputElement.value?.trim(); + if (!this.empty && trimmedValue) { + this._chipGrid._change(); + this._chipGrid.stateChanges.next(); + } this.chipEnd.emit({ input: this.inputElement, value: this.inputElement.value,