Skip to content

Commit fce9d58

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

File tree

4 files changed

+332
-125
lines changed

4 files changed

+332
-125
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
@@ -59,4 +59,13 @@ export class ListDemo {
5959

6060
thirdLine: boolean = false;
6161
infoClicked: boolean = false;
62+
63+
selectedOptions: string[] = ['apples'];
64+
changeEventCount: number = 0;
65+
modelChangeEventCount: number = 0;
66+
67+
onSelectedOptionsChange(values: string[]) {
68+
this.selectedOptions = values;
69+
this.modelChangeEventCount++;
70+
}
6271
}

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

Lines changed: 182 additions & 81 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, MatListOptionChange} 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 =
@@ -480,90 +502,167 @@ describe('MatSelectionList', () => {
480502
expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
481503
});
482504
});
505+
});
483506

507+
describe('MatSelectionList with forms', () => {
484508

485-
describe('with multiple values', () => {
486-
let fixture: ComponentFixture<SelectionListWithMultipleValues>;
487-
let listOption: DebugElement[];
488-
let listItemEl: DebugElement;
489-
let selectionList: DebugElement;
509+
beforeEach(async(() => {
510+
TestBed.configureTestingModule({
511+
imports: [MatListModule, FormsModule, ReactiveFormsModule],
512+
declarations: [
513+
SelectionListWithModel,
514+
SelectionListWithFormControl
515+
]
516+
});
490517

491-
beforeEach(async(() => {
492-
TestBed.configureTestingModule({
493-
imports: [MatListModule],
494-
declarations: [
495-
SelectionListWithMultipleValues
496-
],
497-
});
518+
TestBed.compileComponents();
519+
}));
498520

499-
TestBed.compileComponents();
521+
describe('and ngModel', () => {
522+
let fixture: ComponentFixture<SelectionListWithModel>;
523+
let selectionListDebug: DebugElement;
524+
let selectionList: MatSelectionList;
525+
let listOptions: MatListOption[];
526+
let ngModel: NgModel;
527+
528+
beforeEach(() => {
529+
fixture = TestBed.createComponent(SelectionListWithModel);
530+
fixture.detectChanges();
531+
532+
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
533+
selectionList = selectionListDebug.componentInstance;
534+
ngModel = selectionListDebug.injector.get<NgModel>(NgModel);
535+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
536+
.map(optionDebugEl => optionDebugEl.componentInstance);
537+
});
538+
539+
it('should update the model if an option got selected programmatically', fakeAsync(() => {
540+
expect(fixture.componentInstance.selectedOptions.length)
541+
.toBe(0, 'Expected no options to be selected by default');
542+
543+
listOptions[0].toggle();
544+
fixture.detectChanges();
545+
546+
tick();
547+
548+
expect(fixture.componentInstance.selectedOptions.length)
549+
.toBe(1, 'Expected first list option to be selected');
500550
}));
501551

502-
beforeEach(async(() => {
503-
fixture = TestBed.createComponent(SelectionListWithMultipleValues);
504-
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
505-
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
506-
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
552+
it('should update the model if an option got clicked', fakeAsync(() => {
553+
expect(fixture.componentInstance.selectedOptions.length)
554+
.toBe(0, 'Expected no options to be selected by default');
555+
556+
dispatchFakeEvent(listOptions[0]._getHostElement(), 'click');
507557
fixture.detectChanges();
558+
559+
tick();
560+
561+
expect(fixture.componentInstance.selectedOptions.length)
562+
.toBe(1, 'Expected first list option to be selected');
508563
}));
509564

510-
it('should have a value for each item', () => {
511-
expect(listOption[0].componentInstance.value).toBe(1);
512-
expect(listOption[1].componentInstance.value).toBe('a');
513-
expect(listOption[2].componentInstance.value).toBe(true);
514-
});
565+
it('should update the options if a model value is set', fakeAsync(() => {
566+
expect(fixture.componentInstance.selectedOptions.length)
567+
.toBe(0, 'Expected no options to be selected by default');
515568

516-
});
569+
fixture.componentInstance.selectedOptions = ['opt3'];
570+
fixture.detectChanges();
517571

518-
describe('with option selected events', () => {
519-
let fixture: ComponentFixture<SelectionListWithOptionEvents>;
520-
let testComponent: SelectionListWithOptionEvents;
521-
let listOption: DebugElement[];
522-
let selectionList: DebugElement;
572+
tick();
523573

524-
beforeEach(async(() => {
525-
TestBed.configureTestingModule({
526-
imports: [MatListModule],
527-
declarations: [
528-
SelectionListWithOptionEvents
529-
],
530-
});
574+
expect(fixture.componentInstance.selectedOptions.length)
575+
.toBe(1, 'Expected first list option to be selected');
576+
}));
531577

532-
TestBed.compileComponents();
578+
it('should set the selection-list to touched on blur', fakeAsync(() => {
579+
expect(ngModel.touched)
580+
.toBe(false, 'Expected the selection-list to be untouched by default.');
581+
582+
dispatchFakeEvent(selectionListDebug.nativeElement, 'blur');
583+
fixture.detectChanges();
584+
585+
tick();
586+
587+
expect(ngModel.touched).toBe(true, 'Expected the selection-list to be touched after blur');
533588
}));
534589

535-
beforeEach(async(() => {
536-
fixture = TestBed.createComponent(SelectionListWithOptionEvents);
537-
testComponent = fixture.debugElement.componentInstance;
538-
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
539-
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
590+
it('should be pristine by default', fakeAsync(() => {
591+
fixture = TestBed.createComponent(SelectionListWithModel);
592+
fixture.componentInstance.selectedOptions = ['opt2'];
540593
fixture.detectChanges();
594+
595+
ngModel =
596+
fixture.debugElement.query(By.directive(MatSelectionList)).injector.get<NgModel>(NgModel);
597+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
598+
.map(optionDebugEl => optionDebugEl.componentInstance);
599+
600+
// Flush the initial tick to ensure that every action from the ControlValueAccessor
601+
// happened before the actual test starts.
602+
tick();
603+
604+
expect(ngModel.pristine)
605+
.toBe(true, 'Expected the selection-list to be pristine by default.');
606+
607+
listOptions[1].toggle();
608+
fixture.detectChanges();
609+
610+
tick();
611+
612+
expect(ngModel.pristine)
613+
.toBe(false, 'Expected the selection-list to be dirty after state change.');
541614
}));
615+
});
616+
617+
describe('and formControl', () => {
618+
let fixture: ComponentFixture<SelectionListWithFormControl>;
619+
let selectionListDebug: DebugElement;
620+
let selectionList: MatSelectionList;
621+
let listOptions: MatListOption[];
542622

543-
it('should trigger the selected and deselected events when clicked in succession.', () => {
623+
beforeEach(() => {
624+
fixture = TestBed.createComponent(SelectionListWithFormControl);
625+
fixture.detectChanges();
544626

545-
let selected: boolean = false;
627+
selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList));
628+
selectionList = selectionListDebug.componentInstance;
629+
listOptions = fixture.debugElement.queryAll(By.directive(MatListOption))
630+
.map(optionDebugEl => optionDebugEl.componentInstance);
631+
});
546632

547-
spyOn(testComponent, 'onOptionSelectionChange')
548-
.and.callFake((event: MatListOptionChange) => {
549-
selected = event.selected;
550-
});
633+
it('should be able to disable options from the control', () => {
634+
expect(listOptions.every(option => !option.disabled))
635+
.toBe(true, 'Expected every list option to be enabled.');
551636

552-
listOption[0].nativeElement.click();
553-
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(1);
554-
expect(selected).toBe(true);
637+
fixture.componentInstance.formControl.disable();
638+
fixture.detectChanges();
555639

556-
listOption[0].nativeElement.click();
557-
expect(testComponent.onOptionSelectionChange).toHaveBeenCalledTimes(2);
558-
expect(selected).toBe(false);
640+
expect(listOptions.every(option => option.disabled))
641+
.toBe(true, 'Expected every list option to be disabled.');
559642
});
560643

561-
});
644+
it('should be able to set the value through the form control', () => {
645+
expect(listOptions.every(option => !option.selected))
646+
.toBe(true, 'Expected every list option to be unselected.');
647+
648+
fixture.componentInstance.formControl.setValue(['opt2', 'opt3']);
649+
fixture.detectChanges();
650+
651+
expect(listOptions[1].selected).toBe(true, 'Expected second option to be selected.');
652+
expect(listOptions[2].selected).toBe(true, 'Expected third option to be selected.');
653+
654+
fixture.componentInstance.formControl.setValue(null);
655+
fixture.detectChanges();
562656

657+
expect(listOptions.every(option => !option.selected))
658+
.toBe(true, 'Expected every list option to be unselected.');
659+
});
660+
});
563661
});
564662

663+
565664
@Component({template: `
566-
<mat-selection-list id="selection-list-1">
665+
<mat-selection-list id="selection-list-1" (change)="onValueChange($event)">
567666
<mat-list-option checkboxPosition="before" disabled="true" value="inbox">
568667
Inbox (disabled selection-option)
569668
</mat-list-option>
@@ -580,6 +679,8 @@ describe('MatSelectionList', () => {
580679
</mat-selection-list>`})
581680
class SelectionListWithListOptions {
582681
showLastOption: boolean = true;
682+
683+
onValueChange(_change: MatSelectionListChange) {}
583684
}
584685

585686
@Component({template: `
@@ -656,27 +757,27 @@ class SelectionListWithTabindexBinding {
656757
disabled: boolean;
657758
}
658759

659-
@Component({template: `
660-
<mat-selection-list id="selection-list-5">
661-
<mat-list-option [value]="1" checkboxPosition="after">
662-
1
663-
</mat-list-option>
664-
<mat-list-option value="a" checkboxPosition="after">
665-
a
666-
</mat-list-option>
667-
<mat-list-option [value]="true" checkboxPosition="after">
668-
true
669-
</mat-list-option>
670-
</mat-selection-list>`})
671-
class SelectionListWithMultipleValues {
760+
@Component({
761+
template: `
762+
<mat-selection-list [(ngModel)]="selectedOptions">
763+
<mat-list-option value="opt1">Option 1</mat-list-option>
764+
<mat-list-option value="opt2">Option 2</mat-list-option>
765+
<mat-list-option value="opt3">Option 3</mat-list-option>
766+
</mat-selection-list>`
767+
})
768+
class SelectionListWithModel {
769+
selectedOptions: string[] = [];
672770
}
673771

674-
@Component({template: `
675-
<mat-selection-list id="selection-list-6">
676-
<mat-list-option (selectionChange)="onOptionSelectionChange($event)">
677-
Inbox
678-
</mat-list-option>
679-
</mat-selection-list>`})
680-
class SelectionListWithOptionEvents {
681-
onOptionSelectionChange: (event?: MatListOptionChange) => void = () => {};
772+
@Component({
773+
template: `
774+
<mat-selection-list [formControl]="formControl">
775+
<mat-list-option value="opt1">Option 1</mat-list-option>
776+
<mat-list-option value="opt2">Option 2</mat-list-option>
777+
<mat-list-option value="opt3">Option 3</mat-list-option>
778+
</mat-selection-list>
779+
`
780+
})
781+
class SelectionListWithFormControl {
782+
formControl = new FormControl();
682783
}

0 commit comments

Comments
 (0)