Skip to content

feat(selection-list): support for ngModel #7456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/demo-app/list/list-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ <h2>Nav lists</h2>
<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>
Expand All @@ -114,7 +116,10 @@ <h3 mat-subheader>Groceries</h3>
<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>
Expand Down
9 changes: 9 additions & 0 deletions src/demo-app/list/list-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}
}
287 changes: 205 additions & 82 deletions src/lib/list/selection-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


describe('MatSelectionList', () => {
import {
MatListModule,
MatListOption,
MatListOptionChange,
MatSelectionList,
MatSelectionListChange
} from './index';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';

describe('MatSelectionList without forms', () => {
describe('with list option', () => {
let fixture: ComponentFixture<SelectionListWithListOptions>;
let listOptions: DebugElement[];
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -480,90 +524,167 @@ describe('MatSelectionList', () => {
expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
});
});
});

describe('MatSelectionList with forms', () => {

describe('with multiple values', () => {
let fixture: ComponentFixture<SelectionListWithMultipleValues>;
let listOption: DebugElement[];
let listItemEl: DebugElement;
let selectionList: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatListModule, FormsModule, ReactiveFormsModule],
declarations: [
SelectionListWithModel,
SelectionListWithFormControl
]
});

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [
SelectionListWithMultipleValues
],
});
TestBed.compileComponents();
}));

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');
}));

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));
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 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);
});
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();

describe('with option selected events', () => {
let fixture: ComponentFixture<SelectionListWithOptionEvents>;
let testComponent: SelectionListWithOptionEvents;
let listOption: DebugElement[];
let selectionList: DebugElement;
tick();

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
declarations: [
SelectionListWithOptionEvents
],
});
expect(fixture.componentInstance.selectedOptions.length)
.toBe(1, 'Expected first list option to be selected');
}));

TestBed.compileComponents();
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');
}));

beforeEach(async(() => {
fixture = TestBed.createComponent(SelectionListWithOptionEvents);
testComponent = fixture.debugElement.componentInstance;
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
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[];

it('should trigger the selected and deselected events when clicked in succession.', () => {
beforeEach(() => {
fixture = TestBed.createComponent(SelectionListWithFormControl);
fixture.detectChanges();

let selected: boolean = false;
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
selectionList = selectionListDebug.componentInstance;
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
});

spyOn(testComponent, 'onOptionSelectionChange')
.and.callFake((event: MatListOptionChange) => {
selected = event.selected;
});
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.');

listOption[0].nativeElement.click();
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(1);
expect(selected).toBe(true);
fixture.componentInstance.formControl.disable();
fixture.detectChanges();

listOption[0].nativeElement.click();
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(2);
expect(selected).toBe(false);
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>
Expand All @@ -580,6 +701,8 @@ describe('MatSelectionList', () => {
</mat-selection-list>`})
class SelectionListWithListOptions {
showLastOption: boolean = true;

onValueChange(_change: MatSelectionListChange) {}
}

@Component({template: `
Expand Down Expand Up @@ -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();
}
Loading