From fae47d86b3638a69500c5635bb3a830e79be4768 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Wed, 26 Jul 2017 11:24:47 -0700 Subject: [PATCH] refactor(forms): move value accessor tests into own spec (#18356) PR Close #18356 --- .../forms/test/reactive_integration_spec.ts | 770 +--------- .../forms/test/template_integration_spec.ts | 518 +------ .../test/value_accessor_integration_spec.ts | 1336 +++++++++++++++++ 3 files changed, 1343 insertions(+), 1281 deletions(-) create mode 100644 packages/forms/test/value_accessor_integration_spec.ts diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 1dfd3c63d3..3e758b0aed 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -6,14 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, EventEmitter, Input, Output, Type, forwardRef} from '@angular/core'; +import {Component, Directive, Input, Type, forwardRef} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; -import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, ControlValueAccessor, FormArray, FormControl, FormGroup, FormGroupDirective, FormsModule, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule, Validators} from '@angular/forms'; +import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, FormArray, FormControl, FormGroup, FormGroupDirective, FormsModule, NG_ASYNC_VALIDATORS, NG_VALIDATORS, ReactiveFormsModule, Validators} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {timer} from 'rxjs/observable/timer'; import {_do} from 'rxjs/operator/do'; +import {MyInput, MyInputForm} from './value_accessor_integration_spec'; export function main() { describe('reactive forms integration tests', () => { @@ -75,7 +76,7 @@ export function main() { }); - describe('rebound form groups', () => { + describe('re-bound form groups', () => { it('should update DOM elements initially', () => { const fixture = initTest(FormGroupComp); @@ -110,24 +111,6 @@ export function main() { expect(input.nativeElement.value).toEqual('Carson'); }); - it('should work with radio buttons when reusing control', () => { - const fixture = initTest(FormControlRadioButtons); - const food = new FormControl('chicken'); - fixture.componentInstance.form = - new FormGroup({'food': food, 'drink': new FormControl('')}); - fixture.detectChanges(); - - const newForm = new FormGroup({'food': food, 'drink': new FormControl('')}); - fixture.componentInstance.form = newForm; - fixture.detectChanges(); - - newForm.setValue({food: 'fish', drink: ''}); - fixture.detectChanges(); - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toBe(false); - expect(inputs[1].nativeElement.checked).toBe(true); - }); - it('should update nested form group model when UI changes', () => { const fixture = initTest(NestedFormGroupComp); fixture.componentInstance.form = new FormGroup( @@ -340,95 +323,6 @@ export function main() { }); - describe('select controls', () => { - it(`should support primitive values`, () => { - const fixture = initTest(FormControlNameSelect); - fixture.detectChanges(); - - // model -> view - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('SF'); - expect(sfOption.nativeElement.selected).toBe(true); - - select.nativeElement.value = 'NY'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - - // view -> model - expect(sfOption.nativeElement.selected).toBe(false); - expect(fixture.componentInstance.form.value).toEqual({'city': 'NY'}); - }); - - it(`should support objects`, () => { - const fixture = initTest(FormControlSelectNgValue); - fixture.detectChanges(); - - // model -> view - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - }); - - it('should throw an error if compareWith is not a function', () => { - const fixture = initTest(FormControlSelectWithCompareFn); - fixture.componentInstance.compareFn = null !; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); - }); - - it('should compare options using provided compareWith function', () => { - const fixture = initTest(FormControlSelectWithCompareFn); - fixture.detectChanges(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - }); - }); - - describe('select multiple controls', () => { - it('should support primitive values', () => { - const fixture = initTest(FormControlSelectMultiple); - fixture.detectChanges(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual(`0: 'SF'`); - expect(sfOption.nativeElement.selected).toBe(true); - }); - - it('should support objects', () => { - const fixture = initTest(FormControlSelectMultipleNgValue); - fixture.detectChanges(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - }); - - it('should throw an error when compareWith is not a function', () => { - const fixture = initTest(FormControlSelectMultipleWithCompareFn); - fixture.componentInstance.compareFn = null !; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); - }); - - it('should compare options using provided compareWith function', fakeAsync(() => { - const fixture = initTest(FormControlSelectMultipleWithCompareFn); - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - })); - }); - describe('form arrays', () => { it('should support form arrays', () => { const fixture = initTest(FormArrayComp); @@ -837,482 +731,6 @@ export function main() { }); - describe('value accessors', () => { - - it('should support without type', () => { - TestBed.overrideComponent( - FormControlComp, {set: {template: ``}}); - const fixture = initTest(FormControlComp); - const control = new FormControl('old'); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - // model -> view - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toEqual('old'); - - input.nativeElement.value = 'new'; - dispatchEvent(input.nativeElement, 'input'); - - // view -> model - expect(control.value).toEqual('new'); - }); - - it('should support ', () => { - const fixture = initTest(FormGroupComp); - const form = new FormGroup({'login': new FormControl('old')}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - // model -> view - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toEqual('old'); - - input.nativeElement.value = 'new'; - dispatchEvent(input.nativeElement, 'input'); - - // view -> model - expect(form.value).toEqual({'login': 'new'}); - }); - - it('should ignore the change event for ', () => { - const fixture = initTest(FormGroupComp); - const form = new FormGroup({'login': new FormControl('oldValue')}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - const input = fixture.debugElement.query(By.css('input')); - form.valueChanges.subscribe({next: (value) => { throw 'Should not happen'; }}); - input.nativeElement.value = 'updatedValue'; - - dispatchEvent(input.nativeElement, 'change'); - }); - - it('should support `}}); - const fixture = initTest(FormControlComp); - const control = new FormControl('old'); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - // model -> view - const textarea = fixture.debugElement.query(By.css('textarea')); - expect(textarea.nativeElement.value).toEqual('old'); - - textarea.nativeElement.value = 'new'; - dispatchEvent(textarea.nativeElement, 'input'); - - // view -> model - expect(control.value).toEqual('new'); - }); - - it('should support ', () => { - TestBed.overrideComponent( - FormControlComp, {set: {template: ``}}); - const fixture = initTest(FormControlComp); - const control = new FormControl(true); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - // model -> view - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.checked).toBe(true); - - input.nativeElement.checked = false; - dispatchEvent(input.nativeElement, 'change'); - - // view -> model - expect(control.value).toBe(false); - }); - - describe('should support ', () => { - it('with basic use case', () => { - const fixture = initTest(FormControlNumberInput); - const control = new FormControl(10); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - // model -> view - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toEqual('10'); - - input.nativeElement.value = '20'; - dispatchEvent(input.nativeElement, 'input'); - - // view -> model - expect(control.value).toEqual(20); - }); - - it('when value is cleared in the UI', () => { - const fixture = initTest(FormControlNumberInput); - const control = new FormControl(10, Validators.required); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - const input = fixture.debugElement.query(By.css('input')); - input.nativeElement.value = ''; - dispatchEvent(input.nativeElement, 'input'); - - expect(control.valid).toBe(false); - expect(control.value).toEqual(null); - - input.nativeElement.value = '0'; - dispatchEvent(input.nativeElement, 'input'); - - expect(control.valid).toBe(true); - expect(control.value).toEqual(0); - }); - - it('when value is cleared programmatically', () => { - const fixture = initTest(FormControlNumberInput); - const control = new FormControl(10); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - control.setValue(null); - - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toEqual(''); - }); - }); - - describe('should support ', () => { - - it('should support basic functionality', () => { - const fixture = initTest(FormControlRadioButtons); - const form = - new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - // model -> view - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); - - dispatchEvent(inputs[0].nativeElement, 'change'); - fixture.detectChanges(); - - // view -> model - expect(form.get('food') !.value).toEqual('chicken'); - expect(inputs[1].nativeElement.checked).toEqual(false); - - form.get('food') !.setValue('fish'); - fixture.detectChanges(); - - // programmatic change -> view - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); - }); - - it('should support an initial undefined value', () => { - const fixture = initTest(FormControlRadioButtons); - const form = new FormGroup({'food': new FormControl(), 'drink': new FormControl()}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - }); - - it('should reset properly', () => { - const fixture = initTest(FormControlRadioButtons); - const form = - new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - form.reset(); - fixture.detectChanges(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - }); - - it('should set value to null and undefined properly', () => { - const fixture = initTest(FormControlRadioButtons); - const form = new FormGroup( - {'food': new FormControl('chicken'), 'drink': new FormControl('sprite')}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - form.get('food') !.setValue(null); - fixture.detectChanges(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - - form.get('food') !.setValue('chicken'); - fixture.detectChanges(); - - form.get('food') !.setValue(undefined); - fixture.detectChanges(); - expect(inputs[0].nativeElement.checked).toEqual(false); - }); - - it('should use formControlName to group radio buttons when name is absent', () => { - const fixture = initTest(FormControlRadioButtons); - const foodCtrl = new FormControl('fish'); - const drinkCtrl = new FormControl('sprite'); - fixture.componentInstance.form = new FormGroup({'food': foodCtrl, 'drink': drinkCtrl}); - fixture.detectChanges(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - - dispatchEvent(inputs[0].nativeElement, 'change'); - inputs[0].nativeElement.checked = true; - fixture.detectChanges(); - - const value = fixture.componentInstance.form.value; - expect(value.food).toEqual('chicken'); - expect(inputs[1].nativeElement.checked).toEqual(false); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - - drinkCtrl.setValue('cola'); - fixture.detectChanges(); - - expect(inputs[0].nativeElement.checked).toEqual(true); - expect(inputs[1].nativeElement.checked).toEqual(false); - expect(inputs[2].nativeElement.checked).toEqual(true); - expect(inputs[3].nativeElement.checked).toEqual(false); - }); - - it('should support removing controls from ', () => { - const fixture = initTest(FormControlRadioButtons); - const showRadio = new FormControl('yes'); - const form = - new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); - fixture.componentInstance.form = form; - fixture.componentInstance.showRadio = showRadio; - showRadio.valueChanges.subscribe((change) => { - (change === 'yes') ? form.addControl('food', new FormControl('fish')) : - form.removeControl('food'); - }); - fixture.detectChanges(); - - const input = fixture.debugElement.query(By.css('[value="no"]')); - dispatchEvent(input.nativeElement, 'change'); - - fixture.detectChanges(); - expect(form.value).toEqual({drink: 'sprite'}); - }); - - it('should differentiate controls on different levels with the same name', () => { - TestBed.overrideComponent(FormControlRadioButtons, { - set: { - template: ` -
- - -
- - -
-
- ` - } - }); - const fixture = initTest(FormControlRadioButtons); - const form = new FormGroup({ - food: new FormControl('fish'), - nested: new FormGroup({food: new FormControl('fish')}) - }); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - // model -> view - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - - dispatchEvent(inputs[0].nativeElement, 'change'); - fixture.detectChanges(); - - // view -> model - expect(form.get('food') !.value).toEqual('chicken'); - expect(form.get('nested.food') !.value).toEqual('fish'); - - expect(inputs[1].nativeElement.checked).toEqual(false); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - - }); - - it('should disable all radio buttons when disable() is called', () => { - const fixture = initTest(FormControlRadioButtons); - const form = - new FormGroup({food: new FormControl('fish'), drink: new FormControl('cola')}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.disabled).toEqual(false); - expect(inputs[1].nativeElement.disabled).toEqual(false); - expect(inputs[2].nativeElement.disabled).toEqual(false); - expect(inputs[3].nativeElement.disabled).toEqual(false); - - form.get('food') !.disable(); - expect(inputs[0].nativeElement.disabled).toEqual(true); - expect(inputs[1].nativeElement.disabled).toEqual(true); - expect(inputs[2].nativeElement.disabled).toEqual(false); - expect(inputs[3].nativeElement.disabled).toEqual(false); - - form.disable(); - expect(inputs[0].nativeElement.disabled).toEqual(true); - expect(inputs[1].nativeElement.disabled).toEqual(true); - expect(inputs[2].nativeElement.disabled).toEqual(true); - expect(inputs[3].nativeElement.disabled).toEqual(true); - - form.enable(); - expect(inputs[0].nativeElement.disabled).toEqual(false); - expect(inputs[1].nativeElement.disabled).toEqual(false); - expect(inputs[2].nativeElement.disabled).toEqual(false); - expect(inputs[3].nativeElement.disabled).toEqual(false); - }); - - it('should disable all radio buttons when initially disabled', () => { - const fixture = initTest(FormControlRadioButtons); - const form = new FormGroup({ - food: new FormControl({value: 'fish', disabled: true}), - drink: new FormControl('cola') - }); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.disabled).toEqual(true); - expect(inputs[1].nativeElement.disabled).toEqual(true); - expect(inputs[2].nativeElement.disabled).toEqual(false); - expect(inputs[3].nativeElement.disabled).toEqual(false); - }); - - }); - - describe('should support ', () => { - it('with basic use case', () => { - const fixture = initTest(FormControlRangeInput); - const control = new FormControl(10); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - // model -> view - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toEqual('10'); - - input.nativeElement.value = '20'; - dispatchEvent(input.nativeElement, 'input'); - - // view -> model - expect(control.value).toEqual(20); - }); - - it('when value is cleared in the UI', () => { - const fixture = initTest(FormControlNumberInput); - const control = new FormControl(10, Validators.required); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - const input = fixture.debugElement.query(By.css('input')); - input.nativeElement.value = ''; - dispatchEvent(input.nativeElement, 'input'); - - expect(control.valid).toBe(false); - expect(control.value).toEqual(null); - - input.nativeElement.value = '0'; - dispatchEvent(input.nativeElement, 'input'); - - expect(control.valid).toBe(true); - expect(control.value).toEqual(0); - }); - - it('when value is cleared programmatically', () => { - const fixture = initTest(FormControlNumberInput); - const control = new FormControl(10); - fixture.componentInstance.control = control; - fixture.detectChanges(); - - control.setValue(null); - - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toEqual(''); - }); - }); - - describe('custom value accessors', () => { - it('should support basic functionality', () => { - const fixture = initTest(WrappedValueForm, WrappedValue); - const form = new FormGroup({'login': new FormControl('aa')}); - fixture.componentInstance.form = form; - fixture.detectChanges(); - - // model -> view - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toEqual('!aa!'); - - input.nativeElement.value = '!bb!'; - dispatchEvent(input.nativeElement, 'input'); - - // view -> model - expect(form.value).toEqual({'login': 'bb'}); - - // custom validator - expect(form.get('login') !.errors).toEqual({'err': true}); - form.setValue({login: 'expected'}); - expect(form.get('login') !.errors).toEqual(null); - }); - - it('should support non builtin input elements that fire a change event without a \'target\' property', - () => { - const fixture = initTest(MyInputForm, MyInput); - fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')}); - fixture.detectChanges(); - - const input = fixture.debugElement.query(By.css('my-input')); - expect(input.componentInstance.value).toEqual('!aa!'); - - input.componentInstance.value = '!bb!'; - input.componentInstance.onInput.subscribe((value: any) => { - expect(fixture.componentInstance.form.value).toEqual({'login': 'bb'}); - }); - input.componentInstance.dispatchChangeEvent(); - }); - - it('should support custom accessors without setDisabledState - formControlName', () => { - const fixture = initTest(WrappedValueForm, WrappedValue); - fixture.componentInstance.form = new FormGroup({ - 'login': new FormControl({value: 'aa', disabled: true}), - }); - fixture.detectChanges(); - expect(fixture.componentInstance.form.status).toEqual('DISABLED'); - expect(fixture.componentInstance.form.get('login') !.status).toEqual('DISABLED'); - }); - - it('should support custom accessors without setDisabledState - formControlDirective', - () => { - TestBed.overrideComponent( - FormControlComp, - {set: {template: ``}}); - const fixture = initTest(FormControlComp); - fixture.componentInstance.control = new FormControl({value: 'aa', disabled: true}); - fixture.detectChanges(); - expect(fixture.componentInstance.control.status).toEqual('DISABLED'); - }); - - }); - - }); - describe('ngModel interactions', () => { it('should support ngModel for complex forms', fakeAsync(() => { @@ -1992,44 +1410,6 @@ export function main() { }); } -@Directive({ - selector: '[wrapped-value]', - host: {'(input)': 'handleOnInput($event.target.value)', '[value]': 'value'}, - providers: [ - {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: WrappedValue}, - {provide: NG_VALIDATORS, multi: true, useExisting: WrappedValue} - ] -}) -class WrappedValue implements ControlValueAccessor { - value: any; - onChange: Function; - - writeValue(value: any) { this.value = `!${value}!`; } - - registerOnChange(fn: (value: any) => void) { this.onChange = fn; } - registerOnTouched(fn: any) {} - - handleOnInput(value: any) { this.onChange(value.substring(1, value.length - 1)); } - - validate(c: AbstractControl) { return c.value === 'expected' ? null : {'err': true}; } -} - -@Component({selector: 'my-input', template: ''}) -class MyInput implements ControlValueAccessor { - @Output('input') onInput = new EventEmitter(); - value: string; - - constructor(cd: NgControl) { cd.valueAccessor = this; } - - writeValue(value: any) { this.value = `!${value}!`; } - - registerOnChange(fn: (value: any) => void) { this.onInput.subscribe({next: fn}); } - - registerOnTouched(fn: any) {} - - dispatchChangeEvent() { this.onInput.emit(this.value.substring(1, this.value.length - 1)); } -} - function uniqLoginAsyncValidator(expectedValue: string, timeout: number = 0) { return (c: AbstractControl) => { let resolve: (result: any) => void; @@ -2109,39 +1489,6 @@ class NestedFormGroupComp { form: FormGroup; } -@Component({ - selector: 'form-control-number-input', - template: `` -}) -class FormControlNumberInput { - control: FormControl; -} - -@Component({ - selector: 'form-control-range-input', - template: `` -}) -class FormControlRangeInput { - control: FormControl; -} - -@Component({ - selector: 'form-control-radio-buttons', - template: ` -
- - - - -
- - ` -}) -class FormControlRadioButtons { - form: FormGroup; - showRadio = new FormControl('yes'); -} - @Component({ selector: 'form-array-comp', template: ` @@ -2175,115 +1522,6 @@ class FormArrayNestedGroup { cityArray: FormArray; } -@Component({ - selector: 'form-control-name-select', - template: ` -
- -
` -}) -class FormControlNameSelect { - cities = ['SF', 'NY']; - form = new FormGroup({city: new FormControl('SF')}); -} - -@Component({ - selector: 'form-control-select-ngValue', - template: ` -
- -
` -}) -class FormControlSelectNgValue { - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; - form = new FormGroup({city: new FormControl(this.cities[0])}); -} - -@Component({ - selector: 'form-control-select-compare-with', - template: ` -
- -
` -}) -class FormControlSelectWithCompareFn { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; - form = new FormGroup({city: new FormControl({id: 1, name: 'SF'})}); -} - -@Component({ - selector: 'form-control-select-multiple', - template: ` -
- -
` -}) -class FormControlSelectMultiple { - cities = ['SF', 'NY']; - form = new FormGroup({city: new FormControl(['SF'])}); -} - -@Component({ - selector: 'form-control-select-multiple', - template: ` -
- -
` -}) -class FormControlSelectMultipleNgValue { - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; - form = new FormGroup({city: new FormControl([this.cities[0]])}); -} - -@Component({ - selector: 'form-control-select-multiple-compare-with', - template: ` -
- -
` -}) -class FormControlSelectMultipleWithCompareFn { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; - form = new FormGroup({city: new FormControl([{id: 1, name: 'SF'}])}); -} - -@Component({ - selector: 'wrapped-value-form', - template: ` -
- -
` -}) -class WrappedValueForm { - form: FormGroup; -} - -@Component({ - selector: 'my-input-form', - template: ` -
- -
` -}) -class MyInputForm { - form: FormGroup; -} @Component({ selector: 'form-group-ng-model', diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index 87fd77bdf8..6436350d85 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, Input, Type, forwardRef} from '@angular/core'; +import {Component, Directive, Type, forwardRef} from '@angular/core'; import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; -import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, ControlValueAccessor, FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, NgForm} from '@angular/forms'; +import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormsModule, NG_ASYNC_VALIDATORS, NgForm} from '@angular/forms'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; +import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integration_spec'; export function main() { describe('template-driven forms integration tests', () => { @@ -488,393 +489,6 @@ export function main() { expect(input.nativeElement.disabled).toEqual(false); })); - it('should disable radio controls properly with programmatic call', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'fish'; - fixture.detectChanges(); - tick(); - - const form = fixture.debugElement.children[0].injector.get(NgForm); - form.control.get('food') !.disable(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.disabled).toBe(true); - expect(inputs[1].nativeElement.disabled).toBe(true); - expect(inputs[2].nativeElement.disabled).toBe(false); - expect(inputs[3].nativeElement.disabled).toBe(false); - - form.control.disable(); - tick(); - - expect(inputs[0].nativeElement.disabled).toBe(true); - expect(inputs[1].nativeElement.disabled).toBe(true); - expect(inputs[2].nativeElement.disabled).toBe(true); - expect(inputs[3].nativeElement.disabled).toBe(true); - - form.control.enable(); - tick(); - - expect(inputs[0].nativeElement.disabled).toBe(false); - expect(inputs[1].nativeElement.disabled).toBe(false); - expect(inputs[2].nativeElement.disabled).toBe(false); - expect(inputs[3].nativeElement.disabled).toBe(false); - })); - - }); - - describe('range control', () => { - it('should support ', fakeAsync(() => { - const fixture = initTest(NgModelRangeForm); - // model -> view - fixture.componentInstance.val = 4; - fixture.detectChanges(); - tick(); - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toBe('4'); - fixture.detectChanges(); - tick(); - const newVal = '4'; - input.triggerEventHandler('input', {target: {value: newVal}}); - tick(); - // view -> model - fixture.detectChanges(); - expect(typeof(fixture.componentInstance.val)).toBe('number'); - })); - }); - - describe('radio controls', () => { - it('should support ', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'fish'; - fixture.detectChanges(); - tick(); - - // model -> view - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); - - dispatchEvent(inputs[0].nativeElement, 'change'); - tick(); - - // view -> model - expect(fixture.componentInstance.food).toEqual('chicken'); - expect(inputs[1].nativeElement.checked).toEqual(false); - })); - - it('should support multiple named groups', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'fish'; - fixture.componentInstance.drink = 'sprite'; - fixture.detectChanges(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - - dispatchEvent(inputs[0].nativeElement, 'change'); - tick(); - - expect(fixture.componentInstance.food).toEqual('chicken'); - expect(fixture.componentInstance.drink).toEqual('sprite'); - expect(inputs[1].nativeElement.checked).toEqual(false); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - })); - - it('should support initial undefined value', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.detectChanges(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(false); - })); - - it('should support resetting properly', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'chicken'; - fixture.detectChanges(); - tick(); - - const form = fixture.debugElement.query(By.css('form')); - dispatchEvent(form.nativeElement, 'reset'); - fixture.detectChanges(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - })); - - it('should support setting value to null and undefined', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'chicken'; - fixture.detectChanges(); - tick(); - - fixture.componentInstance.food = null !; - fixture.detectChanges(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - - fixture.componentInstance.food = 'chicken'; - fixture.detectChanges(); - tick(); - - fixture.componentInstance.food = undefined !; - fixture.detectChanges(); - tick(); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - })); - - }); - - describe('select controls', () => { - it('with option values that are objects', fakeAsync(() => { - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; - comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const nycOption = fixture.debugElement.queryAll(By.css('option'))[1]; - - // model -> view - expect(select.nativeElement.value).toEqual('1: Object'); - expect(nycOption.nativeElement.selected).toBe(true); - - select.nativeElement.value = '2: Object'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - tick(); - - // view -> model - expect(comp.selectedCity['name']).toEqual('Buffalo'); - })); - - it('when new options are added', fakeAsync(() => { - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; - comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - tick(); - - comp.cities.push({'name': 'Buffalo'}); - comp.selectedCity = comp.cities[2]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const buffalo = fixture.debugElement.queryAll(By.css('option'))[2]; - expect(select.nativeElement.value).toEqual('2: Object'); - expect(buffalo.nativeElement.selected).toBe(true); - })); - - it('when options are removed', fakeAsync(() => { - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; - comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - expect(select.nativeElement.value).toEqual('1: Object'); - - comp.cities.pop(); - fixture.detectChanges(); - tick(); - - expect(select.nativeElement.value).not.toEqual('1: Object'); - })); - - it('when option values have same content, but different identities', fakeAsync(() => { - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}]; - comp.selectedCity = comp.cities[0]; - fixture.detectChanges(); - - comp.selectedCity = comp.cities[2]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const secondNYC = fixture.debugElement.queryAll(By.css('option'))[2]; - expect(select.nativeElement.value).toEqual('2: Object'); - expect(secondNYC.nativeElement.selected).toBe(true); - })); - - it('should work with null option', fakeAsync(() => { - const fixture = initTest(NgModelSelectWithNullForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; - comp.selectedCity = null !; - fixture.detectChanges(); - - const select = fixture.debugElement.query(By.css('select')); - - select.nativeElement.value = '2: Object'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - tick(); - expect(comp.selectedCity['name']).toEqual('NYC'); - - select.nativeElement.value = '0: null'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - tick(); - expect(comp.selectedCity).toEqual(null); - })); - - it('should throw an error when compareWith is not a function', () => { - const fixture = initTest(NgModelSelectWithCustomCompareFnForm); - const comp = fixture.componentInstance; - comp.compareFn = null !; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); - }); - - it('should compare options using provided compareWith function', fakeAsync(() => { - const fixture = initTest(NgModelSelectWithCustomCompareFnForm); - const comp = fixture.componentInstance; - comp.selectedCity = {id: 1, name: 'SF'}; - comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - })); - }); - - describe('select multiple controls', () => { - describe('select options', () => { - let fixture: ComponentFixture; - let comp: NgModelSelectMultipleForm; - - beforeEach(() => { - fixture = initTest(NgModelSelectMultipleForm); - comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; - }); - - const detectChangesAndTick = (): void => { - fixture.detectChanges(); - tick(); - }; - - const setSelectedCities = (selectedCities: any): void => { - comp.selectedCities = selectedCities; - detectChangesAndTick(); - }; - - const selectOptionViaUI = (valueString: string): void => { - const select = fixture.debugElement.query(By.css('select')); - select.nativeElement.value = valueString; - dispatchEvent(select.nativeElement, 'change'); - detectChangesAndTick(); - }; - - const assertOptionElementSelectedState = (selectedStates: boolean[]): void => { - const options = fixture.debugElement.queryAll(By.css('option')); - if (options.length !== selectedStates.length) { - throw 'the selected state values to assert does not match the number of options'; - } - for (let i = 0; i < selectedStates.length; i++) { - expect(options[i].nativeElement.selected).toBe(selectedStates[i]); - } - }; - - it('should reflect state of model after option selected and new options subsequently added', - fakeAsync(() => { - setSelectedCities([]); - - selectOptionViaUI('1: Object'); - assertOptionElementSelectedState([false, true, false]); - - comp.cities.push({'name': 'Chicago'}); - detectChangesAndTick(); - - assertOptionElementSelectedState([false, true, false, false]); - })); - - it('should reflect state of model after option selected and then other options removed', - fakeAsync(() => { - setSelectedCities([]); - - selectOptionViaUI('1: Object'); - assertOptionElementSelectedState([false, true, false]); - - comp.cities.pop(); - detectChangesAndTick(); - - assertOptionElementSelectedState([false, true]); - })); - }); - - it('should throw an error when compareWith is not a function', () => { - const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); - const comp = fixture.componentInstance; - comp.compareFn = null !; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); - }); - - it('should compare options using provided compareWith function', fakeAsync(() => { - const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); - const comp = fixture.componentInstance; - comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}]; - comp.selectedCities = [comp.cities[0]]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - })); - }); - - describe('custom value accessors', () => { - it('should support standard writing to view and model', async(() => { - const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp); - fixture.componentInstance.name = 'Nancy'; - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - fixture.whenStable().then(() => { - // model -> view - const customInput = fixture.debugElement.query(By.css('[name="custom"]')); - expect(customInput.nativeElement.value).toEqual('Nancy'); - - customInput.nativeElement.value = 'Carson'; - dispatchEvent(customInput.nativeElement, 'input'); - fixture.detectChanges(); - - // view -> model - expect(fixture.componentInstance.name).toEqual('Carson'); - }); - }); - })); - }); describe('validation directives', () => { @@ -1381,132 +995,6 @@ class NgModelOptionsStandalone { two: string; } -@Component({selector: 'ng-model-range-form', template: ''}) -class NgModelRangeForm { - val: any; -} - -@Component({ - selector: 'ng-model-radio-form', - template: ` -
- - - - - -
- ` -}) -class NgModelRadioForm { - food: string; - drink: string; -} - -@Component({ - selector: 'ng-model-select-form', - template: ` - - ` -}) -class NgModelSelectForm { - selectedCity: {[k: string]: string} = {}; - cities: any[] = []; -} - -@Component({ - selector: 'ng-model-select-null-form', - template: ` - - ` -}) -class NgModelSelectWithNullForm { - selectedCity: {[k: string]: string} = {}; - cities: any[] = []; -} - -@Component({ - selector: 'ng-model-select-compare-with', - template: ` - - ` -}) -class NgModelSelectWithCustomCompareFnForm { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; - selectedCity: any = {}; - cities: any[] = []; -} - -@Component({ - selector: 'ng-model-select-multiple-compare-with', - template: ` - - ` -}) -class NgModelSelectMultipleWithCustomCompareFnForm { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; - selectedCities: any[] = []; - cities: any[] = []; -} - -@Component({ - selector: 'ng-model-select-multiple-form', - template: ` - - ` -}) -class NgModelSelectMultipleForm { - selectedCities: any[]; - cities: any[] = []; -} - -@Component({ - selector: 'ng-model-custom-comp', - template: ` - - `, - providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NgModelCustomComp}] -}) -class NgModelCustomComp implements ControlValueAccessor { - model: string; - @Input('disabled') isDisabled: boolean = false; - changeFn: (value: any) => void; - - writeValue(value: any) { this.model = value; } - - registerOnChange(fn: (value: any) => void) { this.changeFn = fn; } - - registerOnTouched() {} - - setDisabledState(isDisabled: boolean) { this.isDisabled = isDisabled; } -} - -@Component({ - selector: 'ng-model-custom-wrapper', - template: ` -
- -
- ` -}) -class NgModelCustomWrapper { - name: string; - isDisabled = false; -} - @Component({ selector: 'ng-model-validation-bindings', template: ` diff --git a/packages/forms/test/value_accessor_integration_spec.ts b/packages/forms/test/value_accessor_integration_spec.ts new file mode 100644 index 0000000000..e8c2c5cad6 --- /dev/null +++ b/packages/forms/test/value_accessor_integration_spec.ts @@ -0,0 +1,1336 @@ +import {Component, Directive, EventEmitter, Input, Output, Type} from '@angular/core'; +import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; +import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, NgForm, ReactiveFormsModule, Validators} from '@angular/forms'; +import {By} from '@angular/platform-browser/src/dom/debug/by'; +import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; + +export function main() { + describe('value accessors', () => { + + function initTest(component: Type, ...directives: Type[]): ComponentFixture { + TestBed.configureTestingModule( + {declarations: [component, ...directives], imports: [FormsModule, ReactiveFormsModule]}); + return TestBed.createComponent(component); + } + + it('should support without type', () => { + TestBed.overrideComponent( + FormControlComp, {set: {template: ``}}); + const fixture = initTest(FormControlComp); + const control = new FormControl('old'); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + // model -> view + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual('old'); + + input.nativeElement.value = 'new'; + dispatchEvent(input.nativeElement, 'input'); + + // view -> model + expect(control.value).toEqual('new'); + }); + + it('should support ', () => { + const fixture = initTest(FormGroupComp); + const form = new FormGroup({'login': new FormControl('old')}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + // model -> view + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual('old'); + + input.nativeElement.value = 'new'; + dispatchEvent(input.nativeElement, 'input'); + + // view -> model + expect(form.value).toEqual({'login': 'new'}); + }); + + it('should ignore the change event for ', () => { + const fixture = initTest(FormGroupComp); + const form = new FormGroup({'login': new FormControl('oldValue')}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + form.valueChanges.subscribe({next: (value) => { throw 'Should not happen'; }}); + input.nativeElement.value = 'updatedValue'; + + dispatchEvent(input.nativeElement, 'change'); + }); + + it('should support `}}); + const fixture = initTest(FormControlComp); + const control = new FormControl('old'); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + // model -> view + const textarea = fixture.debugElement.query(By.css('textarea')); + expect(textarea.nativeElement.value).toEqual('old'); + + textarea.nativeElement.value = 'new'; + dispatchEvent(textarea.nativeElement, 'input'); + + // view -> model + expect(control.value).toEqual('new'); + }); + + it('should support ', () => { + TestBed.overrideComponent( + FormControlComp, {set: {template: ``}}); + const fixture = initTest(FormControlComp); + const control = new FormControl(true); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + // model -> view + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.checked).toBe(true); + + input.nativeElement.checked = false; + dispatchEvent(input.nativeElement, 'change'); + + // view -> model + expect(control.value).toBe(false); + }); + + describe('should support ', () => { + it('with basic use case', () => { + const fixture = initTest(FormControlNumberInput); + const control = new FormControl(10); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + // model -> view + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual('10'); + + input.nativeElement.value = '20'; + dispatchEvent(input.nativeElement, 'input'); + + // view -> model + expect(control.value).toEqual(20); + }); + + it('when value is cleared in the UI', () => { + const fixture = initTest(FormControlNumberInput); + const control = new FormControl(10, Validators.required); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + input.nativeElement.value = ''; + dispatchEvent(input.nativeElement, 'input'); + + expect(control.valid).toBe(false); + expect(control.value).toEqual(null); + + input.nativeElement.value = '0'; + dispatchEvent(input.nativeElement, 'input'); + + expect(control.valid).toBe(true); + expect(control.value).toEqual(0); + }); + + it('when value is cleared programmatically', () => { + const fixture = initTest(FormControlNumberInput); + const control = new FormControl(10); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + control.setValue(null); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual(''); + }); + }); + + describe('select controls', () => { + + describe('in reactive forms', () => { + + it(`should support primitive values`, () => { + const fixture = initTest(FormControlNameSelect); + fixture.detectChanges(); + + // model -> view + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('SF'); + expect(sfOption.nativeElement.selected).toBe(true); + + select.nativeElement.value = 'NY'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + + // view -> model + expect(sfOption.nativeElement.selected).toBe(false); + expect(fixture.componentInstance.form.value).toEqual({'city': 'NY'}); + }); + + it(`should support objects`, () => { + const fixture = initTest(FormControlSelectNgValue); + fixture.detectChanges(); + + // model -> view + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + }); + + it('should throw an error if compareWith is not a function', () => { + const fixture = initTest(FormControlSelectWithCompareFn); + fixture.componentInstance.compareFn = null !; + expect(() => fixture.detectChanges()) + .toThrowError(/compareWith must be a function, but received null/); + }); + + it('should compare options using provided compareWith function', () => { + const fixture = initTest(FormControlSelectWithCompareFn); + fixture.detectChanges(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + }); + + }); + + describe('in template-driven forms', () => { + it('with option values that are objects', fakeAsync(() => { + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; + comp.selectedCity = comp.cities[1]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const nycOption = fixture.debugElement.queryAll(By.css('option'))[1]; + + // model -> view + expect(select.nativeElement.value).toEqual('1: Object'); + expect(nycOption.nativeElement.selected).toBe(true); + + select.nativeElement.value = '2: Object'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + + // view -> model + expect(comp.selectedCity['name']).toEqual('Buffalo'); + })); + + it('when new options are added', fakeAsync(() => { + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; + comp.selectedCity = comp.cities[1]; + fixture.detectChanges(); + tick(); + + comp.cities.push({'name': 'Buffalo'}); + comp.selectedCity = comp.cities[2]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const buffalo = fixture.debugElement.queryAll(By.css('option'))[2]; + expect(select.nativeElement.value).toEqual('2: Object'); + expect(buffalo.nativeElement.selected).toBe(true); + })); + + it('when options are removed', fakeAsync(() => { + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; + comp.selectedCity = comp.cities[1]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + expect(select.nativeElement.value).toEqual('1: Object'); + + comp.cities.pop(); + fixture.detectChanges(); + tick(); + + expect(select.nativeElement.value).not.toEqual('1: Object'); + })); + + it('when option values have same content, but different identities', fakeAsync(() => { + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}]; + comp.selectedCity = comp.cities[0]; + fixture.detectChanges(); + + comp.selectedCity = comp.cities[2]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const secondNYC = fixture.debugElement.queryAll(By.css('option'))[2]; + expect(select.nativeElement.value).toEqual('2: Object'); + expect(secondNYC.nativeElement.selected).toBe(true); + })); + + it('should work with null option', fakeAsync(() => { + const fixture = initTest(NgModelSelectWithNullForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; + comp.selectedCity = null !; + fixture.detectChanges(); + + const select = fixture.debugElement.query(By.css('select')); + + select.nativeElement.value = '2: Object'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + expect(comp.selectedCity['name']).toEqual('NYC'); + + select.nativeElement.value = '0: null'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + expect(comp.selectedCity).toEqual(null); + })); + + it('should throw an error when compareWith is not a function', () => { + const fixture = initTest(NgModelSelectWithCustomCompareFnForm); + const comp = fixture.componentInstance; + comp.compareFn = null !; + expect(() => fixture.detectChanges()) + .toThrowError(/compareWith must be a function, but received null/); + }); + + it('should compare options using provided compareWith function', fakeAsync(() => { + const fixture = initTest(NgModelSelectWithCustomCompareFnForm); + const comp = fixture.componentInstance; + comp.selectedCity = {id: 1, name: 'SF'}; + comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + })); + + }); + + }); + + describe('select multiple controls', () => { + + describe('in reactive forms', () => { + + it('should support primitive values', () => { + const fixture = initTest(FormControlSelectMultiple); + fixture.detectChanges(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual(`0: 'SF'`); + expect(sfOption.nativeElement.selected).toBe(true); + }); + + it('should support objects', () => { + const fixture = initTest(FormControlSelectMultipleNgValue); + fixture.detectChanges(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + }); + + it('should throw an error when compareWith is not a function', () => { + const fixture = initTest(FormControlSelectMultipleWithCompareFn); + fixture.componentInstance.compareFn = null !; + expect(() => fixture.detectChanges()) + .toThrowError(/compareWith must be a function, but received null/); + }); + + it('should compare options using provided compareWith function', fakeAsync(() => { + const fixture = initTest(FormControlSelectMultipleWithCompareFn); + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + })); + + }); + + describe('in template-driven forms', () => { + let fixture: ComponentFixture; + let comp: NgModelSelectMultipleForm; + + beforeEach(() => { + fixture = initTest(NgModelSelectMultipleForm); + comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; + }); + + const detectChangesAndTick = (): void => { + fixture.detectChanges(); + tick(); + }; + + const setSelectedCities = (selectedCities: any): void => { + comp.selectedCities = selectedCities; + detectChangesAndTick(); + }; + + const selectOptionViaUI = (valueString: string): void => { + const select = fixture.debugElement.query(By.css('select')); + select.nativeElement.value = valueString; + dispatchEvent(select.nativeElement, 'change'); + detectChangesAndTick(); + }; + + const assertOptionElementSelectedState = (selectedStates: boolean[]): void => { + const options = fixture.debugElement.queryAll(By.css('option')); + if (options.length !== selectedStates.length) { + throw 'the selected state values to assert does not match the number of options'; + } + for (let i = 0; i < selectedStates.length; i++) { + expect(options[i].nativeElement.selected).toBe(selectedStates[i]); + } + }; + + it('should reflect state of model after option selected and new options subsequently added', + fakeAsync(() => { + setSelectedCities([]); + + selectOptionViaUI('1: Object'); + assertOptionElementSelectedState([false, true, false]); + + comp.cities.push({'name': 'Chicago'}); + detectChangesAndTick(); + + assertOptionElementSelectedState([false, true, false, false]); + })); + + it('should reflect state of model after option selected and then other options removed', + fakeAsync(() => { + setSelectedCities([]); + + selectOptionViaUI('1: Object'); + assertOptionElementSelectedState([false, true, false]); + + comp.cities.pop(); + detectChangesAndTick(); + + assertOptionElementSelectedState([false, true]); + })); + }); + + it('should throw an error when compareWith is not a function', () => { + const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); + const comp = fixture.componentInstance; + comp.compareFn = null !; + expect(() => fixture.detectChanges()) + .toThrowError(/compareWith must be a function, but received null/); + }); + + it('should compare options using provided compareWith function', fakeAsync(() => { + const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); + const comp = fixture.componentInstance; + comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}]; + comp.selectedCities = [comp.cities[0]]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + })); + + }); + + describe('should support ', () => { + + describe('in reactive forms', () => { + + it('should support basic functionality', () => { + const fixture = initTest(FormControlRadioButtons); + const form = + new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + // model -> view + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + + dispatchEvent(inputs[0].nativeElement, 'change'); + fixture.detectChanges(); + + // view -> model + expect(form.get('food') !.value).toEqual('chicken'); + expect(inputs[1].nativeElement.checked).toEqual(false); + + form.get('food') !.setValue('fish'); + fixture.detectChanges(); + + // programmatic change -> view + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + }); + + it('should support an initial undefined value', () => { + const fixture = initTest(FormControlRadioButtons); + const form = new FormGroup({'food': new FormControl(), 'drink': new FormControl()}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + }); + + it('should reset properly', () => { + const fixture = initTest(FormControlRadioButtons); + const form = + new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + form.reset(); + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + }); + + it('should properly set value to null and undefined', () => { + const fixture = initTest(FormControlRadioButtons); + const form = new FormGroup( + {'food': new FormControl('chicken'), 'drink': new FormControl('sprite')}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + form.get('food') !.setValue(null); + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + + form.get('food') !.setValue('chicken'); + fixture.detectChanges(); + + form.get('food') !.setValue(undefined); + fixture.detectChanges(); + expect(inputs[0].nativeElement.checked).toEqual(false); + }); + + it('should use formControlName to group radio buttons when name is absent', () => { + const fixture = initTest(FormControlRadioButtons); + const foodCtrl = new FormControl('fish'); + const drinkCtrl = new FormControl('sprite'); + fixture.componentInstance.form = new FormGroup({'food': foodCtrl, 'drink': drinkCtrl}); + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + dispatchEvent(inputs[0].nativeElement, 'change'); + inputs[0].nativeElement.checked = true; + fixture.detectChanges(); + + const value = fixture.componentInstance.form.value; + expect(value.food).toEqual('chicken'); + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + drinkCtrl.setValue('cola'); + fixture.detectChanges(); + + expect(inputs[0].nativeElement.checked).toEqual(true); + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(true); + expect(inputs[3].nativeElement.checked).toEqual(false); + }); + + it('should support removing controls from ', () => { + const fixture = initTest(FormControlRadioButtons); + const showRadio = new FormControl('yes'); + const form = + new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); + fixture.componentInstance.form = form; + fixture.componentInstance.showRadio = showRadio; + showRadio.valueChanges.subscribe((change) => { + (change === 'yes') ? form.addControl('food', new FormControl('fish')) : + form.removeControl('food'); + }); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('[value="no"]')); + dispatchEvent(input.nativeElement, 'change'); + + fixture.detectChanges(); + expect(form.value).toEqual({drink: 'sprite'}); + }); + + it('should differentiate controls on different levels with the same name', () => { + TestBed.overrideComponent(FormControlRadioButtons, { + set: { + template: ` +
+ + +
+ + +
+
+ ` + } + }); + const fixture = initTest(FormControlRadioButtons); + const form = new FormGroup({ + food: new FormControl('fish'), + nested: new FormGroup({food: new FormControl('fish')}) + }); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + // model -> view + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + dispatchEvent(inputs[0].nativeElement, 'change'); + fixture.detectChanges(); + + // view -> model + expect(form.get('food') !.value).toEqual('chicken'); + expect(form.get('nested.food') !.value).toEqual('fish'); + + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + }); + + it('should disable all radio buttons when disable() is called', () => { + const fixture = initTest(FormControlRadioButtons); + const form = + new FormGroup({food: new FormControl('fish'), drink: new FormControl('cola')}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.disabled).toEqual(false); + expect(inputs[1].nativeElement.disabled).toEqual(false); + expect(inputs[2].nativeElement.disabled).toEqual(false); + expect(inputs[3].nativeElement.disabled).toEqual(false); + + form.get('food') !.disable(); + expect(inputs[0].nativeElement.disabled).toEqual(true); + expect(inputs[1].nativeElement.disabled).toEqual(true); + expect(inputs[2].nativeElement.disabled).toEqual(false); + expect(inputs[3].nativeElement.disabled).toEqual(false); + + form.disable(); + expect(inputs[0].nativeElement.disabled).toEqual(true); + expect(inputs[1].nativeElement.disabled).toEqual(true); + expect(inputs[2].nativeElement.disabled).toEqual(true); + expect(inputs[3].nativeElement.disabled).toEqual(true); + + form.enable(); + expect(inputs[0].nativeElement.disabled).toEqual(false); + expect(inputs[1].nativeElement.disabled).toEqual(false); + expect(inputs[2].nativeElement.disabled).toEqual(false); + expect(inputs[3].nativeElement.disabled).toEqual(false); + }); + + it('should disable all radio buttons when initially disabled', () => { + const fixture = initTest(FormControlRadioButtons); + const form = new FormGroup({ + food: new FormControl({value: 'fish', disabled: true}), + drink: new FormControl('cola') + }); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.disabled).toEqual(true); + expect(inputs[1].nativeElement.disabled).toEqual(true); + expect(inputs[2].nativeElement.disabled).toEqual(false); + expect(inputs[3].nativeElement.disabled).toEqual(false); + }); + + it('should work with reusing controls', () => { + const fixture = initTest(FormControlRadioButtons); + const food = new FormControl('chicken'); + fixture.componentInstance.form = + new FormGroup({'food': food, 'drink': new FormControl('')}); + fixture.detectChanges(); + + const newForm = new FormGroup({'food': food, 'drink': new FormControl('')}); + fixture.componentInstance.form = newForm; + fixture.detectChanges(); + + newForm.setValue({food: 'fish', drink: ''}); + fixture.detectChanges(); + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toBe(false); + expect(inputs[1].nativeElement.checked).toBe(true); + }); + + }); + + describe('in template-driven forms', () => { + it('should support basic functionality', fakeAsync(() => { + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'fish'; + fixture.detectChanges(); + tick(); + + // model -> view + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + + dispatchEvent(inputs[0].nativeElement, 'change'); + tick(); + + // view -> model + expect(fixture.componentInstance.food).toEqual('chicken'); + expect(inputs[1].nativeElement.checked).toEqual(false); + })); + + it('should support multiple named groups', fakeAsync(() => { + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'fish'; + fixture.componentInstance.drink = 'sprite'; + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + dispatchEvent(inputs[0].nativeElement, 'change'); + tick(); + + expect(fixture.componentInstance.food).toEqual('chicken'); + expect(fixture.componentInstance.drink).toEqual('sprite'); + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + })); + + it('should support initial undefined value', fakeAsync(() => { + const fixture = initTest(NgModelRadioForm); + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(false); + })); + + it('should support resetting properly', fakeAsync(() => { + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'chicken'; + fixture.detectChanges(); + tick(); + + const form = fixture.debugElement.query(By.css('form')); + dispatchEvent(form.nativeElement, 'reset'); + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + })); + + it('should support setting value to null and undefined', fakeAsync(() => { + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'chicken'; + fixture.detectChanges(); + tick(); + + fixture.componentInstance.food = null !; + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + + fixture.componentInstance.food = 'chicken'; + fixture.detectChanges(); + tick(); + + fixture.componentInstance.food = undefined !; + fixture.detectChanges(); + tick(); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + })); + + it('should disable radio controls properly with programmatic call', fakeAsync(() => { + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'fish'; + fixture.detectChanges(); + tick(); + + const form = fixture.debugElement.children[0].injector.get(NgForm); + form.control.get('food') !.disable(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.disabled).toBe(true); + expect(inputs[1].nativeElement.disabled).toBe(true); + expect(inputs[2].nativeElement.disabled).toBe(false); + expect(inputs[3].nativeElement.disabled).toBe(false); + + form.control.disable(); + tick(); + + expect(inputs[0].nativeElement.disabled).toBe(true); + expect(inputs[1].nativeElement.disabled).toBe(true); + expect(inputs[2].nativeElement.disabled).toBe(true); + expect(inputs[3].nativeElement.disabled).toBe(true); + + form.control.enable(); + tick(); + + expect(inputs[0].nativeElement.disabled).toBe(false); + expect(inputs[1].nativeElement.disabled).toBe(false); + expect(inputs[2].nativeElement.disabled).toBe(false); + expect(inputs[3].nativeElement.disabled).toBe(false); + })); + + }); + + }); + + describe('should support ', () => { + + describe('in reactive forms', () => { + + it('with basic use case', () => { + const fixture = initTest(FormControlRangeInput); + const control = new FormControl(10); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + // model -> view + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual('10'); + + input.nativeElement.value = '20'; + dispatchEvent(input.nativeElement, 'input'); + + // view -> model + expect(control.value).toEqual(20); + }); + + it('when value is cleared in the UI', () => { + const fixture = initTest(FormControlNumberInput); + const control = new FormControl(10, Validators.required); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + input.nativeElement.value = ''; + dispatchEvent(input.nativeElement, 'input'); + + expect(control.valid).toBe(false); + expect(control.value).toEqual(null); + + input.nativeElement.value = '0'; + dispatchEvent(input.nativeElement, 'input'); + + expect(control.valid).toBe(true); + expect(control.value).toEqual(0); + }); + + it('when value is cleared programmatically', () => { + const fixture = initTest(FormControlNumberInput); + const control = new FormControl(10); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + control.setValue(null); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual(''); + }); + + }); + + describe('in template-driven forms', () => { + it('with basic use case', fakeAsync(() => { + const fixture = initTest(NgModelRangeForm); + // model -> view + fixture.componentInstance.val = 4; + fixture.detectChanges(); + tick(); + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toBe('4'); + fixture.detectChanges(); + tick(); + const newVal = '4'; + input.triggerEventHandler('input', {target: {value: newVal}}); + tick(); + // view -> model + fixture.detectChanges(); + expect(typeof(fixture.componentInstance.val)).toBe('number'); + })); + + }); + + }); + + describe('custom value accessors', () => { + + describe('in reactive forms', () => { + it('should support basic functionality', () => { + const fixture = initTest(WrappedValueForm, WrappedValue); + const form = new FormGroup({'login': new FormControl('aa')}); + fixture.componentInstance.form = form; + fixture.detectChanges(); + + // model -> view + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual('!aa!'); + + input.nativeElement.value = '!bb!'; + dispatchEvent(input.nativeElement, 'input'); + + // view -> model + expect(form.value).toEqual({'login': 'bb'}); + + // custom validator + expect(form.get('login') !.errors).toEqual({'err': true}); + form.setValue({login: 'expected'}); + expect(form.get('login') !.errors).toEqual(null); + }); + + it('should support non builtin input elements that fire a change event without a \'target\' property', + () => { + const fixture = initTest(MyInputForm, MyInput); + fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')}); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('my-input')); + expect(input.componentInstance.value).toEqual('!aa!'); + + input.componentInstance.value = '!bb!'; + input.componentInstance.onInput.subscribe((value: any) => { + expect(fixture.componentInstance.form.value).toEqual({'login': 'bb'}); + }); + input.componentInstance.dispatchChangeEvent(); + }); + + it('should support custom accessors without setDisabledState - formControlName', () => { + const fixture = initTest(WrappedValueForm, WrappedValue); + fixture.componentInstance.form = new FormGroup({ + 'login': new FormControl({value: 'aa', disabled: true}), + }); + fixture.detectChanges(); + expect(fixture.componentInstance.form.status).toEqual('DISABLED'); + expect(fixture.componentInstance.form.get('login') !.status).toEqual('DISABLED'); + }); + + it('should support custom accessors without setDisabledState - formControlDirective', + () => { + TestBed.overrideComponent( + FormControlComp, + {set: {template: ``}}); + const fixture = initTest(FormControlComp); + fixture.componentInstance.control = new FormControl({value: 'aa', disabled: true}); + fixture.detectChanges(); + expect(fixture.componentInstance.control.status).toEqual('DISABLED'); + }); + }); + + describe('in template-driven forms', () => { + it('should support standard writing to view and model', async(() => { + const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp); + fixture.componentInstance.name = 'Nancy'; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + // model -> view + const customInput = fixture.debugElement.query(By.css('[name="custom"]')); + expect(customInput.nativeElement.value).toEqual('Nancy'); + + customInput.nativeElement.value = 'Carson'; + dispatchEvent(customInput.nativeElement, 'input'); + fixture.detectChanges(); + + // view -> model + expect(fixture.componentInstance.name).toEqual('Carson'); + }); + }); + })); + + }); + + }); + + }); +} + +@Component({selector: 'form-control-comp', template: ``}) +export class FormControlComp { + control: FormControl; +} + +@Component({ + selector: 'form-group-comp', + template: ` +
+ +
` +}) +export class FormGroupComp { + control: FormControl; + form: FormGroup; + myGroup: FormGroup; + event: Event; +} + +@Component({ + selector: 'form-control-number-input', + template: `` +}) +class FormControlNumberInput { + control: FormControl; +} + +@Component({ + selector: 'form-control-name-select', + template: ` +
+ +
` +}) +class FormControlNameSelect { + cities = ['SF', 'NY']; + form = new FormGroup({city: new FormControl('SF')}); +} + +@Component({ + selector: 'form-control-select-ngValue', + template: ` +
+ +
` +}) +class FormControlSelectNgValue { + cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + form = new FormGroup({city: new FormControl(this.cities[0])}); +} + +@Component({ + selector: 'form-control-select-compare-with', + template: ` +
+ +
` +}) +class FormControlSelectWithCompareFn { + compareFn: + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; + cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + form = new FormGroup({city: new FormControl({id: 1, name: 'SF'})}); +} + +@Component({ + selector: 'form-control-select-multiple', + template: ` +
+ +
` +}) +class FormControlSelectMultiple { + cities = ['SF', 'NY']; + form = new FormGroup({city: new FormControl(['SF'])}); +} + +@Component({ + selector: 'form-control-select-multiple', + template: ` +
+ +
` +}) +class FormControlSelectMultipleNgValue { + cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + form = new FormGroup({city: new FormControl([this.cities[0]])}); +} + +@Component({ + selector: 'form-control-select-multiple-compare-with', + template: ` +
+ +
` +}) +class FormControlSelectMultipleWithCompareFn { + compareFn: + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; + cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + form = new FormGroup({city: new FormControl([{id: 1, name: 'SF'}])}); +} + + +@Component({ + selector: 'ng-model-select-form', + template: ` + + ` +}) +class NgModelSelectForm { + selectedCity: {[k: string]: string} = {}; + cities: any[] = []; +} + +@Component({ + selector: 'ng-model-select-null-form', + template: ` + + ` +}) +class NgModelSelectWithNullForm { + selectedCity: {[k: string]: string} = {}; + cities: any[] = []; +} + +@Component({ + selector: 'ng-model-select-compare-with', + template: ` + + ` +}) +class NgModelSelectWithCustomCompareFnForm { + compareFn: + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; + selectedCity: any = {}; + cities: any[] = []; +} + + +@Component({ + selector: 'ng-model-select-multiple-compare-with', + template: ` + + ` +}) +class NgModelSelectMultipleWithCustomCompareFnForm { + compareFn: + (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2? o1.id === o2.id: o1 === o2; + selectedCities: any[] = []; + cities: any[] = []; +} + +@Component({ + selector: 'ng-model-select-multiple-form', + template: ` + + ` +}) +class NgModelSelectMultipleForm { + selectedCities: any[]; + cities: any[] = []; +} + +@Component({ + selector: 'form-control-range-input', + template: `` +}) +class FormControlRangeInput { + control: FormControl; +} + +@Component({selector: 'ng-model-range-form', template: ''}) +class NgModelRangeForm { + val: any; +} + +@Component({ + selector: 'form-control-radio-buttons', + template: ` +
+ + + + +
+ + ` +}) +export class FormControlRadioButtons { + form: FormGroup; + showRadio = new FormControl('yes'); +} + +@Component({ + selector: 'ng-model-radio-form', + template: ` +
+ + + + + +
+ ` +}) +class NgModelRadioForm { + food: string; + drink: string; +} + +@Directive({ + selector: '[wrapped-value]', + host: {'(input)': 'handleOnInput($event.target.value)', '[value]': 'value'}, + providers: [ + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: WrappedValue}, + {provide: NG_VALIDATORS, multi: true, useExisting: WrappedValue} + ] +}) +class WrappedValue implements ControlValueAccessor { + value: any; + onChange: Function; + + writeValue(value: any) { this.value = `!${value}!`; } + + registerOnChange(fn: (value: any) => void) { this.onChange = fn; } + registerOnTouched(fn: any) {} + + handleOnInput(value: any) { this.onChange(value.substring(1, value.length - 1)); } + + validate(c: AbstractControl) { return c.value === 'expected' ? null : {'err': true}; } +} + +@Component({selector: 'my-input', template: ''}) +export class MyInput implements ControlValueAccessor { + @Output('input') onInput = new EventEmitter(); + value: string; + + constructor(cd: NgControl) { cd.valueAccessor = this; } + + writeValue(value: any) { this.value = `!${value}!`; } + + registerOnChange(fn: (value: any) => void) { this.onInput.subscribe({next: fn}); } + + registerOnTouched(fn: any) {} + + dispatchChangeEvent() { this.onInput.emit(this.value.substring(1, this.value.length - 1)); } +} + +@Component({ + selector: 'my-input-form', + template: ` +
+ +
` +}) +export class MyInputForm { + form: FormGroup; +} + +@Component({ + selector: 'wrapped-value-form', + template: ` +
+ +
` +}) +class WrappedValueForm { + form: FormGroup; +} + +@Component({ + selector: 'ng-model-custom-comp', + template: ` + + `, + providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NgModelCustomComp}] +}) +export class NgModelCustomComp implements ControlValueAccessor { + model: string; + @Input('disabled') isDisabled: boolean = false; + changeFn: (value: any) => void; + + writeValue(value: any) { this.model = value; } + + registerOnChange(fn: (value: any) => void) { this.changeFn = fn; } + + registerOnTouched() {} + + setDisabledState(isDisabled: boolean) { this.isDisabled = isDisabled; } +} + +@Component({ + selector: 'ng-model-custom-wrapper', + template: ` +
+ +
+ ` +}) +export class NgModelCustomWrapper { + name: string; + isDisabled = false; +} \ No newline at end of file