Skip to content

Commit f40a7cc

Browse files
crisbetokara
authored andcommitted
fix(selection-list): model not updated when option is selected programmatically (#7334)
Fixes #7318.
1 parent 0584cdf commit f40a7cc

File tree

3 files changed

+57
-34
lines changed

3 files changed

+57
-34
lines changed

src/demo-app/list/list-demo.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ <h2>Selection list</h2>
108108
<mat-selection-list #groceries>
109109
<h3 mat-subheader>Groceries</h3>
110110

111-
<mat-list-option *ngFor="let item of items" [value]="item">
112-
{{item}}
113-
</mat-list-option>
111+
<mat-list-option value="bananas">Bananas</mat-list-option>
112+
<mat-list-option selected value="oranges">Oranges</mat-list-option>
113+
<mat-list-option value="apples">Apples</mat-list-option>
114+
<mat-list-option value="strawberries">Strawberries</mat-list-option>
114115
</mat-selection-list>
115116

116117
<p>Selected: {{groceries.selectedOptions.selected.length}}</p>

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

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {MatListModule, MatListOption, MatSelectionList} from './index';
1010
describe('MatSelectionList', () => {
1111
describe('with list option', () => {
1212
let fixture: ComponentFixture<SelectionListWithListOptions>;
13-
let listOption: DebugElement[];
13+
let listOptions: DebugElement[];
1414
let listItemEl: DebugElement;
1515
let selectionList: DebugElement;
1616

@@ -33,14 +33,14 @@ describe('MatSelectionList', () => {
3333
fixture = TestBed.createComponent(SelectionListWithListOptions);
3434
fixture.detectChanges();
3535

36-
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
36+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption));
3737
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
3838
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
3939
}));
4040

4141
it('should add and remove focus class on focus/blur', () => {
4242
// Use the second list item, because the first one is always disabled.
43-
const listItem = listOption[1].nativeElement;
43+
const listItem = listOptions[1].nativeElement;
4444

4545
expect(listItem.classList).not.toContain('mat-list-item-focus');
4646

@@ -57,35 +57,35 @@ describe('MatSelectionList', () => {
5757
const optionValues = ['inbox', 'starred', 'sent-mail', 'drafts'];
5858

5959
optionValues.forEach((optionValue, index) => {
60-
expect(listOption[index].componentInstance.value).toBe(optionValue);
60+
expect(listOptions[index].componentInstance.value).toBe(optionValue);
6161
});
6262
});
6363

6464
it('should be able to dispatch one selected item', () => {
65-
let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
65+
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
6666
let selectList =
6767
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;
6868

6969
expect(selectList.selected.length).toBe(0);
70-
expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('false');
70+
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');
7171

7272
testListItem.toggle();
7373
fixture.detectChanges();
7474

75-
expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('true');
76-
expect(listOption[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
75+
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('true');
76+
expect(listOptions[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
7777
expect(selectList.selected.length).toBe(1);
7878
});
7979

8080
it('should be able to dispatch multiple selected items', () => {
81-
let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
82-
let testListItem2 = listOption[1].injector.get<MatListOption>(MatListOption);
81+
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
82+
let testListItem2 = listOptions[1].injector.get<MatListOption>(MatListOption);
8383
let selectList =
8484
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;
8585

8686
expect(selectList.selected.length).toBe(0);
87-
expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('false');
88-
expect(listOption[1].nativeElement.getAttribute('aria-selected')).toBe('false');
87+
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');
88+
expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('false');
8989

9090
testListItem.toggle();
9191
fixture.detectChanges();
@@ -94,14 +94,14 @@ describe('MatSelectionList', () => {
9494
fixture.detectChanges();
9595

9696
expect(selectList.selected.length).toBe(2);
97-
expect(listOption[2].nativeElement.getAttribute('aria-selected')).toBe('true');
98-
expect(listOption[1].nativeElement.getAttribute('aria-selected')).toBe('true');
99-
expect(listOption[1].nativeElement.getAttribute('aria-disabled')).toBe('false');
100-
expect(listOption[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
97+
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('true');
98+
expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('true');
99+
expect(listOptions[1].nativeElement.getAttribute('aria-disabled')).toBe('false');
100+
expect(listOptions[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
101101
});
102102

103103
it('should be able to deselect an option', () => {
104-
let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
104+
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
105105
let selectList =
106106
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;
107107

@@ -119,12 +119,12 @@ describe('MatSelectionList', () => {
119119
});
120120

121121
it('should not allow selection of disabled items', () => {
122-
let testListItem = listOption[0].injector.get<MatListOption>(MatListOption);
122+
let testListItem = listOptions[0].injector.get<MatListOption>(MatListOption);
123123
let selectList =
124124
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;
125125

126126
expect(selectList.selected.length).toBe(0);
127-
expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
127+
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
128128

129129
testListItem._handleClick();
130130
fixture.detectChanges();
@@ -133,18 +133,18 @@ describe('MatSelectionList', () => {
133133
});
134134

135135
it('should be able to un-disable disabled items', () => {
136-
let testListItem = listOption[0].injector.get<MatListOption>(MatListOption);
136+
let testListItem = listOptions[0].injector.get<MatListOption>(MatListOption);
137137

138-
expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
138+
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
139139

140140
testListItem.disabled = false;
141141
fixture.detectChanges();
142142

143-
expect(listOption[0].nativeElement.getAttribute('aria-disabled')).toBe('false');
143+
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('false');
144144
});
145145

146146
it('should be able to use keyboard select with SPACE', () => {
147-
let testListItem = listOption[1].nativeElement as HTMLElement;
147+
let testListItem = listOptions[1].nativeElement as HTMLElement;
148148
let SPACE_EVENT: KeyboardEvent =
149149
createKeyboardEvent('keydown', SPACE, testListItem);
150150
let selectList =
@@ -162,7 +162,7 @@ describe('MatSelectionList', () => {
162162
it('should restore focus if active option is destroyed', () => {
163163
const manager = selectionList.componentInstance._keyManager;
164164

165-
listOption[3].componentInstance._handleFocus();
165+
listOptions[3].componentInstance._handleFocus();
166166

167167
expect(manager.activeItemIndex).toBe(3);
168168

@@ -173,12 +173,12 @@ describe('MatSelectionList', () => {
173173
});
174174

175175
it('should focus previous item when press UP ARROW', () => {
176-
let testListItem = listOption[2].nativeElement as HTMLElement;
176+
let testListItem = listOptions[2].nativeElement as HTMLElement;
177177
let UP_EVENT: KeyboardEvent =
178178
createKeyboardEvent('keydown', UP_ARROW, testListItem);
179179
let manager = selectionList.componentInstance._keyManager;
180180

181-
dispatchFakeEvent(listOption[2].nativeElement, 'focus');
181+
dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
182182
expect(manager.activeItemIndex).toEqual(2);
183183

184184
selectionList.componentInstance._keydown(UP_EVENT);
@@ -189,12 +189,12 @@ describe('MatSelectionList', () => {
189189
});
190190

191191
it('should focus next item when press DOWN ARROW', () => {
192-
let testListItem = listOption[2].nativeElement as HTMLElement;
192+
let testListItem = listOptions[2].nativeElement as HTMLElement;
193193
let DOWN_EVENT: KeyboardEvent =
194194
createKeyboardEvent('keydown', DOWN_ARROW, testListItem);
195195
let manager = selectionList.componentInstance._keyManager;
196196

197-
dispatchFakeEvent(listOption[2].nativeElement, 'focus');
197+
dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
198198
expect(manager.activeItemIndex).toEqual(2);
199199

200200
selectionList.componentInstance._keydown(DOWN_EVENT);
@@ -226,6 +226,20 @@ describe('MatSelectionList', () => {
226226

227227
expect(list.options.toArray().every(option => option.selected)).toBe(false);
228228
});
229+
230+
it('should update the list value when an item is selected programmatically', () => {
231+
const list: MatSelectionList = selectionList.componentInstance;
232+
233+
expect(list.selectedOptions.isEmpty()).toBe(true);
234+
235+
listOptions[0].componentInstance.selected = true;
236+
listOptions[2].componentInstance.selected = true;
237+
fixture.detectChanges();
238+
239+
expect(list.selectedOptions.isEmpty()).toBe(false);
240+
expect(list.selectedOptions.isSelected(listOptions[0].componentInstance)).toBe(true);
241+
expect(list.selectedOptions.isSelected(listOptions[2].componentInstance)).toBe(true);
242+
});
229243
});
230244

231245
describe('with list option selected', () => {

src/lib/list/selection-list.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,17 @@ export class MatListOption extends _MatListOptionMixinBase
103103
/** Whether the option is selected. */
104104
@Input()
105105
get selected() { return this._selected; }
106-
set selected(value: boolean) { this._selected = coerceBooleanProperty(value); }
106+
set selected(value: boolean) {
107+
const isSelected = coerceBooleanProperty(value);
108+
109+
if (isSelected !== this._selected) {
110+
const selectionModel = this.selectionList.selectedOptions;
111+
112+
this._selected = isSelected;
113+
isSelected ? selectionModel.select(this) : selectionModel.deselect(this);
114+
this._changeDetector.markForCheck();
115+
}
116+
}
107117

108118
/** Emitted when the option is selected. */
109119
@Output() selectChange = new EventEmitter<MatSelectionListOptionEvent>();
@@ -140,8 +150,6 @@ export class MatListOption extends _MatListOptionMixinBase
140150
/** Toggles the selection state of the option. */
141151
toggle(): void {
142152
this.selected = !this.selected;
143-
this.selectionList.selectedOptions.toggle(this);
144-
this._changeDetector.markForCheck();
145153
}
146154

147155
/** Allows for programmatic focusing of the option. */

0 commit comments

Comments
 (0)