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,