angular-cn/packages/forms/test/template_integration_spec.ts

1582 lines
58 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, Input, Type, forwardRef} from '@angular/core';
import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing';
fix(forms): make composition event buffering configurable (#15256) This commit fixes a regression where `ngModel` no longer syncs letter by letter on Android devices, and instead syncs at the end of every word. This broke when we introduced buffering of IME events so IMEs like Pinyin keyboards or Katakana keyboards wouldn't display composition strings. Unfortunately, iOS devices and Android devices have opposite event behavior. Whereas iOS devices fire composition events for IME keyboards only, Android fires composition events for Latin-language keyboards. For this reason, languages like English don't work as expected on Android if we always buffer. So to support both platforms, composition string buffering will only be turned on by default for non-Android devices. However, we have also added a `COMPOSITION_BUFFER_MODE` token to make this configurable by the application. In some cases, apps might might still want to receive intermediate values. For example, some inputs begin searching based on Latin letters before a character selection is made. As a provider, this is fairly flexible. If you want to turn composition buffering off, simply provide the token at the top level: ```ts providers: [ {provide: COMPOSITION_BUFFER_MODE, useValue: false} ] ``` Or, if you want to change the mode based on locale or platform, you can use a factory: ```ts import {shouldUseBuffering} from 'my/lib'; .... providers: [ {provide: COMPOSITION_BUFFER_MODE, useFactory: shouldUseBuffering} ] ``` Closes #15079. PR Close #15256
2017-03-20 20:38:33 -04:00
import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, ControlValueAccessor, FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, 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';
export function main() {
describe('template-driven forms integration tests', () => {
function initTest<T>(component: Type<T>, ...directives: Type<any>[]): ComponentFixture<T> {
TestBed.configureTestingModule(
{declarations: [component, ...directives], imports: [FormsModule]});
return TestBed.createComponent(component);
}
describe('basic functionality', () => {
it('should support ngModel for standalone fields', fakeAsync(() => {
const fixture = initTest(StandaloneNgModel);
fixture.componentInstance.name = 'oldValue';
fixture.detectChanges();
tick();
// model -> view
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(input.value).toEqual('oldValue');
input.value = 'updatedValue';
dispatchEvent(input, 'input');
tick();
// view -> model
expect(fixture.componentInstance.name).toEqual('updatedValue');
}));
it('should support ngModel registration with a parent form', fakeAsync(() => {
const fixture = initTest(NgModelForm);
fixture.componentInstance.name = 'Nancy';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.value).toEqual({name: 'Nancy'});
expect(form.valid).toBe(false);
}));
it('should add novalidate by default to form element', fakeAsync(() => {
const fixture = initTest(NgModelForm);
fixture.detectChanges();
tick();
const form = fixture.debugElement.query(By.css('form'));
expect(form.nativeElement.getAttribute('novalidate')).toEqual('');
}));
it('should be possible to use native validation and angular forms', fakeAsync(() => {
const fixture = initTest(NgModelNativeValidateForm);
fixture.detectChanges();
tick();
const form = fixture.debugElement.query(By.css('form'));
expect(form.nativeElement.hasAttribute('novalidate')).toEqual(false);
}));
it('should support ngModelGroup', fakeAsync(() => {
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.first = 'Nancy';
fixture.componentInstance.last = 'Drew';
fixture.componentInstance.email = 'some email';
fixture.detectChanges();
tick();
// model -> view
const inputs = fixture.debugElement.queryAll(By.css('input'));
expect(inputs[0].nativeElement.value).toEqual('Nancy');
expect(inputs[1].nativeElement.value).toEqual('Drew');
inputs[0].nativeElement.value = 'Carson';
dispatchEvent(inputs[0].nativeElement, 'input');
tick();
// view -> model
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.value).toEqual({name: {first: 'Carson', last: 'Drew'}, email: 'some email'});
}));
it('should add controls and control groups to form control model', fakeAsync(() => {
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.first = 'Nancy';
fixture.componentInstance.last = 'Drew';
fixture.componentInstance.email = 'some email';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.get('name').value).toEqual({first: 'Nancy', last: 'Drew'});
expect(form.control.get('name.first').value).toEqual('Nancy');
expect(form.control.get('email').value).toEqual('some email');
}));
it('should remove controls and control groups from form control model', fakeAsync(() => {
const fixture = initTest(NgModelNgIfForm);
fixture.componentInstance.emailShowing = true;
fixture.componentInstance.first = 'Nancy';
fixture.componentInstance.email = 'some email';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.get('email').value).toEqual('some email');
expect(form.value).toEqual({name: {first: 'Nancy'}, email: 'some email'});
// should remove individual control successfully
fixture.componentInstance.emailShowing = false;
fixture.detectChanges();
tick();
expect(form.control.get('email')).toBe(null);
expect(form.value).toEqual({name: {first: 'Nancy'}});
expect(form.control.get('name').value).toEqual({first: 'Nancy'});
expect(form.control.get('name.first').value).toEqual('Nancy');
// should remove form group successfully
fixture.componentInstance.groupShowing = false;
fixture.detectChanges();
tick();
expect(form.control.get('name')).toBe(null);
expect(form.control.get('name.first')).toBe(null);
expect(form.value).toEqual({});
}));
it('should set status classes with ngModel', async(() => {
const fixture = initTest(NgModelForm);
fixture.componentInstance.name = 'aa';
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']);
input.value = 'updatedValue';
dispatchEvent(input, 'input');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
});
}));
it('should set status classes with ngModel and async validators', fakeAsync(() => {
const fixture = initTest(NgModelAsyncValidation, NgAsyncValidator);
fixture.whenStable().then(() => {
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur');
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']);
input.value = 'updatedValue';
dispatchEvent(input, 'input');
tick();
fixture.detectChanges();
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
});
}));
it('should set status classes with ngModelGroup and ngForm', async(() => {
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.first = '';
fixture.detectChanges();
const form = fixture.debugElement.query(By.css('form')).nativeElement;
const modelGroup = fixture.debugElement.query(By.css('[ngModelGroup]')).nativeElement;
const input = fixture.debugElement.query(By.css('input')).nativeElement;
// ngModelGroup creates its control asynchronously
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(sortedClassList(modelGroup)).toEqual([
'ng-invalid', 'ng-pristine', 'ng-untouched'
]);
expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, 'blur');
fixture.detectChanges();
expect(sortedClassList(modelGroup)).toEqual([
'ng-invalid', 'ng-pristine', 'ng-touched'
]);
expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']);
input.value = 'updatedValue';
dispatchEvent(input, 'input');
fixture.detectChanges();
expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
});
}));
it('should not create a template-driven form when ngNoForm is used', () => {
const fixture = initTest(NgNoFormComp);
fixture.detectChanges();
expect(fixture.debugElement.children[0].providerTokens.length).toEqual(0);
});
it('should not add novalidate when ngNoForm is used', () => {
const fixture = initTest(NgNoFormComp);
fixture.detectChanges();
const form = fixture.debugElement.query(By.css('form'));
expect(form.nativeElement.hasAttribute('novalidate')).toEqual(false);
});
});
describe('name and ngModelOptions', () => {
it('should throw if ngModel has a parent form but no name attr or standalone label', () => {
const fixture = initTest(InvalidNgModelNoName);
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(`name attribute must be set`));
});
it('should not throw if ngModel has a parent form, no name attr, and a standalone label',
() => {
const fixture = initTest(NgModelOptionsStandalone);
expect(() => fixture.detectChanges()).not.toThrow();
});
it('should not register standalone ngModels with parent form', fakeAsync(() => {
const fixture = initTest(NgModelOptionsStandalone);
fixture.componentInstance.one = 'some data';
fixture.componentInstance.two = 'should not show';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const inputs = fixture.debugElement.queryAll(By.css('input'));
tick();
expect(form.value).toEqual({one: 'some data'});
expect(inputs[1].nativeElement.value).toEqual('should not show');
}));
it('should override name attribute with ngModelOptions name if provided', fakeAsync(() => {
const fixture = initTest(NgModelForm);
fixture.componentInstance.options = {name: 'override'};
fixture.componentInstance.name = 'some data';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.value).toEqual({override: 'some data'});
}));
});
describe('submit and reset events', () => {
it('should emit ngSubmit event with the original submit event on submit', fakeAsync(() => {
const fixture = initTest(NgModelForm);
fixture.componentInstance.event = null;
const form = fixture.debugElement.query(By.css('form'));
dispatchEvent(form.nativeElement, 'submit');
tick();
expect(fixture.componentInstance.event.type).toEqual('submit');
}));
it('should mark NgForm as submitted on submit event', fakeAsync(() => {
const fixture = initTest(NgModelForm);
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.submitted).toBe(false);
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
dispatchEvent(formEl, 'submit');
tick();
expect(form.submitted).toBe(true);
}));
it('should reset the form to empty when reset event is fired', fakeAsync(() => {
const fixture = initTest(NgModelForm);
fixture.componentInstance.name = 'should be cleared';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const formEl = fixture.debugElement.query(By.css('form'));
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.value).toBe('should be cleared'); // view value
expect(fixture.componentInstance.name).toBe('should be cleared'); // ngModel value
expect(form.value.name).toEqual('should be cleared'); // control value
dispatchEvent(formEl.nativeElement, 'reset');
fixture.detectChanges();
tick();
expect(input.nativeElement.value).toBe(''); // view value
expect(fixture.componentInstance.name).toBe(null); // ngModel value
expect(form.value.name).toEqual(null); // control value
}));
it('should reset the form submit state when reset button is clicked', fakeAsync(() => {
const fixture = initTest(NgModelForm);
const form = fixture.debugElement.children[0].injector.get(NgForm);
const formEl = fixture.debugElement.query(By.css('form'));
dispatchEvent(formEl.nativeElement, 'submit');
fixture.detectChanges();
tick();
expect(form.submitted).toBe(true);
dispatchEvent(formEl.nativeElement, 'reset');
fixture.detectChanges();
tick();
expect(form.submitted).toBe(false);
}));
});
describe('valueChange and statusChange events', () => {
it('should emit valueChanges and statusChanges on init', fakeAsync(() => {
const fixture = initTest(NgModelForm);
const form = fixture.debugElement.children[0].injector.get(NgForm);
fixture.componentInstance.name = 'aa';
fixture.detectChanges();
expect(form.valid).toEqual(true);
expect(form.value).toEqual({});
let formValidity: string;
let formValue: Object;
form.statusChanges.subscribe((status: string) => formValidity = status);
form.valueChanges.subscribe((value: string) => formValue = value);
tick();
expect(formValidity).toEqual('INVALID');
expect(formValue).toEqual({name: 'aa'});
}));
it('should mark controls dirty before emitting the value change event', fakeAsync(() => {
const fixture = initTest(NgModelForm);
const form = fixture.debugElement.children[0].injector.get(NgForm).form;
fixture.detectChanges();
tick();
form.get('name').valueChanges.subscribe(
() => { expect(form.get('name').dirty).toBe(true); });
const inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
inputEl.value = 'newValue';
dispatchEvent(inputEl, 'input');
}));
it('should mark controls pristine before emitting the value change event when resetting ',
fakeAsync(() => {
const fixture = initTest(NgModelForm);
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm).form;
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
const inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
inputEl.value = 'newValue';
dispatchEvent(inputEl, 'input');
expect(form.get('name').pristine).toBe(false);
form.get('name').valueChanges.subscribe(
() => { expect(form.get('name').pristine).toBe(true); });
dispatchEvent(formEl, 'reset');
}));
});
describe('disabled controls', () => {
it('should not consider disabled controls in value or validation', fakeAsync(() => {
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.isDisabled = false;
fixture.componentInstance.first = '';
fixture.componentInstance.last = 'Drew';
fixture.componentInstance.email = 'some email';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.value).toEqual({name: {first: '', last: 'Drew'}, email: 'some email'});
expect(form.valid).toBe(false);
expect(form.control.get('name.first').disabled).toBe(false);
fixture.componentInstance.isDisabled = true;
fixture.detectChanges();
tick();
expect(form.value).toEqual({name: {last: 'Drew'}, email: 'some email'});
expect(form.valid).toBe(true);
expect(form.control.get('name.first').disabled).toBe(true);
}));
it('should add disabled attribute in the UI if disable() is called programmatically',
fakeAsync(() => {
const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.isDisabled = false;
fixture.componentInstance.first = 'Nancy';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
form.control.get('name.first').disable();
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css(`[name="first"]`));
expect(input.nativeElement.disabled).toBe(true);
}));
it('should disable a custom control if disabled attr is added', async(() => {
const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp);
fixture.componentInstance.name = 'Nancy';
fixture.componentInstance.isDisabled = true;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.get('name').disabled).toBe(true);
const customInput = fixture.debugElement.query(By.css('[name="custom"]'));
expect(customInput.nativeElement.disabled).toEqual(true);
});
});
}));
it('should disable a control with unbound disabled attr', fakeAsync(() => {
TestBed.overrideComponent(NgModelForm, {
set: {
template: `
<form>
<input name="name" [(ngModel)]="name" disabled>
</form>
`,
}
});
const fixture = initTest(NgModelForm);
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.get('name').disabled).toBe(true);
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.disabled).toEqual(true);
form.control.enable();
fixture.detectChanges();
tick();
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 <type=range>', 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 <type=radio>', 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 <type=radio> 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<NgModelSelectMultipleForm>;
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', () => {
it('required validator should validate checkbox', fakeAsync(() => {
const fixture = initTest(NgModelCheckboxRequiredValidator);
fixture.detectChanges();
tick();
const control =
fixture.debugElement.children[0].injector.get(NgForm).control.get('checkbox');
const input = fixture.debugElement.query(By.css('input'));
expect(input.nativeElement.checked).toBe(false);
expect(control.hasError('required')).toBe(false);
fixture.componentInstance.required = true;
fixture.detectChanges();
tick();
expect(input.nativeElement.checked).toBe(false);
expect(control.hasError('required')).toBe(true);
input.nativeElement.checked = true;
dispatchEvent(input.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(input.nativeElement.checked).toBe(true);
expect(control.hasError('required')).toBe(false);
input.nativeElement.checked = false;
dispatchEvent(input.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(input.nativeElement.checked).toBe(false);
expect(control.hasError('required')).toBe(true);
}));
it('should validate email', fakeAsync(() => {
const fixture = initTest(NgModelEmailValidator);
fixture.detectChanges();
tick();
const control =
fixture.debugElement.children[0].injector.get(NgForm).control.get('email');
const input = fixture.debugElement.query(By.css('input'));
expect(control.hasError('email')).toBe(false);
fixture.componentInstance.validatorEnabled = true;
fixture.detectChanges();
tick();
expect(input.nativeElement.value).toEqual('');
expect(control.hasError('email')).toBe(true);
input.nativeElement.value = 'test@gmail.com';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
tick();
expect(input.nativeElement.value).toEqual('test@gmail.com');
expect(control.hasError('email')).toBe(false);
input.nativeElement.value = 'text';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
tick();
expect(input.nativeElement.value).toEqual('text');
expect(control.hasError('email')).toBe(true);
}));
it('should support dir validators using bindings', fakeAsync(() => {
const fixture = initTest(NgModelValidationBindings);
fixture.componentInstance.required = true;
fixture.componentInstance.minLen = 3;
fixture.componentInstance.maxLen = 3;
fixture.componentInstance.pattern = '.{3,}';
fixture.detectChanges();
tick();
const required = fixture.debugElement.query(By.css('[name=required]'));
const minLength = fixture.debugElement.query(By.css('[name=minlength]'));
const maxLength = fixture.debugElement.query(By.css('[name=maxlength]'));
const pattern = fixture.debugElement.query(By.css('[name=pattern]'));
required.nativeElement.value = '';
minLength.nativeElement.value = '1';
maxLength.nativeElement.value = '1234';
pattern.nativeElement.value = '12';
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
fixture.detectChanges();
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.hasError('required', ['required'])).toEqual(true);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(true);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(true);
required.nativeElement.value = '1';
minLength.nativeElement.value = '123';
maxLength.nativeElement.value = '123';
pattern.nativeElement.value = '123';
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
expect(form.valid).toEqual(true);
}));
it('should support optional fields with string pattern validator', fakeAsync(() => {
const fixture = initTest(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.pattern = '[a-z]+';
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeTruthy();
input.nativeElement.value = '1';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeFalsy();
expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy();
}));
it('should support optional fields with RegExp pattern validator', fakeAsync(() => {
const fixture = initTest(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.pattern = /^[a-z]+$/;
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeTruthy();
input.nativeElement.value = '1';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeFalsy();
expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy();
}));
it('should support optional fields with minlength validator', fakeAsync(() => {
const fixture = initTest(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.minLen = 2;
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeTruthy();
input.nativeElement.value = '1';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeFalsy();
expect(form.control.hasError('minlength', ['tovalidate'])).toBeTruthy();
}));
it('changes on bound properties should change the validation state of the form',
fakeAsync(() => {
const fixture = initTest(NgModelValidationBindings);
fixture.detectChanges();
tick();
const required = fixture.debugElement.query(By.css('[name=required]'));
const minLength = fixture.debugElement.query(By.css('[name=minlength]'));
const maxLength = fixture.debugElement.query(By.css('[name=maxlength]'));
const pattern = fixture.debugElement.query(By.css('[name=pattern]'));
required.nativeElement.value = '';
minLength.nativeElement.value = '1';
maxLength.nativeElement.value = '1234';
pattern.nativeElement.value = '12';
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.hasError('required', ['required'])).toEqual(false);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(false);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true);
fixture.componentInstance.required = true;
fixture.componentInstance.minLen = 3;
fixture.componentInstance.maxLen = 3;
fixture.componentInstance.pattern = '.{3,}';
fixture.detectChanges();
dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input');
expect(form.control.hasError('required', ['required'])).toEqual(true);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(true);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(true);
expect(form.valid).toEqual(false);
expect(required.nativeElement.getAttribute('required')).toEqual('');
expect(fixture.componentInstance.minLen.toString())
.toEqual(minLength.nativeElement.getAttribute('minlength'));
expect(fixture.componentInstance.maxLen.toString())
.toEqual(maxLength.nativeElement.getAttribute('maxlength'));
expect(fixture.componentInstance.pattern.toString())
.toEqual(pattern.nativeElement.getAttribute('pattern'));
fixture.componentInstance.required = false;
fixture.componentInstance.minLen = null;
fixture.componentInstance.maxLen = null;
fixture.componentInstance.pattern = null;
fixture.detectChanges();
expect(form.control.hasError('required', ['required'])).toEqual(false);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(false);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true);
expect(required.nativeElement.getAttribute('required')).toEqual(null);
expect(required.nativeElement.getAttribute('minlength')).toEqual(null);
expect(required.nativeElement.getAttribute('maxlength')).toEqual(null);
expect(required.nativeElement.getAttribute('pattern')).toEqual(null);
}));
});
fix(forms): make composition event buffering configurable (#15256) This commit fixes a regression where `ngModel` no longer syncs letter by letter on Android devices, and instead syncs at the end of every word. This broke when we introduced buffering of IME events so IMEs like Pinyin keyboards or Katakana keyboards wouldn't display composition strings. Unfortunately, iOS devices and Android devices have opposite event behavior. Whereas iOS devices fire composition events for IME keyboards only, Android fires composition events for Latin-language keyboards. For this reason, languages like English don't work as expected on Android if we always buffer. So to support both platforms, composition string buffering will only be turned on by default for non-Android devices. However, we have also added a `COMPOSITION_BUFFER_MODE` token to make this configurable by the application. In some cases, apps might might still want to receive intermediate values. For example, some inputs begin searching based on Latin letters before a character selection is made. As a provider, this is fairly flexible. If you want to turn composition buffering off, simply provide the token at the top level: ```ts providers: [ {provide: COMPOSITION_BUFFER_MODE, useValue: false} ] ``` Or, if you want to change the mode based on locale or platform, you can use a factory: ```ts import {shouldUseBuffering} from 'my/lib'; .... providers: [ {provide: COMPOSITION_BUFFER_MODE, useFactory: shouldUseBuffering} ] ``` Closes #15079. PR Close #15256
2017-03-20 20:38:33 -04:00
describe('IME events', () => {
it('should determine IME event handling depending on platform by default', fakeAsync(() => {
const fixture = initTest(StandaloneNgModel);
const inputEl = fixture.debugElement.query(By.css('input'));
const inputNativeEl = inputEl.nativeElement;
fixture.componentInstance.name = 'oldValue';
fixture.detectChanges();
tick();
expect(inputNativeEl.value).toEqual('oldValue');
inputEl.triggerEventHandler('compositionstart', null);
inputNativeEl.value = 'updatedValue';
dispatchEvent(inputNativeEl, 'input');
tick();
const isAndroid = /android (\d+)/.test(getDOM().getUserAgent().toLowerCase());
if (isAndroid) {
// On Android, values should update immediately
expect(fixture.componentInstance.name).toEqual('updatedValue');
} else {
// On other platforms, values should wait until compositionend
expect(fixture.componentInstance.name).toEqual('oldValue');
inputEl.triggerEventHandler('compositionend', {target: {value: 'updatedValue'}});
fixture.detectChanges();
tick();
expect(fixture.componentInstance.name).toEqual('updatedValue');
}
}));
it('should hold IME events until compositionend if composition mode', fakeAsync(() => {
TestBed.overrideComponent(
StandaloneNgModel,
{set: {providers: [{provide: COMPOSITION_BUFFER_MODE, useValue: true}]}});
const fixture = initTest(StandaloneNgModel);
const inputEl = fixture.debugElement.query(By.css('input'));
const inputNativeEl = inputEl.nativeElement;
fixture.componentInstance.name = 'oldValue';
fixture.detectChanges();
tick();
expect(inputNativeEl.value).toEqual('oldValue');
inputEl.triggerEventHandler('compositionstart', null);
inputNativeEl.value = 'updatedValue';
dispatchEvent(inputNativeEl, 'input');
tick();
// ngModel should not update when compositionstart
expect(fixture.componentInstance.name).toEqual('oldValue');
inputEl.triggerEventHandler('compositionend', {target: {value: 'updatedValue'}});
fixture.detectChanges();
tick();
// ngModel should update when compositionend
expect(fixture.componentInstance.name).toEqual('updatedValue');
}));
it('should work normally with composition events if composition mode is off',
fakeAsync(() => {
TestBed.overrideComponent(
StandaloneNgModel,
{set: {providers: [{provide: COMPOSITION_BUFFER_MODE, useValue: false}]}});
const fixture = initTest(StandaloneNgModel);
const inputEl = fixture.debugElement.query(By.css('input'));
const inputNativeEl = inputEl.nativeElement;
fixture.componentInstance.name = 'oldValue';
fixture.detectChanges();
tick();
expect(inputNativeEl.value).toEqual('oldValue');
inputEl.triggerEventHandler('compositionstart', null);
inputNativeEl.value = 'updatedValue';
dispatchEvent(inputNativeEl, 'input');
tick();
// ngModel should update normally
expect(fixture.componentInstance.name).toEqual('updatedValue');
}));
});
describe('ngModel corner cases', () => {
it('should update the view when the model is set back to what used to be in the view',
fakeAsync(() => {
const fixture = initTest(StandaloneNgModel);
fixture.componentInstance.name = '';
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
input.value = 'aa';
input.selectionStart = 1;
dispatchEvent(input, 'input');
fixture.detectChanges();
tick();
expect(fixture.componentInstance.name).toEqual('aa');
// Programmatically update the input value to be "bb".
fixture.componentInstance.name = 'bb';
fixture.detectChanges();
tick();
expect(input.value).toEqual('bb');
// Programatically set it back to "aa".
fixture.componentInstance.name = 'aa';
fixture.detectChanges();
tick();
expect(input.value).toEqual('aa');
}));
it('should not crash when validity is checked from a binding', fakeAsync(() => {
const fixture = initTest(NgModelValidBinding);
tick();
expect(() => fixture.detectChanges()).not.toThrowError();
}));
});
});
}
@Component({
selector: 'standalone-ng-model',
template: `
<input type="text" [(ngModel)]="name">
`
})
class StandaloneNgModel {
name: string;
}
@Component({
selector: 'ng-model-form',
template: `
<form (ngSubmit)="event=$event" (reset)="onReset()">
<input name="name" [(ngModel)]="name" minlength="10" [ngModelOptions]="options">
</form>
`
})
class NgModelForm {
name: string;
event: Event;
options = {};
onReset() {}
}
@Component({selector: 'ng-model-native-validate-form', template: `<form ngNativeValidate></form>`})
class NgModelNativeValidateForm {
}
@Component({
selector: 'ng-model-group-form',
template: `
<form>
<div ngModelGroup="name">
<input name="first" [(ngModel)]="first" required [disabled]="isDisabled">
<input name="last" [(ngModel)]="last">
</div>
<input name="email" [(ngModel)]="email">
</form>
`
})
class NgModelGroupForm {
first: string;
last: string;
email: string;
isDisabled: boolean;
}
@Component({
selector: 'ng-model-valid-binding',
template: `
<form>
<div ngModelGroup="name" #group="ngModelGroup">
<input name="first" [(ngModel)]="first" required>
{{ group.valid }}
</div>
</form>
`
})
class NgModelValidBinding {
first: string;
}
@Component({
selector: 'ng-model-ngif-form',
template: `
<form>
<div ngModelGroup="name" *ngIf="groupShowing">
<input name="first" [(ngModel)]="first">
</div>
<input name="email" [(ngModel)]="email" *ngIf="emailShowing">
</form>
`
})
class NgModelNgIfForm {
first: string;
groupShowing = true;
emailShowing = true;
email: string;
}
@Component({
selector: 'ng-no-form',
template: `
<form ngNoForm>
<input name="name">
</form>
`
})
class NgNoFormComp {
}
@Component({
selector: 'invalid-ng-model-noname',
template: `
<form>
<input [(ngModel)]="name">
</form>
`
})
class InvalidNgModelNoName {
}
@Component({
selector: 'ng-model-options-standalone',
template: `
<form>
<input name="one" [(ngModel)]="one">
<input [(ngModel)]="two" [ngModelOptions]="{standalone: true}">
</form>
`
})
class NgModelOptionsStandalone {
one: string;
two: string;
}
@Component({selector: 'ng-model-range-form', template: '<input type="range" [(ngModel)]="val">'})
class NgModelRangeForm {
val: any;
}
@Component({
selector: 'ng-model-radio-form',
template: `
<form>
<input type="radio" name="food" [(ngModel)]="food" value="chicken">
<input type="radio" name="food" [(ngModel)]="food" value="fish">
<input type="radio" name="drink" [(ngModel)]="drink" value="cola">
<input type="radio" name="drink" [(ngModel)]="drink" value="sprite">
</form>
`
})
class NgModelRadioForm {
food: string;
drink: string;
}
@Component({
selector: 'ng-model-select-form',
template: `
<select [(ngModel)]="selectedCity">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
</select>
`
})
class NgModelSelectForm {
selectedCity: {[k: string]: string} = {};
cities: any[] = [];
}
@Component({
selector: 'ng-model-select-null-form',
template: `
<select [(ngModel)]="selectedCity">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
<option [ngValue]="null">Unspecified</option>
</select>
`
})
class NgModelSelectWithNullForm {
selectedCity: {[k: string]: string} = {};
cities: any[] = [];
}
@Component({
selector: 'ng-model-select-compare-with',
template: `
<select [(ngModel)]="selectedCity" [compareWith]="compareFn">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
</select>
`
})
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: `
<select multiple [(ngModel)]="selectedCities" [compareWith]="compareFn">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
</select>
`
})
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: `
<select multiple [(ngModel)]="selectedCities">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
</select>
`
})
class NgModelSelectMultipleForm {
selectedCities: any[];
cities: any[] = [];
}
@Component({
selector: 'ng-model-custom-comp',
template: `
<input name="custom" [(ngModel)]="model" (ngModelChange)="changeFn($event)" [disabled]="isDisabled">
`,
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: `
<form>
<ng-model-custom-comp name="name" [(ngModel)]="name" [disabled]="isDisabled"></ng-model-custom-comp>
</form>
`
})
class NgModelCustomWrapper {
name: string;
isDisabled = false;
}
@Component({
selector: 'ng-model-validation-bindings',
template: `
<form>
<input name="required" ngModel [required]="required">
<input name="minlength" ngModel [minlength]="minLen">
<input name="maxlength" ngModel [maxlength]="maxLen">
<input name="pattern" ngModel [pattern]="pattern">
</form>
`
})
class NgModelValidationBindings {
required: boolean;
minLen: number;
maxLen: number;
pattern: string;
}
@Component({
selector: 'ng-model-multiple-validators',
template: `
<form>
<input name="tovalidate" ngModel [required]="required" [minlength]="minLen" [pattern]="pattern">
</form>
`
})
class NgModelMultipleValidators {
required: boolean;
minLen: number;
pattern: string|RegExp;
}
@Component({
selector: 'ng-model-checkbox-validator',
template:
`<form><input type="checkbox" [(ngModel)]="accepted" [required]="required" name="checkbox"></form>`
})
class NgModelCheckboxRequiredValidator {
accepted: boolean = false;
required: boolean = false;
}
@Component({
selector: 'ng-model-email',
template: `<form><input type="email" ngModel [email]="validatorEnabled" name="email"></form>`
})
class NgModelEmailValidator {
validatorEnabled: boolean = false;
}
@Directive({
selector: '[ng-async-validator]',
providers: [
{provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => NgAsyncValidator), multi: true}
]
})
class NgAsyncValidator implements AsyncValidator {
validate(c: AbstractControl) { return Promise.resolve(null); }
}
@Component({
selector: 'ng-model-async-validation',
template: `<input name="async" ngModel ng-async-validator>`
})
class NgModelAsyncValidation {
}
function sortedClassList(el: HTMLElement) {
const l = getDOM().classList(el);
l.sort();
return l;
}