Skip to content

Commit e918191

Browse files
committed
feat(selection-list): support for ngModel
* Adds support for NgModel to the selection-list. Fixes #6896
1 parent c1712ac commit e918191

File tree

4 files changed

+288
-35
lines changed

4 files changed

+288
-35
lines changed

src/demo-app/list/list-demo.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ <h2>Nav lists</h2>
105105
<div>
106106
<h2>Selection list</h2>
107107

108-
<mat-selection-list #groceries>
108+
<mat-selection-list #groceries [ngModel]="selectedOptions"
109+
(ngModelChange)="onSelectedOptionsChange($event)"
110+
(change)="changeEventCount = changeEventCount + 1">
109111
<h3 mat-subheader>Groceries</h3>
110112

111113
<mat-list-option value="bananas">Bananas</mat-list-option>
@@ -114,7 +116,10 @@ <h3 mat-subheader>Groceries</h3>
114116
<mat-list-option value="strawberries">Strawberries</mat-list-option>
115117
</mat-selection-list>
116118

117-
<p>Selected: {{groceries.selectedOptions.selected.length}}</p>
119+
<p>Selected: {{selectedOptions | json}}</p>
120+
<p>Change Event Count {{changeEventCount}}</p>
121+
<p>Model Change Event Count {{modelChangeEventCount}}</p>
122+
118123
<p>
119124
<button mat-raised-button (click)="groceries.selectAll()">Select all</button>
120125
<button mat-raised-button (click)="groceries.deselectAll()">Deselect all</button>

