Skip to content

Commit 298da1c

Browse files
authored
feat(material/list): support two-data binding on list option selected (#23125)
Adds support for two-way data binding on the `selected` property of `mat-list-option`. Fixes #23122.
1 parent fec20ad commit 298da1c

File tree

5 files changed

+117
-1
lines changed

5 files changed

+117
-1
lines changed

src/material-experimental/mdc-list/list-option.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import {
1515
Component,
1616
ContentChildren,
1717
ElementRef,
18+
EventEmitter,
1819
Inject,
1920
InjectionToken,
2021
Input,
2122
NgZone,
2223
OnDestroy,
2324
OnInit,
2425
Optional,
26+
Output,
2527
QueryList,
2628
ViewChild,
2729
ViewEncapsulation
@@ -96,6 +98,14 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
9698
*/
9799
private _inputsInitialized = false;
98100

101+
/**
102+
* Emits when the selected state of the option has changed.
103+
* Use to facilitate two-data binding to the `selected` property.
104+
* @docs-private
105+
*/
106+
@Output()
107+
readonly selectedChange: EventEmitter<boolean> = new EventEmitter<boolean>();
108+
99109
@ViewChild('text') _itemText: ElementRef<HTMLElement>;
100110

101111
@ContentChildren(MatLine, {read: ElementRef, descendants: true}) lines:
@@ -241,6 +251,7 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit
241251
this._selectionList.selectedOptions.deselect(this);
242252
}
243253

254+
this.selectedChange.emit(selected);
244255
this._changeDetectorRef.markForCheck();
245256
return true;
246257
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,44 @@ describe('MDC-based MatSelectionList without forms', () => {
985985
});
986986

987987
});
988+
989+
describe('with single selection', () => {
990+
let fixture: ComponentFixture<ListOptionWithTwoWayBinding>;
991+
let optionElement: HTMLElement;
992+
let option: MatListOption;
993+
994+
beforeEach(waitForAsync(() => {
995+
TestBed.configureTestingModule({
996+
imports: [MatListModule],
997+
declarations: [ListOptionWithTwoWayBinding],
998+
}).compileComponents();
999+
1000+
fixture = TestBed.createComponent(ListOptionWithTwoWayBinding);
1001+
fixture.detectChanges();
1002+
const optionDebug = fixture.debugElement.query(By.directive(MatListOption));
1003+
option = optionDebug.componentInstance;
1004+
optionElement = optionDebug.nativeElement;
1005+
}));
1006+
1007+
it('should sync the value from the view to the option', () => {
1008+
expect(option.selected).toBe(false);
1009+
1010+
fixture.componentInstance.selected = true;
1011+
fixture.detectChanges();
1012+
1013+
expect(option.selected).toBe(true);
1014+
});
1015+
1016+
it('should sync the value from the option to the view', () => {
1017+
expect(fixture.componentInstance.selected).toBe(false);
1018+
1019+
optionElement.click();
1020+
fixture.detectChanges();
1021+
1022+
expect(fixture.componentInstance.selected).toBe(true);
1023+
});
1024+
});
1025+
9881026
});
9891027

9901028
describe('MDC-based MatSelectionList with forms', () => {
@@ -1651,3 +1689,13 @@ class SelectionListWithIndirectChildOptions {
16511689
})
16521690
class SelectionListWithIndirectDescendantLines {
16531691
}
1692+
1693+
1694+
@Component({template: `
1695+
<mat-selection-list>
1696+
<mat-list-option [(selected)]="selected">Item</mat-list-option>
1697+
</mat-selection-list>
1698+
`})
1699+
class ListOptionWithTwoWayBinding {
1700+
selected = false;
1701+
}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,43 @@ describe('MatSelectionList without forms', () => {
11451145
});
11461146

11471147
});
1148+
1149+
describe('with single selection', () => {
1150+
let fixture: ComponentFixture<ListOptionWithTwoWayBinding>;
1151+
let optionElement: HTMLElement;
1152+
let option: MatListOption;
1153+
1154+
beforeEach(waitForAsync(() => {
1155+
TestBed.configureTestingModule({
1156+
imports: [MatListModule],
1157+
declarations: [ListOptionWithTwoWayBinding],
1158+
}).compileComponents();
1159+
1160+
fixture = TestBed.createComponent(ListOptionWithTwoWayBinding);
1161+
fixture.detectChanges();
1162+
const optionDebug = fixture.debugElement.query(By.directive(MatListOption));
1163+
option = optionDebug.componentInstance;
1164+
optionElement = optionDebug.nativeElement;
1165+
}));
1166+
1167+
it('should sync the value from the view to the option', () => {
1168+
expect(option.selected).toBe(false);
1169+
1170+
fixture.componentInstance.selected = true;
1171+
fixture.detectChanges();
1172+
1173+
expect(option.selected).toBe(true);
1174+
});
1175+
1176+
it('should sync the value from the option to the view', () => {
1177+
expect(fixture.componentInstance.selected).toBe(false);
1178+
1179+
optionElement.click();
1180+
fixture.detectChanges();
1181+
1182+
expect(fixture.componentInstance.selected).toBe(true);
1183+
});
1184+
});
11481185
});
11491186

