From 1dc18e7d842c3638f21057bf442337215846d907 Mon Sep 17 00:00:00 2001 From: Karan Mistry Date: Wed, 23 Apr 2025 11:17:06 +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 | 72 ++++++++++++++++++++++++++++ src/material/chips/chip-grid.ts | 32 +++++++++++-- src/material/chips/chip-input.ts | 5 ++ 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/goldens/material/chips/index.api.md b/goldens/material/chips/index.api.md index 9ff35385222d..e75473be83d3 100644 --- a/goldens/material/chips/index.api.md +++ b/goldens/material/chips/index.api.md @@ -165,6 +165,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 476d19ef4c50..8c1707e8f279 100644 --- a/src/material/chips/chip-grid.spec.ts +++ b/src/material/chips/chip-grid.spec.ts @@ -1019,6 +1019,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', @@ -1228,3 +1266,37 @@ class ChipGridWithRemove { this.chips.splice(event.chip.value, 1); } } + +@Component({ + template: ` + + Keywords + + @for (keyword of keywords; track keyword) { + {{keyword}} + } + + + + + `, + standalone: false, +}) +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,