feat(forms): added touched and untouched to Control

This commit is contained in:
vsavkin 2015-06-02 08:41:33 -07:00
parent f303f0c17a
commit ec3a78289f
8 changed files with 66 additions and 11 deletions

View File

@ -17,15 +17,17 @@ import {ControlValueAccessor} from './control_value_accessor';
@Directive({ @Directive({
selector: selector:
'input[type=checkbox][ng-control],input[type=checkbox][ng-form-control],input[type=checkbox][ng-model]', 'input[type=checkbox][ng-control],input[type=checkbox][ng-form-control],input[type=checkbox][ng-model]',
hostListeners: {'change': 'onChange($event.target.checked)'}, hostListeners: {'change': 'onChange($event.target.checked)', 'blur': 'onTouched()'},
hostProperties: {'checked': 'checked'} hostProperties: {'checked': 'checked'}
}) })
export class CheckboxControlValueAccessor implements ControlValueAccessor { export class CheckboxControlValueAccessor implements ControlValueAccessor {
checked: boolean; checked: boolean;
onChange: Function; onChange: Function;
onTouched: Function;
constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) {
this.onChange = (_) => {}; this.onChange = (_) => {};
this.onTouched = (_) => {};
cd.valueAccessor = this; cd.valueAccessor = this;
} }
@ -34,5 +36,6 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor {
this._elementRef.boundElementIndex, 'checked', value) this._elementRef.boundElementIndex, 'checked', value)
} }
registerOnChange(fn) { this.onChange = fn; } registerOnChange(fn): void { this.onChange = fn; }
registerOnTouched(fn): void { this.onTouched = fn; }
} }

View File

@ -1,4 +1,5 @@
export interface ControlValueAccessor { export interface ControlValueAccessor {
writeValue(obj: any): void; writeValue(obj: any): void;
registerOnChange(fun: any): void; registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
} }

View File

@ -19,16 +19,21 @@ import {ControlValueAccessor} from './control_value_accessor';
@Directive({ @Directive({
selector: selector:
'input:not([type=checkbox])[ng-control],textarea[ng-control],input:not([type=checkbox])[ng-form-control],textarea[ng-form-control],input:not([type=checkbox])[ng-model],textarea[ng-model]', 'input:not([type=checkbox])[ng-control],textarea[ng-control],input:not([type=checkbox])[ng-form-control],textarea[ng-form-control],input:not([type=checkbox])[ng-model],textarea[ng-model]',
hostListeners: hostListeners: {
{'change': 'onChange($event.target.value)', 'input': 'onChange($event.target.value)'}, 'change': 'onChange($event.target.value)',
'input': 'onChange($event.target.value)',
'blur': 'onTouched()'
},
hostProperties: {'value': 'value'} hostProperties: {'value': 'value'}
}) })
export class DefaultValueAccessor implements ControlValueAccessor { export class DefaultValueAccessor implements ControlValueAccessor {
value = null; value = null;
onChange: Function; onChange: Function;
onTouched: Function;
constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) {
this.onChange = (_) => {}; this.onChange = (_) => {};
this.onTouched = (_) => {};
cd.valueAccessor = this; cd.valueAccessor = this;
} }
@ -37,5 +42,6 @@ export class DefaultValueAccessor implements ControlValueAccessor {
this._elementRef.boundElementIndex, 'value', value) this._elementRef.boundElementIndex, 'value', value)
} }
registerOnChange(fn) { this.onChange = fn; } registerOnChange(fn): void { this.onChange = fn; }
} registerOnTouched(fn): void { this.onTouched = fn; }
}

View File

@ -18,16 +18,21 @@ import {ControlValueAccessor} from './control_value_accessor';
*/ */
@Directive({ @Directive({
selector: 'select[ng-control],select[ng-form-control],select[ng-model]', selector: 'select[ng-control],select[ng-form-control],select[ng-model]',
hostListeners: hostListeners: {
{'change': 'onChange($event.target.value)', 'input': 'onChange($event.target.value)'}, 'change': 'onChange($event.target.value)',
'input': 'onChange($event.target.value)',
'blur': 'onTouched()'
},
hostProperties: {'value': 'value'} hostProperties: {'value': 'value'}
}) })
export class SelectControlValueAccessor implements ControlValueAccessor { export class SelectControlValueAccessor implements ControlValueAccessor {
value = null; value = null;
onChange: Function; onChange: Function;
onTouched: Function;
constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) {
this.onChange = (_) => {}; this.onChange = (_) => {};
this.onTouched = (_) => {};
this.value = ''; this.value = '';
cd.valueAccessor = this; cd.valueAccessor = this;
} }
@ -37,5 +42,6 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
this._elementRef.boundElementIndex, 'value', value) this._elementRef.boundElementIndex, 'value', value)
} }
registerOnChange(fn) { this.onChange = fn; } registerOnChange(fn): void { this.onChange = fn; }
} registerOnTouched(fn): void { this.onTouched = fn; }
}