src/demo-app/list/list-demo.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,13 @@ export class ListDemo {
5151

5252
thirdLine: boolean = false;
5353
infoClicked: boolean = false;
54+
55+
selectedOptions: string[] = ['apples'];
56+
changeEventCount: number = 0;
57+
modelChangeEventCount: number = 0;
58+
59+
onSelectedOptionsChange(values: string[]) {
60+
this.selectedOptions = values;
61+
this.modelChangeEventCount++;
62+
}
5463
}

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

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import {DOWN_ARROW, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
22
import {Platform} from '@angular/cdk/platform';
33
import {createKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
44
import {Component, DebugElement} from '@angular/core';
5-
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
5+
import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
66
import {By} from '@angular/platform-browser';
77
import {MatListModule, MatListOption, MatSelectionList} from './index';
8+
import {FormsModule, NgModel} from '@angular/forms';
89

910

10-
describe('MatSelectionList', () => {
11+
describe('MatSelectionList without forms', () => {
1112
describe('with list option', () => {
1213
let fixture: ComponentFixture<SelectionListWithListOptions>;
1314
let listOptions: DebugElement[];
@@ -442,6 +443,131 @@ describe('MatSelectionList', () => {
442443
});
443444
});
444445

446+
describe('MatSelectionList with forms', () => {
447+
448+
beforeEach(async(() => {
449+
TestBed.configureTestingModule({
450+
imports: [MatListModule, FormsModule],
451+
declarations: [
452+
SelectionListWithModel
453+
]
454+
});
455+
456+
TestBed.compileComponents();
457+
}));
458+
459+
describe('and ngModel', () => {
460+
let fixture: ComponentFixture<SelectionListWithModel>;
461+
let selectionListDebug: DebugElement;
462+
let selectionList: MatSelectionList;
463+
let listOptions: MatListOption[];
464+
let ngModel: NgModel;
465+
466+
beforeEach(() => {
467+
fixture = TestBed.createComponent(SelectionListWithModel);
468+
fixture.detectChanges();
469+
470+
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
471+
selectionList = selectionListDebug.componentInstance;
472+
ngModel = selectionListDebug.injector.get<NgModel>(NgModel);
473+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
474+
.map(optionDebugEl => optionDebugEl.componentInstance);
475+
});
476+
477+
it('should update the model if an option got selected', fakeAsync(() => {
478+
expect(fixture.componentInstance.selectedOptions.length)
479+
.toBe(0, 'Expected no options to be selected by default');
480+
481+
listOptions[0].toggle();
482+
fixture.detectChanges();
483+
484+
tick();
485+
486+
expect(fixture.componentInstance.selectedOptions.length)
487+
.toBe(1, 'Expected first list option to be selected');
488+
}));
489+
490+
it('should update the model if an option got clicked', fakeAsync(() => {
491+
expect(fixture.componentInstance.selectedOptions.length)
492+
.toBe(0, 'Expected no options to be selected by default');
493+
494+
dispatchFakeEvent(listOptions[0]._getHostElement(), 'click');
495+
fixture.detectChanges();
496+
497+
tick();
498+
499+
expect(fixture.componentInstance.selectedOptions.length)
500+
.toBe(1, 'Expected first list option to be selected');
501+
}));
502+
503+
it('should update the options if a model value is set', fakeAsync(() => {
504+
expect(fixture.componentInstance.selectedOptions.length)
505+
.toBe(0, 'Expected no options to be selected by default');
506+
507+
fixture.componentInstance.selectedOptions = ['opt3'];
508+
fixture.detectChanges();
509+
510+
tick();
511+
512+
expect(fixture.componentInstance.selectedOptions.length)
513+
.toBe(1, 'Expected first list option to be selected');
514+
}));
515+
516+
it('should set the selection-list to touched on blur', fakeAsync(() => {
517+
expect(ngModel.touched)
518+
.toBe(false, 'Expected the selection-list to be untouched by default.');
519+
520+
dispatchFakeEvent(selectionListDebug.nativeElement, 'blur');
521+
fixture.detectChanges();
522+
523+
tick();
524+
525+
expect(ngModel.touched).toBe(true, 'Expected the selection-list to be touched after blur');
526+
}));
527+
528+
it('should be able to disable the selection-list using the form control', fakeAsync(() => {
529+
expect(listOptions.every(option => !option.disabled))
530+
.toBe(true, 'Expected every list-option to be enabled');
531+
532+
ngModel.control.disable();
533+
fixture.detectChanges();
534+
535+
tick();
536+
537+
expect(listOptions.every(option => option.disabled))
538+
.toBe(true, 'Expected every list-option to be disabled');
539+
}));
540+
541+
542+
it('should be pristine by default', fakeAsync(() => {
543+
fixture = TestBed.createComponent(SelectionListWithModel);
544+
fixture.componentInstance.selectedOptions = ['opt2'];
545+
fixture.detectChanges();
546+
547+
ngModel =
548+
fixture.debugElement.query(By.directive(MatSelectionList)).injector.get<NgModel>(NgModel);
549+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
550+
.map(optionDebugEl => optionDebugEl.componentInstance);
551+
552+
// Flush the initial tick to ensure that every action from the ControlValueAccessor
553+
// happened before the actual test starts.
554+
tick();
555+
556+
expect(ngModel.pristine)
557+
.toBe(true, 'Expected the selection-list to be pristine by default.');
558+
559+
listOptions[1].toggle();
560+
fixture.detectChanges();
561+
562+
tick();
563+
564+
expect(ngModel.pristine)
565+
.toBe(false, 'Expected the selection-list to be dirty after state change.');
566+
}));
567+
});
568+
569+
});
570+
445571

446572
@Component({template: `
447573
<mat-selection-list id="selection-list-1">
@@ -523,3 +649,15 @@ class SelectionListWithSelectedOption {
523649
</mat-selection-list>`})
524650
class SelectionListWithOnlyOneOption {
525651
}
652+
653+
@Component({
654+
template: `
655+
<mat-selection-list [(ngModel)]="selectedOptions">
656+
<mat-list-option value="opt1">Option 1</mat-list-option>
657+
<mat-list-option value="opt2">Option 2</mat-list-option>
658+
<mat-list-option value="opt3">Option 3</mat-list-option>
659+
</mat-selection-list>`
660+
})
661+
class SelectionListWithModel {
662+
selectedOptions: string[] = [];
663+
}

0 commit comments

Comments
 (0)