Skip to content

Commit

Permalink
feat(selection-list): support for ngModel
Browse files Browse the repository at this point in the history
* Adds support for NgModel to the selection-list.

Fixes angular#6896
  • Loading branch information
devversion committed Oct 14, 2017
1 parent 9673f63 commit a72026d
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 125 deletions.
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 @@ -51,4 +51,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++;
}
}
263 changes: 182 additions & 81 deletions src/lib/list/selection-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ 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, 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[];
Expand Down Expand Up @@ -61,6 +61,28 @@ describe('MatSelectionList', () => {
});
});

it('should not emit a change 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 not emit a change 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 be able to dispatch one selected item', () => {
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
let selectList =
Expand Down Expand Up @@ -483,90 +505,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" (change)="onValueChange($event)">
<mat-list-option checkboxPosition="before" disabled="true" value="inbox">
Inbox (disabled selection-option)
</mat-list-option>
Expand All @@ -583,6 +682,8 @@ describe('MatSelectionList', () => {
</mat-selection-list>`})
class SelectionListWithListOptions {
showLastOption: boolean = true;

onValueChange(_change: MatSelectionListChange) {}
}

@Component({template: `
Expand Down Expand Up @@ -659,27 +760,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

0 comments on commit a72026d

Please sign in to comment.