Skip to content

Commit 92d65d8

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

File tree

4 files changed

+363
-43
lines changed

4 files changed

+363
-43
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: 215 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ 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';
7-
import {MatListModule, MatListOption, MatSelectionList} from './index';
7+
import {MatListModule, MatListOption, MatSelectionList, MatSelectionListChange} from './index';
8+
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
89

9-
10-
describe('MatSelectionList', () => {
10+
describe('MatSelectionList without forms', () => {
1111
describe('with list option', () => {
1212
let fixture: ComponentFixture<SelectionListWithListOptions>;
1313
let listOptions: DebugElement[];
@@ -61,6 +61,28 @@ describe('MatSelectionList', () => {
6161
});
6262
});
6363

64+
it('should not emit a change event if an option changed programmatically', () => {
65+
spyOn(fixture.componentInstance, 'onValueChange');
66+
67+
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(0);
68+
69+
listOptions[2].componentInstance.toggle();
70+
fixture.detectChanges();
71+
72+
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(0);
73+
});
74+
75+
it('should not emit a change event if an option got clicked', () => {
76+
spyOn(fixture.componentInstance, 'onValueChange');
77+
78+
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(0);
79+
80+
dispatchFakeEvent(listOptions[2].nativeElement, 'click');
81+
fixture.detectChanges();
82+
83+
expect(fixture.componentInstance.onValueChange).toHaveBeenCalledTimes(1);
84+
});
85+
6486
it('should be able to dispatch one selected item', () => {
6587
let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
6688
let selectList =
@@ -442,9 +464,165 @@ describe('MatSelectionList', () => {
442464
});
443465
});
444466

467+
describe('MatSelectionList with forms', () => {
468+
469+
beforeEach(async(() => {
470+
TestBed.configureTestingModule({
471+
imports: [MatListModule, FormsModule, ReactiveFormsModule],
472+
declarations: [
473+
SelectionListWithModel,
474+
SelectionListWithFormControl
475+
]
476+
});
477+
478+
TestBed.compileComponents();
479+
}));
480+
481+
describe('and ngModel', () => {
482+
let fixture: ComponentFixture<SelectionListWithModel>;
483+
let selectionListDebug: DebugElement;
484+
let selectionList: MatSelectionList;
485+
let listOptions: MatListOption[];
486+
let ngModel: NgModel;
487+
488+
beforeEach(() => {
489+
fixture = TestBed.createComponent(SelectionListWithModel);
490+
fixture.detectChanges();
491+
492+
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
493+
selectionList = selectionListDebug.componentInstance;
494+
ngModel = selectionListDebug.injector.get<NgModel>(NgModel);
495+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
496+
.map(optionDebugEl => optionDebugEl.componentInstance);
497+
});
498+
499+
it('should update the model if an option got selected', fakeAsync(() => {
500+
expect(fixture.componentInstance.selectedOptions.length)
501+
.toBe(0, 'Expected no options to be selected by default');
502+
503+
listOptions[0].toggle();
504+
fixture.detectChanges();
505+
506+
tick();
507+
508+
expect(fixture.componentInstance.selectedOptions.length)
509+
.toBe(1, 'Expected first list option to be selected');
510+
}));
511+
512+
it('should update the model if an option got clicked', fakeAsync(() => {
513+
expect(fixture.componentInstance.selectedOptions.length)
514+
.toBe(0, 'Expected no options to be selected by default');
515+
516+
dispatchFakeEvent(listOptions[0]._getHostElement(), 'click');
517+
fixture.detectChanges();
518+
519+
tick();
520+
521+
expect(fixture.componentInstance.selectedOptions.length)
522+
.toBe(1, 'Expected first list option to be selected');
523+
}));
524+
525+
it('should update the options if a model value is set', fakeAsync(() => {
526+
expect(fixture.componentInstance.selectedOptions.length)
527+
.toBe(0, 'Expected no options to be selected by default');
528+
529+
fixture.componentInstance.selectedOptions = ['opt3'];
530+
fixture.detectChanges();
531+
532+
tick();
533+
534+
expect(fixture.componentInstance.selectedOptions.length)
535+
.toBe(1, 'Expected first list option to be selected');
536+
}));
537+
538+
it('should set the selection-list to touched on blur', fakeAsync(() => {
539+
expect(ngModel.touched)
540+
.toBe(false, 'Expected the selection-list to be untouched by default.');
541+
542+
dispatchFakeEvent(selectionListDebug.nativeElement, 'blur');
543+
fixture.detectChanges();
544+
545+
tick();
546+
547+
expect(ngModel.touched).toBe(true, 'Expected the selection-list to be touched after blur');
548+
}));
549+
550+
it('should be pristine by default', fakeAsync(() => {
551+
fixture = TestBed.createComponent(SelectionListWithModel);
552+
fixture.componentInstance.selectedOptions = ['opt2'];
553+
fixture.detectChanges();
554+
555+
ngModel =
556+
fixture.debugElement.query(By.directive(MatSelectionList)).injector.get<NgModel>(NgModel);
557+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
558+
.map(optionDebugEl => optionDebugEl.componentInstance);
559+
560+
// Flush the initial tick to ensure that every action from the ControlValueAccessor
561+
// happened before the actual test starts.
562+
tick();
563+
564+
expect(ngModel.pristine)
565+
.toBe(true, 'Expected the selection-list to be pristine by default.');
566+
567+
listOptions[1].toggle();
568+
fixture.detectChanges();
569+
570+
tick();
571+
572+
expect(ngModel.pristine)
573+
.toBe(false, 'Expected the selection-list to be dirty after state change.');
574+
}));
575+
});
576+
577+
describe('and formControl', () => {
578+
let fixture: ComponentFixture<SelectionListWithFormControl>;
579+
let selectionListDebug: DebugElement;
580+
let selectionList: MatSelectionList;
581+
let listOptions: MatListOption[];
582+
583+
beforeEach(() => {
584+
fixture = TestBed.createComponent(SelectionListWithFormControl);
585+
fixture.detectChanges();
586+
587+
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
588+
selectionList = selectionListDebug.componentInstance;
589+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
590+
.map(optionDebugEl => optionDebugEl.componentInstance);
591+
});
592+
593+
it('should be able to disable options from the control', () => {
594+
expect(listOptions.every(option => !option.disabled))
595+
.toBe(true, 'Expected every list option to be enabled.');
596+
597+
fixture.componentInstance.formControl.disable();
598+
fixture.detectChanges();
599+
600+
expect(listOptions.every(option => option.disabled))
601+
.toBe(true, 'Expected every list option to be disabled.');
602+
});
603+
604+
it('should be able to set the value through the form control', () => {
605+
expect(listOptions.every(option => !option.selected))
606+
.toBe(true, 'Expected every list option to be unselected.');
607+
608+
fixture.componentInstance.formControl.setValue(['opt2', 'opt3']);
609+
fixture.detectChanges();
610+
611+
expect(listOptions[1].selected).toBe(true, 'Expected option to be selected.');
612+
expect(listOptions[2].selected).toBe(true, 'Expected option to be selected.');
613+
614+
fixture.componentInstance.formControl.setValue(null);
615+
fixture.detectChanges();
616+
617+
expect(listOptions.every(option => !option.selected))
618+
.toBe(true, 'Expected every list option to be unselected.');
619+
});
620+
});
621+
});
622+
445623

446624
@Component({template: `
447-
<mat-selection-list id="selection-list-1">
625+
<mat-selection-list id="selection-list-1" (change)="onValueChange($event)">
448626
<mat-list-option checkboxPosition="before" disabled="true" value="inbox">
449627
Inbox (disabled selection-option)
450628
</mat-list-option>
@@ -461,20 +639,22 @@ describe('MatSelectionList', () => {
461639
</mat-selection-list>`})
462640
class SelectionListWithListOptions {
463641
showLastOption: boolean = true;
642+
643+
onValueChange(_change: MatSelectionListChange) {}
464644
}
465645

466646
@Component({template: `
467-
<mat-selection-list id = "selection-list-2">
468-
<mat-list-option checkboxPosition = "after">
647+
<mat-selection-list id="selection-list-2">
648+
<mat-list-option checkboxPosition="after">
469649
Inbox (disabled selection-option)
470650
</mat-list-option>
471-
<mat-list-option id = "testSelect" checkboxPosition = "after">
651+
<mat-list-option id="testSelect" checkboxPosition="after">
472652
Starred
473653
</mat-list-option>
474-
<mat-list-option checkboxPosition = "after">
654+
<mat-list-option checkboxPosition="after">
475655
Sent Mail
476656
</mat-list-option>
477-
<mat-list-option checkboxPosition = "after">
657+
<mat-list-option checkboxPosition="after">
478658
Drafts
479659
</mat-list-option>
480660
</mat-selection-list>`})
@@ -523,3 +703,28 @@ class SelectionListWithSelectedOption {
523703
</mat-selection-list>`})
524704
class SelectionListWithOnlyOneOption {
525705
}
706+
707+
@Component({
708+
template: `
709+
<mat-selection-list [(ngModel)]="selectedOptions">
710+
<mat-list-option value="opt1">Option 1</mat-list-option>
711+
<mat-list-option value="opt2">Option 2</mat-list-option>
712+
<mat-list-option value="opt3">Option 3</mat-list-option>
713+
</mat-selection-list>`
714+
})
715+
class SelectionListWithModel {
716+
selectedOptions: string[] = [];
717+
}
718+
719+
@Component({
720+
template: `
721+
<mat-selection-list [formControl]="formControl">
722+
<mat-list-option value="opt1">Option 1</mat-list-option>
723+
<mat-list-option value="opt2">Option 2</mat-list-option>
724+
<mat-list-option value="opt3">Option 3</mat-list-option>
725+
</mat-selection-list>
726+
`
727+
})
728+
class SelectionListWithFormControl {
729+
formControl = new FormControl();
730+
}

0 commit comments

Comments
 (0)