From bd5b54c0307f5aba9c5f150b0df024e1673ea9f1 Mon Sep 17 00:00:00 2001 From: yurakhomitsky Date: Sun, 2 Jul 2023 14:01:25 +0300 Subject: [PATCH] fix(material/chips): Async chips with a delay are not highlighted Fixes a bug in the Angular Material `chips` component where async chips with a delay were not highlighted correctly. Fixes #27370 --- src/material/chips/chip-listbox.spec.ts | 77 +++++++++++++++++++++++++ src/material/chips/chip-listbox.ts | 27 ++++----- 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/material/chips/chip-listbox.spec.ts b/src/material/chips/chip-listbox.spec.ts index ac964fa2cbda..929857d9f2c1 100644 --- a/src/material/chips/chip-listbox.spec.ts +++ b/src/material/chips/chip-listbox.spec.ts @@ -20,6 +20,8 @@ import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/t import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {MatChipListbox, MatChipOption, MatChipsModule} from './index'; +import {asyncScheduler, BehaviorSubject, Observable} from 'rxjs'; +import {observeOn} from 'rxjs/operators'; describe('MDC-based MatChipListbox', () => { let fixture: ComponentFixture; @@ -828,6 +830,60 @@ describe('MDC-based MatChipListbox', () => { .toBeFalsy(); }); }); + + describe('async multiple selection', () => { + it('should select initial async chips', fakeAsync(() => { + fixture = createComponent(AsyncMultiSelectionChipListbox, undefined, initFixture => { + initFixture.componentInstance.control = new FormControl(['tutorial-1', 'tutorial-2']); + }); + fixture.detectChanges(); + flush(); + + tick(400); + fixture.detectChanges(); + + let array = fixture.componentInstance.chips.toArray(); + + expect(array.length).withContext('Expect chips not to be rendered yet').toBe(0); + + tick(100); + fixture.detectChanges(); + + array = fixture.componentInstance.chips.toArray(); + flush(); + + expect(array[0].selected) + .withContext('Expect "tutorial-1" chip to be selected') + .toBe(true); + expect(array[1].selected) + .withContext('Expect "tutorial-2" chip to be selected') + .toBe(true); + })); + + it('should select async chips that changed over time', fakeAsync(() => { + fixture = createComponent(AsyncMultiSelectionChipListbox, undefined, initFixture => { + initFixture.componentInstance.control = new FormControl(['tutorial-1']); + }); + fixture.detectChanges(); + flush(); + + tick(500); + fixture.detectChanges(); + + fixture.componentInstance.control.setValue(['tutorial-4']); + fixture.componentInstance.updateChips(['tutorial-3', 'tutorial-4']); + + tick(500); + fixture.detectChanges(); + + const array = fixture.componentInstance.chips.toArray(); + flush(); + + expect(array[1].selected) + .withContext('Expect "tutorial-4" chip to be selected') + .toBe(true); + })); + }); }); }); @@ -947,6 +1003,27 @@ class MultiSelectionChipListbox { @ViewChildren(MatChipOption) chips: QueryList; } +@Component({ + template: ` + + + {{ chip }} + + + `, +}) +class AsyncMultiSelectionChipListbox { + private _chipsSubject = new BehaviorSubject(['tutorial-1', 'tutorial-2', 'tutorial-3']); + chips$: Observable = this._chipsSubject.pipe(observeOn(asyncScheduler, 500)); + control = new FormControl(null); + @ViewChild(MatChipListbox) chipListbox: MatChipListbox; + @ViewChildren(MatChipOption) chips: QueryList; + + updateChips(chips: string[]): void { + this._chipsSubject.next(chips); + } +} + @Component({ template: ` diff --git a/src/material/chips/chip-listbox.ts b/src/material/chips/chip-listbox.ts index 542543e17e65..e9d0e9592d27 100644 --- a/src/material/chips/chip-listbox.ts +++ b/src/material/chips/chip-listbox.ts @@ -105,9 +105,6 @@ export class MatChipListbox // TODO: MDC uses `grid` here protected override _defaultRole = 'listbox'; - /** Value that was assigned before the listbox was initialized. */ - private _pendingInitialValue: any; - /** Default chip options. */ private _defaultOptions = inject(MAT_CHIPS_DEFAULT_OPTIONS, {optional: true}); @@ -192,7 +189,9 @@ export class MatChipListbox return this._value; } set value(value: any) { - this.writeValue(value); + if (this._chips && this._chips.length) { + this._setSelectionByValue(value, false); + } this._value = value; } protected _value: any; @@ -210,14 +209,12 @@ export class MatChipListbox override _chips: QueryList = undefined!; ngAfterContentInit() { - if (this._pendingInitialValue !== undefined) { - Promise.resolve().then(() => { - this._setSelectionByValue(this._pendingInitialValue, false); - this._pendingInitialValue = undefined; - }); - } - this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + if (this.value !== undefined) { + Promise.resolve().then(() => { + this._setSelectionByValue(this.value, false); + }); + } // Update listbox selectable/multiple properties on chips this._syncListboxProperties(); }); @@ -263,10 +260,10 @@ export class MatChipListbox * @docs-private */ writeValue(value: any): void { - if (this._chips) { - this._setSelectionByValue(value, false); - } else if (value != null) { - this._pendingInitialValue = value; + if (value) { + this.value = value; + } else { + this.value = undefined; } }