Skip to content

Commit 4002140

Browse files
crisbetojelbourn
authored andcommitted
fix(list): matching item not preselected if added after init (#16080)
Fixes a `mat-list-option` that is added after initialization not being selected, even if it matches the control value. Fixes #16062.
1 parent 19af6ec commit 4002140

File tree

3 files changed

+48
-26
lines changed

3 files changed

+48
-26
lines changed

src/material/list/selection-list.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,20 @@ describe('MatSelectionList with forms', () => {
10981098
expect(fixture.componentInstance.formControl.value).toEqual(['opt2']);
10991099
}));
11001100

1101+
it('should mark options added at a later point as selected', () => {
1102+
fixture.componentInstance.formControl.setValue(['opt4']);
1103+
fixture.detectChanges();
1104+
1105+
fixture.componentInstance.renderExtraOption = true;
1106+
fixture.detectChanges();
1107+
1108+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
1109+
.map(optionDebugEl => optionDebugEl.componentInstance);
1110+
1111+
expect(listOptions.length).toBe(4);
1112+
expect(listOptions[3].selected).toBe(true);
1113+
});
1114+
11011115
});
11021116

11031117
describe('preselected values', () => {
@@ -1285,12 +1299,14 @@ class SelectionListWithModel {
12851299
<mat-list-option value="opt1">Option 1</mat-list-option>
12861300
<mat-list-option value="opt2">Option 2</mat-list-option>
12871301
<mat-list-option value="opt3">Option 3</mat-list-option>
1302+
<mat-list-option value="opt4" *ngIf="renderExtraOption">Option 4</mat-list-option>
12881303
</mat-selection-list>
12891304
`
12901305
})
12911306
class SelectionListWithFormControl {
12921307
formControl = new FormControl();
12931308
renderList = true;
1309+
renderExtraOption = false;
12941310
}
12951311

12961312

src/material/list/selection-list.ts

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ import {
4949
ThemePalette,
5050
} from '@angular/material/core';
5151
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
52-
import {Subscription} from 'rxjs';
52+
import {Subject} from 'rxjs';
53+
import {takeUntil} from 'rxjs/operators';
5354
import {MatListAvatarCssMatStyler, MatListIconCssMatStyler} from './list';
5455

5556

@@ -103,8 +104,8 @@ export class MatSelectionListChange {
103104
// its theme. The accent theme palette is the default and doesn't need to be set.
104105
'[class.mat-primary]': 'color === "primary"',
105106
'[class.mat-warn]': 'color === "warn"',
106-
'[attr.aria-selected]': 'selected.toString()',
107-
'[attr.aria-disabled]': 'disabled.toString()',
107+
'[attr.aria-selected]': 'selected',
108+
'[attr.aria-disabled]': 'disabled',
108109
},
109110
templateUrl: 'list-option.html',
110111
encapsulation: ViewEncapsulation.None,
@@ -177,13 +178,19 @@ export class MatListOption extends _MatListOptionMixinBase
177178
}
178179

179180
ngOnInit() {
181+
const list = this.selectionList;
182+
183+
if (list._value && list._value.some(value => list.compareWith(value, this._value))) {
184+
this._setSelected(true);
185+
}
186+
187+
const wasSelected = this._selected;
188+
180189
// List options that are selected at initialization can't be reported properly to the form
181190
// control. This is because it takes some time until the selection-list knows about all
182191
// available options. Also it can happen that the ControlValueAccessor has an initial value
183192
// that should be used instead. Deferring the value change report to the next tick ensures
184193
// that the form control value is not being overwritten.
185-
const wasSelected = this._selected;
186-
187194
Promise.resolve().then(() => {
188195
if (this._selected || wasSelected) {
189196
this.selected = true;
@@ -337,7 +344,7 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
337344
* options should appear as selected. The first argument is the value of an options. The second
338345
* one is a value from the selected value. A boolean must be returned.
339346
*/
340-
@Input() compareWith: (o1: any, o2: any) => boolean;
347+
@Input() compareWith: (o1: any, o2: any) => boolean = (a1, a2) => a1 === a2;
341348

342349
/** Whether the selection list is disabled. */
343350
@Input()
@@ -359,17 +366,17 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
359366
/** View to model callback that should be called whenever the selected options change. */
360367
private _onChange: (value: any) => void = (_: any) => {};
361368

362-
/** Used for storing the values that were assigned before the options were initialized. */
363-
private _tempValues: string[]|null;
369+
/** Keeps track of the currently-selected value. */
370+
_value: string[]|null;
364371

365-
/** Subscription to sync value changes in the SelectionModel back to the SelectionList. */
366-
private _modelChanges = Subscription.EMPTY;
372+
/** Emits when the list has been destroyed. */
373+
private _destroyed = new Subject<void>();
367374

368375
/** View to model callback that should be called if the list or its options lost focus. */
369376
_onTouched: () => void = () => {};
370377

371378
/** Whether the list has been destroyed. */
372-
private _destroyed: boolean;
379+
private _isDestroyed: boolean;
373380

374381
constructor(private _element: ElementRef<HTMLElement>, @Attribute('tabindex') tabIndex: string) {
375382
super();
@@ -385,13 +392,12 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
385392
.skipPredicate(() => false)
386393
.withAllowedModifierKeys(['shiftKey']);
387394

388-
if (this._tempValues) {
389-
this._setOptionsFromValues(this._tempValues);
390-
this._tempValues = null;
395+
if (this._value) {
396+
this._setOptionsFromValues(this._value);
391397
}
392398

393399
// Sync external changes to the model back to the options.
394-
this._modelChanges = this.selectedOptions.onChange.subscribe(event => {
400+
this.selectedOptions.onChange.pipe(takeUntil(this._destroyed)).subscribe(event => {
395401
if (event.added) {
396402
for (let item of event.added) {
397403
item.selected = true;
@@ -417,8 +423,9 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
417423
}
418424

419425
ngOnDestroy() {
420-
this._destroyed = true;
421-
this._modelChanges.unsubscribe();
426+
this._destroyed.next();
427+
this._destroyed.complete();
428+
this._isDestroyed = true;
422429
}
423430

424431
/** Focuses the selection list. */
@@ -504,8 +511,10 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
504511
// Stop reporting value changes after the list has been destroyed. This avoids
505512
// cases where the list might wrongly reset its value once it is removed, but
506513
// the form control is still live.
507-
if (this.options && !this._destroyed) {
508-
this._onChange(this._getSelectedOptionValues());
514+
if (this.options && !this._isDestroyed) {
515+
const value = this._getSelectedOptionValues();
516+
this._onChange(value);
517+
this._value = value;
509518
}
510519
}
511520

@@ -516,10 +525,10 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
516525

517526
/** Implemented as part of ControlValueAccessor. */
518527
writeValue(values: string[]): void {
528+
this._value = values;
529+
519530
if (this.options) {
520531
this._setOptionsFromValues(values || []);
521-
} else {
522-
this._tempValues = values;
523532
}
524533
}
525534

@@ -546,11 +555,7 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
546555
const correspondingOption = this.options.find(option => {
547556
// Skip options that are already in the model. This allows us to handle cases
548557
// where the same primitive value is selected multiple times.
549-
if (option.selected) {
550-
return false;
551-
}
552-
553-
return this.compareWith ? this.compareWith(option.value, value) : option.value === value;
558+
return option.selected ? false : this.compareWith(option.value, value);
554559
});
555560

556561
if (correspondingOption) {

tools/public_api_guard/material/list.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export declare class MatNavList extends _MatListMixinBase implements CanDisableR
6868
export declare class MatSelectionList extends _MatSelectionListMixinBase implements FocusableOption, CanDisableRipple, AfterContentInit, ControlValueAccessor, OnDestroy, OnChanges {
6969
_keyManager: FocusKeyManager<MatListOption>;
7070
_onTouched: () => void;
71+
_value: string[] | null;
7172
color: ThemePalette;
7273
compareWith: (o1: any, o2: any) => boolean;
7374
disabled: boolean;

0 commit comments

Comments
 (0)