Skip to content

Commit 80671bf

Browse files
dereekbandrewseguin
authored andcommitted
fix(selection-list): fix option value coercion and selection events (#6901)
* fix(selection-list): fix md-list-option value coercion and selection event emitters * md-list-option value is no longer coerced to a boolean value * md-list-option EventEmitters selectedChange and deselected now emit an event when an option is selected/deselected Fixes #6864 * review feedback changes * review feedback changes * review feedback changes * review feedback changes
1 parent 074f6ce commit 80671bf

File tree

2 files changed

+148
-35
lines changed

2 files changed

+148
-35
lines changed

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

+118-14
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {createKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
44
import {Component, DebugElement} from '@angular/core';
55
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
66
import {By} from '@angular/platform-browser';
7-
import {MatListModule, MatListOption, MatSelectionList} from './index';
7+
import {MatListModule, MatListOption, MatSelectionList, MatListOptionChange} from './index';
88

99

1010
describe('MatSelectionList', () => {
@@ -483,9 +483,88 @@ describe('MatSelectionList', () => {
483483
expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
484484
});
485485
});
486-
});
487486

488487

488+
describe('with multiple values', () => {
489+
let fixture: ComponentFixture<SelectionListWithMultipleValues>;
490+
let listOption: DebugElement[];
491+
let listItemEl: DebugElement;
492+
let selectionList: DebugElement;
493+
494+
beforeEach(async(() => {
495+
TestBed.configureTestingModule({
496+
imports: [MatListModule],
497+
declarations: [
498+
SelectionListWithMultipleValues
499+
],
500+
});
501+
502+
TestBed.compileComponents();
503+
}));
504+
505+
beforeEach(async(() => {
506+
fixture = TestBed.createComponent(SelectionListWithMultipleValues);
507+
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
508+
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
509+
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
510+
fixture.detectChanges();
511+
}));
512+
513+
it('should have a value for each item', () => {
514+
expect(listOption[0].componentInstance.value).toBe(1);
515+
expect(listOption[1].componentInstance.value).toBe('a');
516+
expect(listOption[2].componentInstance.value).toBe(true);
517+
});
518+
519+
});
520+
521+
describe('with option selected events', () => {
522+
let fixture: ComponentFixture<SelectionListWithOptionEvents>;
523+
let testComponent: SelectionListWithOptionEvents;
524+
let listOption: DebugElement[];
525+
let selectionList: DebugElement;
526+
527+
beforeEach(async(() => {
528+
TestBed.configureTestingModule({
529+
imports: [MatListModule],
530+
declarations: [
531+
SelectionListWithOptionEvents
532+
],
533+
});
534+
535+
TestBed.compileComponents();
536+
}));
537+
538+
beforeEach(async(() => {
539+
fixture = TestBed.createComponent(SelectionListWithOptionEvents);
540+
testComponent = fixture.debugElement.componentInstance;
541+
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
542+
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
543+
fixture.detectChanges();
544+
}));
545+
546+
it('should trigger the selected and deselected events when clicked in succession.', () => {
547+
548+
let selected: boolean = false;
549+
550+
spyOn(testComponent, 'onOptionSelectionChange')
551+
.and.callFake((event: MatListOptionChange) => {
552+
selected = event.selected;
553+
});
554+
555+
listOption[0].nativeElement.click();
556+
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(1);
557+
expect(selected).toBe(true);
558+
559+
listOption[0].nativeElement.click();
560+
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(2);
561+
expect(selected).toBe(false);
562+
});
563+
564+
});
565+
566+
});
567+
489568
@Component({template: `
490569
<mat-selection-list id="selection-list-1">
491570
<mat-list-option checkboxPosition="before" disabled="true" value="inbox">
@@ -507,35 +586,35 @@ class SelectionListWithListOptions {
507586
}
508587

509588
@Component({template: `
510-
<mat-selection-list id = "selection-list-2">
511-
<mat-list-option checkboxPosition = "after">
589+
<mat-selection-list id="selection-list-2">
590+
<mat-list-option checkboxPosition="after">
512591
Inbox (disabled selection-option)
513592
</mat-list-option>
514-
<mat-list-option id = "testSelect" checkboxPosition = "after">
593+
<mat-list-option id="testSelect" checkboxPosition="after">
515594
Starred
516595
</mat-list-option>
517-
<mat-list-option checkboxPosition = "after">
596+
<mat-list-option checkboxPosition="after">
518597
Sent Mail
519598
</mat-list-option>
520-
<mat-list-option checkboxPosition = "after">
599+
<mat-list-option checkboxPosition="after">
521600
Drafts
522601
</mat-list-option>
523602
</mat-selection-list>`})
524603
class SelectionListWithCheckboxPositionAfter {
525604
}
526605

527606
@Component({template: `
528-
<mat-selection-list id = "selection-list-3" [disabled] = true>
529-
<mat-list-option checkboxPosition = "after">
607+
<mat-selection-list id="selection-list-3" [disabled]=true>
608+
<mat-list-option checkboxPosition="after">
530609
Inbox (disabled selection-option)
531610
</mat-list-option>
532-
<mat-list-option id = "testSelect" checkboxPosition = "after">
611+
<mat-list-option id="testSelect" checkboxPosition="after">
533612
Starred
534613
</mat-list-option>
535-
<mat-list-option checkboxPosition = "after">
614+
<mat-list-option checkboxPosition="after">
536615
Sent Mail
537616
</mat-list-option>
538-
<mat-list-option checkboxPosition = "after">
617+
<mat-list-option checkboxPosition="after">
539618
Drafts
540619
</mat-list-option>
541620
</mat-selection-list>`})
@@ -559,8 +638,8 @@ class SelectionListWithSelectedOption {
559638
}
560639

561640
@Component({template: `
562-
<mat-selection-list id = "selection-list-4">
563-
<mat-list-option checkboxPosition = "after" class="test-focus" id="123">
641+
<mat-selection-list id="selection-list-4">
642+
<mat-list-option checkboxPosition="after" class="test-focus" id="123">
564643
Inbox
565644
</mat-list-option>
566645
</mat-selection-list>`})
@@ -579,3 +658,28 @@ class SelectionListWithTabindexBinding {
579658
tabIndex: number;
580659
disabled: boolean;
581660
}
661+
662+
@Component({template: `
663+
<mat-selection-list id="selection-list-5">
664+
<mat-list-option [value]="1" checkboxPosition="after">
665+
1
666+
</mat-list-option>
667+
<mat-list-option value="a" checkboxPosition="after">
668+
a
669+
</mat-list-option>
670+
<mat-list-option [value]="true" checkboxPosition="after">
671+
true
672+
</mat-list-option>
673+
</mat-selection-list>`})
674+
class SelectionListWithMultipleValues {
675+
}
676+
677+
@Component({template: `
678+
<mat-selection-list id="selection-list-6">
679+
<mat-list-option (selectionChange)="onOptionSelectionChange($event)">
680+
Inbox
681+
</mat-list-option>
682+
</mat-selection-list>`})
683+
class SelectionListWithOptionEvents {
684+
onOptionSelectionChange: (event?: MatListOptionChange) => void = () => {};
685+
}

src/lib/list/selection-list.ts

+30-21
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@ export const _MatSelectionListMixinBase =
5151
export class MatListOptionBase {}
5252
export const _MatListOptionMixinBase = mixinDisableRipple(MatListOptionBase);
5353

54-
/** Event emitted by a selection-list whenever the state of an option is changed. */
55-
export interface MatSelectionListOptionEvent {
56-
option: MatListOption;
54+
/** Change event object emitted by MatListOption */
55+
export class MatListOptionChange {
56+
/** The source MatListOption of the event. */
57+
source: MatListOption;
58+
/** The new `selected` value of the option. */
59+
selected: boolean;
5760
}
5861

5962
/**
6063
* Component for list-options of selection-list. Each list-option can automatically
6164
* generate a checkbox and can put current item into the selectionModel of selection-list
62-
* if the current item is checked.
65+
* if the current item is selected.
6366
*/
6467
@Component({
6568
moduleId: module.id,
@@ -86,7 +89,6 @@ export interface MatSelectionListOptionEvent {
8689
export class MatListOption extends _MatListOptionMixinBase
8790
implements AfterContentInit, OnInit, OnDestroy, FocusableOption, CanDisableRipple {
8891
private _lineSetter: MatLineSetter;
89-
private _selected: boolean = false;
9092
private _disabled: boolean = false;
9193

9294
/** Whether the option has focus. */
@@ -97,34 +99,31 @@ export class MatListOption extends _MatListOptionMixinBase
9799
/** Whether the label should appear before or after the checkbox. Defaults to 'after' */
98100
@Input() checkboxPosition: 'before' | 'after' = 'after';
99101

100-
/** Value of the option */
101-
@Input() value: any;
102-
103102
/** Whether the option is disabled. */
104103
@Input()
105-
get disabled() { return (this.selectionList && this.selectionList.disabled) || this._disabled; }
106-
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
104+
get disabled(): boolean {
105+
return (this.selectionList && this.selectionList.disabled) || this._disabled;
106+
}
107+
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); }
108+
109+
/** Value of the option */
110+
@Input() value: any;
107111

108112
/** Whether the option is selected. */
109113
@Input()
110-
get selected() { return this._selected; }
114+
get selected(): boolean { return this.selectionList.selectedOptions.isSelected(this); }
111115
set selected(value: boolean) {
112116
const isSelected = coerceBooleanProperty(value);
113117

114-
if (isSelected !== this._selected) {
115-
const selectionModel = this.selectionList.selectedOptions;
116-
117-
this._selected = isSelected;
118-
isSelected ? selectionModel.select(this) : selectionModel.deselect(this);
118+
if (isSelected !== this.selected) {
119+
this.selectionList.selectedOptions.toggle(this);
119120
this._changeDetector.markForCheck();
121+
this.selectionChange.emit(this._createChangeEvent());
120122
}
121123
}
122124

123-
/** Emitted when the option is selected. */
124-
@Output() selectChange = new EventEmitter<MatSelectionListOptionEvent>();
125-
126-
/** Emitted when the option is deselected. */
127-
@Output() deselected = new EventEmitter<MatSelectionListOptionEvent>();
125+
/** Emitted when the option is selected or deselected. */
126+
@Output() selectionChange = new EventEmitter<MatListOptionChange>();
128127

129128
constructor(private _renderer: Renderer2,
130129
private _element: ElementRef,
@@ -174,6 +173,16 @@ export class MatListOption extends _MatListOptionMixinBase
174173
this.selectionList._setFocusedOption(this);
175174
}
176175

176+
/** Creates a selection event object from the specified option. */
177+
private _createChangeEvent(option: MatListOption = this): MatListOptionChange {
178+
const event = new MatListOptionChange();
179+
180+
event.source = option;
181+
event.selected = option.selected;
182+
183+
return event;
184+
}
185+
177186
/** Retrieves the DOM element of the component host. */
178187
_getHostElement(): HTMLElement {
179188
return this._element.nativeElement;

0 commit comments

Comments
 (0)