mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(selection-list): support for ngModel (#7456)
* feat(selection-list): support for ngModel * Adds support for NgModel to the selection-list. Fixes #6896 * Rename to selectionChange. Add deprecated `selectionChange` event on option * Reintroduce MatListOptionChange for deprecation
This commit is contained in:
committed by
mmalerba
parent
b6f2484a94
commit
3fbb28a646
@@ -105,7 +105,9 @@
|
||||
<div>
|
||||
<h2>Selection list</h2>
|
||||
|
||||
<mat-selection-list #groceries>
|
||||
<mat-selection-list #groceries [ngModel]="selectedOptions"
|
||||
(ngModelChange)="onSelectedOptionsChange($event)"
|
||||
(change)="changeEventCount = changeEventCount + 1">
|
||||
<h3 mat-subheader>Groceries</h3>
|
||||
|
||||
<mat-list-option value="bananas">Bananas</mat-list-option>
|
||||
@@ -114,7 +116,10 @@
|
||||
<mat-list-option value="strawberries">Strawberries</mat-list-option>
|
||||
</mat-selection-list>
|
||||
|
||||
<p>Selected: {{groceries.selectedOptions.selected.length}}</p>
|
||||
<p>Selected: {{selectedOptions | json}}</p>
|
||||
<p>Change Event Count {{changeEventCount}}</p>
|
||||
<p>Model Change Event Count {{modelChangeEventCount}}</p>
|
||||
|
||||
<p>
|
||||
<button mat-raised-button (click)="groceries.selectAll()">Select all</button>
|
||||
<button mat-raised-button (click)="groceries.deselectAll()">Deselect all</button>
|
||||
|
||||
@@ -59,4 +59,13 @@ export class ListDemo {
|
||||
|
||||
thirdLine: boolean = false;
|
||||
infoClicked: boolean = false;
|
||||
|
||||
selectedOptions: string[] = ['apples'];
|
||||
changeEventCount: number = 0;
|
||||
modelChangeEventCount: number = 0;
|
||||
|
||||
onSelectedOptionsChange(values: string[]) {
|
||||
this.selectedOptions = values;
|
||||
this.modelChangeEventCount++;
|
||||
}
|
||||
}
|
||||
|
||||
+229
-106
@@ -2,12 +2,18 @@ import {DOWN_ARROW, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
|
||||
import {Platform} from '@angular/cdk/platform';
|
||||
import {createKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
|
||||
import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MatListModule, MatListOption, MatSelectionList, MatListOptionChange} from './index';
|
||||
import {
|
||||
MatListModule,
|
||||
MatListOption,
|
||||
MatListOptionChange,
|
||||
MatSelectionList,
|
||||
MatSelectionListChange
|
||||
} from './index';
|
||||
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
|
||||
|
||||
|
||||
describe('MatSelectionList', () => {
|
||||
describe('MatSelectionList without forms', () => {
|
||||
describe('with list option', () => {
|
||||
let fixture: ComponentFixture<SelectionListWithListOptions>;
|
||||
let listOptions: DebugElement[];
|
||||
@@ -61,6 +67,44 @@ describe('MatSelectionList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit a selectionChange event if an option changed programmatically', () => {
|
||||
spyOn(fixture.componentInstance, 'onValueChange');
|
||||
|
||||
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
listOptions[2].componentInstance.toggle();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should emit a selectionChange event if an option got clicked', () => {
|
||||
spyOn(fixture.componentInstance, 'onValueChange');
|
||||
|
||||
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
dispatchFakeEvent(listOptions[2].nativeElement, 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should emit a deprecated selectionChange event on the list option that got clicked', () => {
|
||||
const optionInstance = listOptions[2].componentInstance as MatListOption;
|
||||
let lastChangeEvent: MatListOptionChange | null = null;
|
||||
|
||||
optionInstance.selectionChange.subscribe(ev => lastChangeEvent = ev);
|
||||
|
||||
expect(lastChangeEvent).toBeNull();
|
||||
|
||||
dispatchFakeEvent(listOptions[2].nativeElement, 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(lastChangeEvent).not.toBeNull();
|
||||
expect(lastChangeEvent!.source).toBe(optionInstance);
|
||||
expect(lastChangeEvent!.selected).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to dispatch one selected item', () => {
|
||||
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
|
||||
let selectList =
|
||||
@@ -480,90 +524,167 @@ describe('MatSelectionList', () => {
|
||||
expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('with multiple values', () => {
|
||||
let fixture: ComponentFixture<SelectionListWithMultipleValues>;
|
||||
let listOption: DebugElement[];
|
||||
let listItemEl: DebugElement;
|
||||
let selectionList: DebugElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatListModule],
|
||||
declarations: [
|
||||
SelectionListWithMultipleValues
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(SelectionListWithMultipleValues);
|
||||
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
|
||||
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
|
||||
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should have a value for each item', () => {
|
||||
expect(listOption[0].componentInstance.value).toBe(1);
|
||||
expect(listOption[1].componentInstance.value).toBe('a');
|
||||
expect(listOption[2].componentInstance.value).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('with option selected events', () => {
|
||||
let fixture: ComponentFixture<SelectionListWithOptionEvents>;
|
||||
let testComponent: SelectionListWithOptionEvents;
|
||||
let listOption: DebugElement[];
|
||||
let selectionList: DebugElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatListModule],
|
||||
declarations: [
|
||||
SelectionListWithOptionEvents
|
||||
],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(SelectionListWithOptionEvents);
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
|
||||
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should trigger the selected and deselected events when clicked in succession.', () => {
|
||||
|
||||
let selected: boolean = false;
|
||||
|
||||
spyOn(testComponent, 'onOptionSelectionChange')
|
||||
.and.callFake((event: MatListOptionChange) => {
|
||||
selected = event.selected;
|
||||
});
|
||||
|
||||
listOption[0].nativeElement.click();
|
||||
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(1);
|
||||
expect(selected).toBe(true);
|
||||
|
||||
listOption[0].nativeElement.click();
|
||||
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(2);
|
||||
expect(selected).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('MatSelectionList with forms', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatListModule, FormsModule, ReactiveFormsModule],
|
||||
declarations: [
|
||||
SelectionListWithModel,
|
||||
SelectionListWithFormControl
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('and ngModel', () => {
|
||||
let fixture: ComponentFixture<SelectionListWithModel>;
|
||||
let selectionListDebug: DebugElement;
|
||||
let selectionList: MatSelectionList;
|
||||
let listOptions: MatListOption[];
|
||||
let ngModel: NgModel;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SelectionListWithModel);
|
||||
fixture.detectChanges();
|
||||
|
||||
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
|
||||
selectionList = selectionListDebug.componentInstance;
|
||||
ngModel = selectionListDebug.injector.get<NgModel>(NgModel);
|
||||
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
|
||||
.map(optionDebugEl => optionDebugEl.componentInstance);
|
||||
});
|
||||
|
||||
it('should update the model if an option got selected programmatically', fakeAsync(() => {
|
||||
expect(fixture.componentInstance.selectedOptions.length)
|
||||
.toBe(0, 'Expected no options to be selected by default');
|
||||
|
||||
listOptions[0].toggle();
|
||||
fixture.detectChanges();
|
||||
|
||||
tick();
|
||||
|
||||
expect(fixture.componentInstance.selectedOptions.length)
|
||||
.toBe(1, 'Expected first list option to be selected');
|
||||
}));
|
||||
|
||||
it('should update the model if an option got clicked', fakeAsync(() => {
|
||||
expect(fixture.componentInstance.selectedOptions.length)
|
||||
.toBe(0, 'Expected no options to be selected by default');
|
||||
|
||||
dispatchFakeEvent(listOptions[0]._getHostElement(), 'click');
|
||||
fixture.detectChanges();
|
||||
|
||||
tick();
|
||||
|
||||
expect(fixture.componentInstance.selectedOptions.length)
|
||||
.toBe(1, 'Expected first list option to be selected');
|
||||
}));
|
||||
|
||||
it('should update the options if a model value is set', fakeAsync(() => {
|
||||
expect(fixture.componentInstance.selectedOptions.length)
|
||||
.toBe(0, 'Expected no options to be selected by default');
|
||||
|
||||
fixture.componentInstance.selectedOptions = ['opt3'];
|
||||
fixture.detectChanges();
|
||||
|
||||
tick();
|
||||
|
||||
expect(fixture.componentInstance.selectedOptions.length)
|
||||
.toBe(1, 'Expected first list option to be selected');
|
||||
}));
|
||||
|
||||
it('should set the selection-list to touched on blur', fakeAsync(() => {
|
||||
expect(ngModel.touched)
|
||||
.toBe(false, 'Expected the selection-list to be untouched by default.');
|
||||
|
||||
dispatchFakeEvent(selectionListDebug.nativeElement, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
tick();
|
||||
|
||||
expect(ngModel.touched).toBe(true, 'Expected the selection-list to be touched after blur');
|
||||
}));
|
||||
|
||||
it('should be pristine by default', fakeAsync(() => {
|
||||
fixture = TestBed.createComponent(SelectionListWithModel);
|
||||
fixture.componentInstance.selectedOptions = ['opt2'];
|
||||
fixture.detectChanges();
|
||||
|
||||
ngModel =
|
||||
fixture.debugElement.query(By.directive(MatSelectionList)).injector.get<NgModel>(NgModel);
|
||||
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
|
||||
.map(optionDebugEl => optionDebugEl.componentInstance);
|
||||
|
||||
// Flush the initial tick to ensure that every action from the ControlValueAccessor
|
||||
// happened before the actual test starts.
|
||||
tick();
|
||||
|
||||
expect(ngModel.pristine)
|
||||
.toBe(true, 'Expected the selection-list to be pristine by default.');
|
||||
|
||||
listOptions[1].toggle();
|
||||
fixture.detectChanges();
|
||||
|
||||
tick();
|
||||
|
||||
expect(ngModel.pristine)
|
||||
.toBe(false, 'Expected the selection-list to be dirty after state change.');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and formControl', () => {
|
||||
let fixture: ComponentFixture<SelectionListWithFormControl>;
|
||||
let selectionListDebug: DebugElement;
|
||||
let selectionList: MatSelectionList;
|
||||
let listOptions: MatListOption[];
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SelectionListWithFormControl);
|
||||
fixture.detectChanges();
|
||||
|
||||
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
|
||||
selectionList = selectionListDebug.componentInstance;
|
||||
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
|
||||
.map(optionDebugEl => optionDebugEl.componentInstance);
|
||||
});
|
||||
|
||||
it('should be able to disable options from the control', () => {
|
||||
expect(listOptions.every(option => !option.disabled))
|
||||
.toBe(true, 'Expected every list option to be enabled.');
|
||||
|
||||
fixture.componentInstance.formControl.disable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(listOptions.every(option => option.disabled))
|
||||
.toBe(true, 'Expected every list option to be disabled.');
|
||||
});
|
||||
|
||||
it('should be able to set the value through the form control', () => {
|
||||
expect(listOptions.every(option => !option.selected))
|
||||
.toBe(true, 'Expected every list option to be unselected.');
|
||||
|
||||
fixture.componentInstance.formControl.setValue(['opt2', 'opt3']);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(listOptions[1].selected).toBe(true, 'Expected second option to be selected.');
|
||||
expect(listOptions[2].selected).toBe(true, 'Expected third option to be selected.');
|
||||
|
||||
fixture.componentInstance.formControl.setValue(null);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(listOptions.every(option => !option.selected))
|
||||
.toBe(true, 'Expected every list option to be unselected.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@Component({template: `
|
||||
<mat-selection-list id="selection-list-1">
|
||||
<mat-selection-list id="selection-list-1" (selectionChange)="onValueChange($event)">
|
||||
<mat-list-option checkboxPosition="before" disabled="true" value="inbox">
|
||||
Inbox (disabled selection-option)
|
||||
</mat-list-option>
|
||||
@@ -580,6 +701,8 @@ describe('MatSelectionList', () => {
|
||||
</mat-selection-list>`})
|
||||
class SelectionListWithListOptions {
|
||||
showLastOption: boolean = true;
|
||||
|
||||
onValueChange(_change: MatSelectionListChange) {}
|
||||
}
|
||||
|
||||
@Component({template: `
|
||||
@@ -656,27 +779,27 @@ class SelectionListWithTabindexBinding {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@Component({template: `
|
||||
<mat-selection-list id="selection-list-5">
|
||||
<mat-list-option [value]="1" checkboxPosition="after">
|
||||
1
|
||||
</mat-list-option>
|
||||
<mat-list-option value="a" checkboxPosition="after">
|
||||
a
|
||||
</mat-list-option>
|
||||
<mat-list-option [value]="true" checkboxPosition="after">
|
||||
true
|
||||
</mat-list-option>
|
||||
</mat-selection-list>`})
|
||||
class SelectionListWithMultipleValues {
|
||||
@Component({
|
||||
template: `
|
||||
<mat-selection-list [(ngModel)]="selectedOptions">
|
||||
<mat-list-option value="opt1">Option 1</mat-list-option>
|
||||
<mat-list-option value="opt2">Option 2</mat-list-option>
|
||||
<mat-list-option value="opt3">Option 3</mat-list-option>
|
||||
</mat-selection-list>`
|
||||
})
|
||||
class SelectionListWithModel {
|
||||
selectedOptions: string[] = [];
|
||||
}
|
||||
|
||||
@Component({template: `
|
||||
<mat-selection-list id="selection-list-6">
|
||||
<mat-list-option (selectionChange)="onOptionSelectionChange($event)">
|
||||
Inbox
|
||||
</mat-list-option>
|
||||
</mat-selection-list>`})
|
||||
class SelectionListWithOptionEvents {
|
||||
onOptionSelectionChange: (event?: MatListOptionChange) => void = () => {};
|
||||
@Component({
|
||||
template: `
|
||||
<mat-selection-list [formControl]="formControl">
|
||||
<mat-list-option value="opt1">Option 1</mat-list-option>
|
||||
<mat-list-option value="opt2">Option 2</mat-list-option>
|
||||
<mat-list-option value="opt3">Option 3</mat-list-option>
|
||||
</mat-selection-list>
|
||||
`
|
||||
})
|
||||
class SelectionListWithFormControl {
|
||||
formControl = new FormControl();
|
||||
}
|
||||
|
||||
+169
-41
@@ -39,6 +39,7 @@ import {
|
||||
mixinDisableRipple,
|
||||
mixinTabIndex,
|
||||
} from '@angular/material/core';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
|
||||
|
||||
/** @docs-private */
|
||||
@@ -50,12 +51,32 @@ export const _MatSelectionListMixinBase =
|
||||
export class MatListOptionBase {}
|
||||
export const _MatListOptionMixinBase = mixinDisableRipple(MatListOptionBase);
|
||||
|
||||
/** Change event object emitted by MatListOption */
|
||||
/** @docs-private */
|
||||
export const MAT_SELECTION_LIST_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => MatSelectionList),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Change event object emitted by MatListOption whenever the selected state changes.
|
||||
* @deprecated Use the `MatSelectionListChange` event on the selection list instead.
|
||||
*/
|
||||
export class MatListOptionChange {
|
||||
/** The source MatListOption of the event. */
|
||||
source: MatListOption;
|
||||
/** The new `selected` value of the option. */
|
||||
selected: boolean;
|
||||
constructor(
|
||||
/** Reference to the list option that changed. */
|
||||
public source: MatListOption,
|
||||
/** The new selected state of the option. */
|
||||
public selected: boolean) {}
|
||||
}
|
||||
|
||||
/** Change event that is being fired whenever the selected state of an option changes. */
|
||||
export class MatSelectionListChange {
|
||||
constructor(
|
||||
/** Reference to the selection list that emitted the event. */
|
||||
public source: MatSelectionList,
|
||||
/** Reference to the option that has been changed. */
|
||||
public option: MatListOption) {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +93,7 @@ export class MatListOptionChange {
|
||||
'role': 'option',
|
||||
'class': 'mat-list-item mat-list-option',
|
||||
'(focus)': '_handleFocus()',
|
||||
'(blur)': '_hasFocus = false',
|
||||
'(blur)': '_handleBlur()',
|
||||
'(click)': '_handleClick()',
|
||||
'tabindex': '-1',
|
||||
'[class.mat-list-item-disabled]': 'disabled',
|
||||
@@ -86,8 +107,10 @@ export class MatListOptionChange {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MatListOption extends _MatListOptionMixinBase
|
||||
implements AfterContentInit, OnInit, OnDestroy, FocusableOption, CanDisableRipple {
|
||||
implements AfterContentInit, OnDestroy, OnInit, FocusableOption, CanDisableRipple {
|
||||
|
||||
private _lineSetter: MatLineSetter;
|
||||
private _selected: boolean = false;
|
||||
private _disabled: boolean = false;
|
||||
|
||||
/** Whether the option has focus. */
|
||||
@@ -98,31 +121,39 @@ export class MatListOption extends _MatListOptionMixinBase
|
||||
/** Whether the label should appear before or after the checkbox. Defaults to 'after' */
|
||||
@Input() checkboxPosition: 'before' | 'after' = 'after';
|
||||
|
||||
/** Whether the option is disabled. */
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return (this.selectionList && this.selectionList.disabled) || this._disabled;
|
||||
}
|
||||
set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); }
|
||||
|
||||
/** Value of the option */
|
||||
@Input() value: any;
|
||||
|
||||
/** Whether the option is disabled. */
|
||||
@Input()
|
||||
get disabled() { return (this.selectionList && this.selectionList.disabled) || this._disabled; }
|
||||
set disabled(value: any) {
|
||||
const newValue = coerceBooleanProperty(value);
|
||||
|
||||
if (newValue !== this._disabled) {
|
||||
this._disabled = newValue;
|
||||
this._changeDetector.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the option is selected. */
|
||||
@Input()
|
||||
get selected(): boolean { return this.selectionList.selectedOptions.isSelected(this); }
|
||||
set selected(value: boolean) {
|
||||
const isSelected = coerceBooleanProperty(value);
|
||||
|
||||
if (isSelected !== this.selected) {
|
||||
this.selectionList.selectedOptions.toggle(this);
|
||||
this._changeDetector.markForCheck();
|
||||
this.selectionChange.emit(this._createChangeEvent());
|
||||
if (isSelected !== this._selected) {
|
||||
this._setSelected(isSelected);
|
||||
this.selectionList._reportValueChange();
|
||||
}
|
||||
}
|
||||
|
||||
/** Emitted when the option is selected or deselected. */
|
||||
@Output() selectionChange = new EventEmitter<MatListOptionChange>();
|
||||
/**
|
||||
* Emits a change event whenever the selected state of an option changes.
|
||||
* @deprecated Use the `selectionChange` event on the `<mat-selection-list>` instead.
|
||||
*/
|
||||
@Output() selectionChange: EventEmitter<MatListOptionChange> =
|
||||
new EventEmitter<MatListOptionChange>();
|
||||
|
||||
constructor(private _element: ElementRef,
|
||||
private _changeDetector: ChangeDetectorRef,
|
||||
@@ -133,7 +164,12 @@ export class MatListOption extends _MatListOptionMixinBase
|
||||
|
||||
ngOnInit() {
|
||||
if (this.selected) {
|
||||
this.selectionList.selectedOptions.select(this);
|
||||
// List options that are selected at initialization can't be reported properly to the form
|
||||
// control. This is because it takes some time until the selection-list knows about all
|
||||
// available options. Also it can happen that the ControlValueAccessor has an initial value
|
||||
// that should be used instead. Deferring the value change report to the next tick ensures
|
||||
// that the form control value is not being overwritten.
|
||||
Promise.resolve(() => this.selected && this.selectionList._reportValueChange());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +199,12 @@ export class MatListOption extends _MatListOptionMixinBase
|
||||
_handleClick() {
|
||||
if (!this.disabled) {
|
||||
this.toggle();
|
||||
|
||||
// Emit a change event if the selected state of the option changed through user interaction.
|
||||
this.selectionList._emitChangeEvent(this);
|
||||
|
||||
// TODO: the `selectionChange` event on the option is deprecated. Remove that in the future.
|
||||
this._emitDeprecatedChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,20 +213,38 @@ export class MatListOption extends _MatListOptionMixinBase
|
||||
this.selectionList._setFocusedOption(this);
|
||||
}
|
||||
|
||||
/** Creates a selection event object from the specified option. */
|
||||
private _createChangeEvent(option: MatListOption = this): MatListOptionChange {
|
||||
const event = new MatListOptionChange();
|
||||
|
||||
event.source = option;
|
||||
event.selected = option.selected;
|
||||
|
||||
return event;
|
||||
_handleBlur() {
|
||||
this._hasFocus = false;
|
||||
this.selectionList.onTouched();
|
||||
}
|
||||
|
||||
/** Retrieves the DOM element of the component host. */
|
||||
_getHostElement(): HTMLElement {
|
||||
return this._element.nativeElement;
|
||||
}
|
||||
|
||||
/** Sets the selected state of the option. */
|
||||
_setSelected(selected: boolean) {
|
||||
if (selected === this._selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selected = selected;
|
||||
|
||||
if (selected) {
|
||||
this.selectionList.selectedOptions.select(this);
|
||||
} else {
|
||||
this.selectionList.selectedOptions.deselect(this);
|
||||
}
|
||||
|
||||
this._changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
/** Emits a selectionChange event for this option. */
|
||||
_emitDeprecatedChangeEvent() {
|
||||
// TODO: the `selectionChange` event on the option is deprecated. Remove that in the future.
|
||||
this.selectionChange.emit(new MatListOptionChange(this, this.selected));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,16 +261,18 @@ export class MatListOption extends _MatListOptionMixinBase
|
||||
'[tabIndex]': 'tabIndex',
|
||||
'class': 'mat-selection-list',
|
||||
'(focus)': 'focus()',
|
||||
'(blur)': 'onTouched()',
|
||||
'(keydown)': '_keydown($event)',
|
||||
'[attr.aria-disabled]': 'disabled.toString()'},
|
||||
template: '<ng-content></ng-content>',
|
||||
styleUrls: ['list.css'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
providers: [MAT_SELECTION_LIST_VALUE_ACCESSOR],
|
||||
preserveWhitespaces: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MatSelectionList extends _MatSelectionListMixinBase implements FocusableOption,
|
||||
CanDisable, CanDisableRipple, HasTabIndex, AfterContentInit {
|
||||
CanDisable, CanDisableRipple, HasTabIndex, AfterContentInit, ControlValueAccessor {
|
||||
|
||||
/** The FocusKeyManager which handles focus. */
|
||||
_keyManager: FocusKeyManager<MatListOption>;
|
||||
@@ -218,9 +280,19 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
|
||||
/** The option components contained within this selection-list. */
|
||||
@ContentChildren(MatListOption) options: QueryList<MatListOption>;
|
||||
|
||||
/** Emits a change event whenever the selected state of an option changes. */
|
||||
@Output() selectionChange: EventEmitter<MatSelectionListChange> =
|
||||
new EventEmitter<MatSelectionListChange>();
|
||||
|
||||
/** The currently selected options. */
|
||||
selectedOptions: SelectionModel<MatListOption> = new SelectionModel<MatListOption>(true);
|
||||
|
||||
/** View to model callback that should be called whenever the selected options change. */
|
||||
private _onChange: (value: any) => void = (_: any) => {};
|
||||
|
||||
/** View to model callback that should be called if the list or its options lost focus. */
|
||||
onTouched: () => void = () => {};
|
||||
|
||||
constructor(private _element: ElementRef, @Attribute('tabindex') tabIndex: string) {
|
||||
super();
|
||||
|
||||
@@ -238,20 +310,14 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
|
||||
|
||||
/** Selects all of the options. */
|
||||
selectAll() {
|
||||
this.options.forEach(option => {
|
||||
if (!option.selected) {
|
||||
option.toggle();
|
||||
}
|
||||
});
|
||||
this.options.forEach(option => option._setSelected(true));
|
||||
this._reportValueChange();
|
||||
}
|
||||
|
||||
/** Deselects all of the options. */
|
||||
deselectAll() {
|
||||
this.options.forEach(option => {
|
||||
if (option.selected) {
|
||||
option.toggle();
|
||||
}
|
||||
});
|
||||
this.options.forEach(option => option._setSelected(false));
|
||||
this._reportValueChange();
|
||||
}
|
||||
|
||||
/** Sets the focused option of the selection-list. */
|
||||
@@ -286,6 +352,62 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
|
||||
}
|
||||
}
|
||||
|
||||
/** Reports a value change to the ControlValueAccessor */
|
||||
_reportValueChange() {
|
||||
if (this.options) {
|
||||
this._onChange(this._getSelectedOptionValues());
|
||||
}
|
||||
}
|
||||
|
||||
/** Emits a change event if the selected state of an option changed. */
|
||||
_emitChangeEvent(option: MatListOption) {
|
||||
this.selectionChange.emit(new MatSelectionListChange(this, option));
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
writeValue(values: string[]): void {
|
||||
if (this.options) {
|
||||
this._setOptionsFromValues(values || []);
|
||||
}
|
||||
}
|
||||
|
||||
/** Implemented as a part of ControlValueAccessor. */
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
if (this.options) {
|
||||
this.options.forEach(option => option.disabled = isDisabled);
|
||||
}
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
registerOnChange(fn: (value: any) => void): void {
|
||||
this._onChange = fn;
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
/** Returns the option with the specified value. */
|
||||
private _getOptionByValue(value: string): MatListOption | undefined {
|
||||
return this.options.find(option => option.value === value);
|
||||
}
|
||||
|
||||
/** Sets the selected options based on the specified values. */
|
||||
private _setOptionsFromValues(values: string[]) {
|
||||
this.options.forEach(option => option._setSelected(false));
|
||||
|
||||
values
|
||||
.map(value => this._getOptionByValue(value))
|
||||
.filter(Boolean)
|
||||
.forEach(option => option!._setSelected(true));
|
||||
}
|
||||
|
||||
/** Returns the values of the selected options. */
|
||||
private _getSelectedOptionValues(): string[] {
|
||||
return this.options.filter(option => option.selected).map(option => option.value);
|
||||
}
|
||||
|
||||
/** Toggles the selected state of the currently focused option. */
|
||||
private _toggleSelectOnFocusedOption(): void {
|
||||
let focusedIndex = this._keyManager.activeItemIndex;
|
||||
@@ -295,13 +417,19 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements Focu
|
||||
|
||||
if (focusedOption) {
|
||||
focusedOption.toggle();
|
||||
|
||||
// Emit a change event because the focused option changed its state through user
|
||||
// interaction.
|
||||
this._emitChangeEvent(focusedOption);
|
||||
|
||||
// TODO: the `selectionChange` event on the option is deprecated. Remove that in the future.
|
||||
focusedOption._emitDeprecatedChangeEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to ensure all indexes are valid.
|
||||
*
|
||||
* @param index The index to be checked.
|
||||
* @returns True if the index is valid for our list of options.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user