View File

@ -27,6 +27,9 @@ export function setUpControl(c: Control, dir: ControlDirective) {
// model -> view // model -> view
c.registerOnChange(newValue => dir.valueAccessor.writeValue(newValue)); c.registerOnChange(newValue => dir.valueAccessor.writeValue(newValue));
// touched
dir.valueAccessor.registerOnTouched(() => c.touch());
} }
function _throwError(dir: ControlDirective, message: string): void { function _throwError(dir: ControlDirective, message: string): void {

View File

@ -30,6 +30,7 @@ export class AbstractControl {
_status: string; _status: string;
_errors: StringMap<string, any>; _errors: StringMap<string, any>;
_pristine: boolean; _pristine: boolean;
_touched: boolean;
_parent: any; /* ControlGroup | ControlArray */ _parent: any; /* ControlGroup | ControlArray */
validator: Function; validator: Function;
@ -38,6 +39,7 @@ export class AbstractControl {
constructor(validator: Function) { constructor(validator: Function) {
this.validator = validator; this.validator = validator;
this._pristine = true; this._pristine = true;
this._touched = false;
} }
get value(): any { return this._value; } get value(): any { return this._value; }
@ -52,8 +54,14 @@ export class AbstractControl {
get dirty(): boolean { return !this.pristine; } get dirty(): boolean { return !this.pristine; }
get touched(): boolean { return this._touched; }
get untouched(): boolean { return !this._touched; }
get valueChanges(): Observable { return this._valueChanges; } get valueChanges(): Observable { return this._valueChanges; }
touch(): void { this._touched = true; }
setParent(parent) { this._parent = parent; } setParent(parent) { this._parent = parent; }
updateValidity({onlySelf}: {onlySelf?: boolean} = {}): void { updateValidity({onlySelf}: {onlySelf?: boolean} = {}): void {

View File

@ -29,6 +29,7 @@ class DummyControlValueAccessor implements ControlValueAccessor {
writtenValue; writtenValue;
registerOnChange(fn) {} registerOnChange(fn) {}
registerOnTouched(fn) {}
writeValue(obj: any): void { this.writtenValue = obj; } writeValue(obj: any): void { this.writtenValue = obj; }
} }

View File

@ -138,6 +138,32 @@ export function main() {
}); });
})); }));
it("should mark controls as touched after interacting with the DOM control",
inject([TestBed, AsyncTestCompleter], (tb, async) => {
var login = new Control("oldValue");
var form = new ControlGroup({"login": login});
var ctx = MyComp.create({form: form});
var t = `<div [ng-form-model]="form">
<input type="text" ng-control="login">
</div>`;
tb.createView(MyComp, {context: ctx, html: t})
.then((view) => {
view.detectChanges();
var loginEl = view.querySelector("input");
expect(login.touched).toBe(false);
dispatchEvent(loginEl, "blur");
expect(login.touched).toBe(true);
async.done();
});
}));
describe("different control types", () => { describe("different control types", () => {
it("should support <input type=text>", inject([TestBed, AsyncTestCompleter], (tb, async) => { it("should support <input type=text>", inject([TestBed, AsyncTestCompleter], (tb, async) => {
var ctx = MyComp.create({form: new ControlGroup({"text": new Control("old")})}); var ctx = MyComp.create({form: new ControlGroup({"text": new Control("old")})});
@ -590,6 +616,7 @@ class WrappedValue implements ControlValueAccessor {
writeValue(value) { this.value = `!${value}!`; } writeValue(value) { this.value = `!${value}!`; }
registerOnChange(fn) { this.onChange = fn; } registerOnChange(fn) { this.onChange = fn; }
registerOnTouched(fn) {}
handleOnChange(value) { this.onChange(value.substring(1, value.length - 1)); } handleOnChange(value) { this.onChange(value.substring(1, value.length - 1)); }
} }