From 5543c6ec92786128186d67fba71cca2d62ad9eda Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 7 Jul 2021 06:53:45 +0200 Subject: [PATCH] feat(material/list): support two-data binding on list option selected Adds support for two-way data binding on the `selected` property of `mat-list-option`. Fixes #23122. --- .../mdc-list/list-option.ts | 11 +++++ .../mdc-list/selection-list.spec.ts | 48 +++++++++++++++++++ src/material/list/selection-list.spec.ts | 47 ++++++++++++++++++ src/material/list/selection-list.ts | 9 ++++ tools/public_api_guard/material/list.d.ts | 3 +- 5 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/material-experimental/mdc-list/list-option.ts b/src/material-experimental/mdc-list/list-option.ts index 7c6fd85742e4..84b66a1fd8eb 100644 --- a/src/material-experimental/mdc-list/list-option.ts +++ b/src/material-experimental/mdc-list/list-option.ts @@ -15,6 +15,7 @@ import { Component, ContentChildren, ElementRef, + EventEmitter, Inject, InjectionToken, Input, @@ -22,6 +23,7 @@ import { OnDestroy, OnInit, Optional, + Output, QueryList, ViewChild, ViewEncapsulation @@ -96,6 +98,14 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit */ private _inputsInitialized = false; + /** + * Emits when the selected state of the option has changed. + * Use to facilitate two-data binding to the `selected` property. + * @docs-private + */ + @Output() + readonly selectedChange: EventEmitter = new EventEmitter(); + @ViewChild('text') _itemText: ElementRef; @ContentChildren(MatLine, {read: ElementRef, descendants: true}) lines: @@ -241,6 +251,7 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit this._selectionList.selectedOptions.deselect(this); } + this.selectedChange.emit(selected); this._changeDetectorRef.markForCheck(); return true; } diff --git a/src/material-experimental/mdc-list/selection-list.spec.ts b/src/material-experimental/mdc-list/selection-list.spec.ts index 9b8fb5941ed3..d3b8e1086fb9 100644 --- a/src/material-experimental/mdc-list/selection-list.spec.ts +++ b/src/material-experimental/mdc-list/selection-list.spec.ts @@ -985,6 +985,44 @@ describe('MDC-based MatSelectionList without forms', () => { }); }); + + describe('with single selection', () => { + let fixture: ComponentFixture; + let optionElement: HTMLElement; + let option: MatListOption; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [MatListModule], + declarations: [ListOptionWithTwoWayBinding], + }).compileComponents(); + + fixture = TestBed.createComponent(ListOptionWithTwoWayBinding); + fixture.detectChanges(); + const optionDebug = fixture.debugElement.query(By.directive(MatListOption)); + option = optionDebug.componentInstance; + optionElement = optionDebug.nativeElement; + })); + + it('should sync the value from the view to the option', () => { + expect(option.selected).toBe(false); + + fixture.componentInstance.selected = true; + fixture.detectChanges(); + + expect(option.selected).toBe(true); + }); + + it('should sync the value from the option to the view', () => { + expect(fixture.componentInstance.selected).toBe(false); + + optionElement.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.selected).toBe(true); + }); + }); + }); describe('MDC-based MatSelectionList with forms', () => { @@ -1651,3 +1689,13 @@ class SelectionListWithIndirectChildOptions { }) class SelectionListWithIndirectDescendantLines { } + + +@Component({template: ` + + Item + +`}) +class ListOptionWithTwoWayBinding { + selected = false; +} diff --git a/src/material/list/selection-list.spec.ts b/src/material/list/selection-list.spec.ts index 3272b9714185..613e5eb58830 100644 --- a/src/material/list/selection-list.spec.ts +++ b/src/material/list/selection-list.spec.ts @@ -1145,6 +1145,43 @@ describe('MatSelectionList without forms', () => { }); }); + + describe('with single selection', () => { + let fixture: ComponentFixture; + let optionElement: HTMLElement; + let option: MatListOption; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [MatListModule], + declarations: [ListOptionWithTwoWayBinding], + }).compileComponents(); + + fixture = TestBed.createComponent(ListOptionWithTwoWayBinding); + fixture.detectChanges(); + const optionDebug = fixture.debugElement.query(By.directive(MatListOption)); + option = optionDebug.componentInstance; + optionElement = optionDebug.nativeElement; + })); + + it('should sync the value from the view to the option', () => { + expect(option.selected).toBe(false); + + fixture.componentInstance.selected = true; + fixture.detectChanges(); + + expect(option.selected).toBe(true); + }); + + it('should sync the value from the option to the view', () => { + expect(fixture.componentInstance.selected).toBe(false); + + optionElement.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.selected).toBe(true); + }); + }); }); describe('MatSelectionList with forms', () => { @@ -1828,3 +1865,13 @@ class SelectionListWithIndirectChildOptions { }) class SelectionListWithIndirectDescendantLines { } + + +@Component({template: ` + + Item + +`}) +class ListOptionWithTwoWayBinding { + selected = false; +} diff --git a/src/material/list/selection-list.ts b/src/material/list/selection-list.ts index caeeb0b4faeb..c3310355c713 100644 --- a/src/material/list/selection-list.ts +++ b/src/material/list/selection-list.ts @@ -127,6 +127,14 @@ export class MatListOption extends _MatListOptionBase implements AfterContentIni @ContentChild(MatListIconCssMatStyler) _icon: MatListIconCssMatStyler; @ContentChildren(MatLine, {descendants: true}) _lines: QueryList; + /** + * Emits when the selected state of the option has changed. + * Use to facilitate two-data binding to the `selected` property. + * @docs-private + */ + @Output() + readonly selectedChange: EventEmitter = new EventEmitter(); + /** DOM element containing the item's text. */ @ViewChild('text') _text: ElementRef; @@ -300,6 +308,7 @@ export class MatListOption extends _MatListOptionBase implements AfterContentIni this.selectionList.selectedOptions.deselect(this); } + this.selectedChange.emit(selected); this._changeDetector.markForCheck(); return true; } diff --git a/tools/public_api_guard/material/list.d.ts b/tools/public_api_guard/material/list.d.ts index 2e23a538e962..b9bc66116f37 100644 --- a/tools/public_api_guard/material/list.d.ts +++ b/tools/public_api_guard/material/list.d.ts @@ -61,6 +61,7 @@ export declare class MatListOption extends _MatListOptionBase implements AfterCo set disabled(value: any); get selected(): boolean; set selected(value: boolean); + readonly selectedChange: EventEmitter; selectionList: MatSelectionList; get value(): any; set value(newValue: any); @@ -82,7 +83,7 @@ export declare class MatListOption extends _MatListOptionBase implements AfterCo static ngAcceptInputType_disableRipple: BooleanInput; static ngAcceptInputType_disabled: BooleanInput; static ngAcceptInputType_selected: BooleanInput; - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; static ɵfac: i0.ɵɵFactoryDeclaration; }