11501187
describe('MatSelectionList with forms', () => {
@@ -1828,3 +1865,13 @@ class SelectionListWithIndirectChildOptions {
18281865
})
18291866
class SelectionListWithIndirectDescendantLines {
18301867
}
1868+
1869+
1870+
@Component({template: `
1871+
<mat-selection-list>
1872+
<mat-list-option [(selected)]="selected">Item</mat-list-option>
1873+
</mat-selection-list>
1874+
`})
1875+
class ListOptionWithTwoWayBinding {
1876+
selected = false;
1877+
}

src/material/list/selection-list.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ export class MatListOption extends _MatListOptionBase implements AfterContentIni
127127
@ContentChild(MatListIconCssMatStyler) _icon: MatListIconCssMatStyler;
128128
@ContentChildren(MatLine, {descendants: true}) _lines: QueryList<MatLine>;
129129

130+
/**
131+
* Emits when the selected state of the option has changed.
132+
* Use to facilitate two-data binding to the `selected` property.
133+
* @docs-private
134+
*/
135+
@Output()
136+
readonly selectedChange: EventEmitter<boolean> = new EventEmitter<boolean>();
137+
130138
/** DOM element containing the item's text. */
131139
@ViewChild('text') _text: ElementRef;
132140

@@ -300,6 +308,7 @@ export class MatListOption extends _MatListOptionBase implements AfterContentIni
300308
this.selectionList.selectedOptions.deselect(this);
301309
}
302310

311+
this.selectedChange.emit(selected);
303312
this._changeDetector.markForCheck();
304313
return true;
305314
}

tools/public_api_guard/material/list.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export declare class MatListOption extends _MatListOptionBase implements AfterCo
6161
set disabled(value: any);
6262
get selected(): boolean;
6363
set selected(value: boolean);
64+
readonly selectedChange: EventEmitter<boolean>;
6465
selectionList: MatSelectionList;
6566
get value(): any;
6667
set value(newValue: any);
@@ -82,7 +83,7 @@ export declare class MatListOption extends _MatListOptionBase implements AfterCo
8283
static ngAcceptInputType_disableRipple: BooleanInput;
8384
static ngAcceptInputType_disabled: BooleanInput;
8485
static ngAcceptInputType_selected: BooleanInput;
85-
static ɵcmp: i0.ɵɵComponentDeclaration<MatListOption, "mat-list-option", ["matListOption"], { "disableRipple": "disableRipple"; "checkboxPosition": "checkboxPosition"; "color": "color"; "value": "value"; "disabled": "disabled"; "selected": "selected"; }, {}, ["_avatar", "_icon", "_lines"], ["*", "[mat-list-avatar], [mat-list-icon], [matListAvatar], [matListIcon]"]>;
86+
static ɵcmp: i0.ɵɵComponentDeclaration<MatListOption, "mat-list-option", ["matListOption"], { "disableRipple": "disableRipple"; "checkboxPosition": "checkboxPosition"; "color": "color"; "value": "value"; "disabled": "disabled"; "selected": "selected"; }, { "selectedChange": "selectedChange"; }, ["_avatar", "_icon", "_lines"], ["*", "[mat-list-avatar], [mat-list-icon], [matListAvatar], [matListIcon]"]>;
8687
static ɵfac: i0.ɵɵFactoryDeclaration<MatListOption, never>;
8788
}
8889

0 commit comments

Comments
 (0)