From 3baf815d76db98ad96a2eb46e511df8439416d0e Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 3 Jun 2015 11:56:01 -0700 Subject: [PATCH] feat(forms): added support for status classes --- .../src/core/compiler/proto_view_factory.ts | 3 + .../directives/checkbox_value_accessor.ts | 20 ++-- .../src/forms/directives/control_directive.ts | 2 + .../directives/control_name_directive.ts | 4 + .../directives/default_value_accessor.ts | 21 ++-- .../directives/form_control_directive.ts | 14 ++- .../src/forms/directives/form_directive.ts | 2 + .../forms/directives/form_model_directive.ts | 2 + .../select_control_value_accessor.ts | 20 ++-- .../template_driven_form_directive.ts | 2 + .../angular2/test/forms/directives_spec.ts | 2 +- .../angular2/test/forms/integration_spec.ts | 101 ++++++++++++++++++ modules/angular2/test/forms/model_spec.ts | 12 +-- 13 files changed, 167 insertions(+), 38 deletions(-) diff --git a/modules/angular2/src/core/compiler/proto_view_factory.ts b/modules/angular2/src/core/compiler/proto_view_factory.ts index 7025b6a0ec..5a35a7c71f 100644 --- a/modules/angular2/src/core/compiler/proto_view_factory.ts +++ b/modules/angular2/src/core/compiler/proto_view_factory.ts @@ -105,7 +105,10 @@ class BindingRecordsCreator { if (directiveRecord.callOnCheck) { ListWrapper.push(bindings, BindingRecord.createDirectiveOnCheck(directiveRecord)); } + } + for (var i = 0; i < directiveBinders.length; i++) { + var directiveBinder = directiveBinders[i]; // host properties MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => { var dirIndex = new DirectiveIndex(boundElementIndex, i); diff --git a/modules/angular2/src/forms/directives/checkbox_value_accessor.ts b/modules/angular2/src/forms/directives/checkbox_value_accessor.ts index 9f7f79a0f7..c4980adcb6 100644 --- a/modules/angular2/src/forms/directives/checkbox_value_accessor.ts +++ b/modules/angular2/src/forms/directives/checkbox_value_accessor.ts @@ -1,5 +1,4 @@ -import {ElementRef, Directive} from 'angular2/angular2'; -import {Renderer} from 'angular2/src/render/api'; +import {Directive} from 'angular2/angular2'; import {ControlDirective} from './control_directive'; import {ControlValueAccessor} from './control_value_accessor'; @@ -18,23 +17,28 @@ import {ControlValueAccessor} from './control_value_accessor'; selector: 'input[type=checkbox][ng-control],input[type=checkbox][ng-form-control],input[type=checkbox][ng-model]', hostListeners: {'change': 'onChange($event.target.checked)', 'blur': 'onTouched()'}, - hostProperties: {'checked': 'checked'} + hostProperties: { + 'checked': 'checked', + 'cd.control?.untouched == true': 'class.ng-untouched', + 'cd.control?.touched == true': 'class.ng-touched', + 'cd.control?.pristine == true': 'class.ng-pristine', + 'cd.control?.dirty == true': 'class.ng-dirty', + 'cd.control?.valid == true': 'class.ng-valid', + 'cd.control?.valid == false': 'class.ng-invalid' + } }) export class CheckboxControlValueAccessor implements ControlValueAccessor { checked: boolean; onChange: Function; onTouched: Function; - constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { + constructor(private cd: ControlDirective) { this.onChange = (_) => {}; this.onTouched = (_) => {}; cd.valueAccessor = this; } - writeValue(value) { - this._renderer.setElementProperty(this._elementRef.parentView.render, - this._elementRef.boundElementIndex, 'checked', value) - } + writeValue(value) { this.checked = value; } registerOnChange(fn): void { this.onChange = fn; } registerOnTouched(fn): void { this.onTouched = fn; } diff --git a/modules/angular2/src/forms/directives/control_directive.ts b/modules/angular2/src/forms/directives/control_directive.ts index dbd9478d19..97ba180711 100644 --- a/modules/angular2/src/forms/directives/control_directive.ts +++ b/modules/angular2/src/forms/directives/control_directive.ts @@ -1,5 +1,6 @@ import {ControlValueAccessor} from './control_value_accessor'; import {Validators} from '../validators'; +import {Control} from '../model'; /** * A directive that bind a [ng-control] object to a DOM element. @@ -12,6 +13,7 @@ export class ControlDirective { validator: Function; get path(): List { return null; } + get control(): Control { return null; } constructor() { this.validator = Validators.nullValidator; } viewToModelUpdate(newValue: any): void {} diff --git a/modules/angular2/src/forms/directives/control_name_directive.ts b/modules/angular2/src/forms/directives/control_name_directive.ts index 4f16fadcce..3d491f1969 100644 --- a/modules/angular2/src/forms/directives/control_name_directive.ts +++ b/modules/angular2/src/forms/directives/control_name_directive.ts @@ -7,6 +7,7 @@ import {FORWARD_REF, Binding, Inject} from 'angular2/di'; import {ControlContainerDirective} from './control_container_directive'; import {ControlDirective} from './control_directive'; import {controlPath} from './shared'; +import {Control} from '../model'; const controlNameBinding = CONST_EXPR(new Binding(ControlDirective, {toAlias: FORWARD_REF(() => ControlNameDirective)})); @@ -84,6 +85,7 @@ export class ControlNameDirective extends ControlDirective { } } + onDestroy() { this.formDirective.removeControl(this); } viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); } @@ -91,4 +93,6 @@ export class ControlNameDirective extends ControlDirective { get path(): List { return controlPath(this.name, this._parent); } get formDirective(): any { return this._parent.formDirective; } + + get control(): Control { return this.formDirective.getControl(this); } } \ No newline at end of file diff --git a/modules/angular2/src/forms/directives/default_value_accessor.ts b/modules/angular2/src/forms/directives/default_value_accessor.ts index 5b5bc7280d..d8b8546672 100644 --- a/modules/angular2/src/forms/directives/default_value_accessor.ts +++ b/modules/angular2/src/forms/directives/default_value_accessor.ts @@ -1,5 +1,4 @@ -import {ElementRef, Directive} from 'angular2/angular2'; -import {Renderer} from 'angular2/src/render/api'; +import {Directive} from 'angular2/angular2'; import {ControlDirective} from './control_directive'; import {ControlValueAccessor} from './control_value_accessor'; @@ -24,24 +23,30 @@ import {ControlValueAccessor} from './control_value_accessor'; 'input': 'onChange($event.target.value)', 'blur': 'onTouched()' }, - hostProperties: {'value': 'value'} + hostProperties: { + 'value': 'value', + 'cd.control?.untouched == true': 'class.ng-untouched', + 'cd.control?.touched == true': 'class.ng-touched', + 'cd.control?.pristine == true': 'class.ng-pristine', + 'cd.control?.dirty == true': 'class.ng-dirty', + 'cd.control?.valid == true': 'class.ng-valid', + 'cd.control?.valid == false': 'class.ng-invalid' + } }) export class DefaultValueAccessor implements ControlValueAccessor { value = null; onChange: Function; onTouched: Function; - constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { + constructor(private cd: ControlDirective) { this.onChange = (_) => {}; this.onTouched = (_) => {}; cd.valueAccessor = this; } - writeValue(value) { - this._renderer.setElementProperty(this._elementRef.parentView.render, - this._elementRef.boundElementIndex, 'value', value) - } + writeValue(value) { this.value = value; } registerOnChange(fn): void { this.onChange = fn; } + registerOnTouched(fn): void { this.onTouched = fn; } } \ No newline at end of file diff --git a/modules/angular2/src/forms/directives/form_control_directive.ts b/modules/angular2/src/forms/directives/form_control_directive.ts index ebfcd599a0..a4bc116adf 100644 --- a/modules/angular2/src/forms/directives/form_control_directive.ts +++ b/modules/angular2/src/forms/directives/form_control_directive.ts @@ -47,12 +47,12 @@ const formControlBinding = @Directive({ selector: '[ng-form-control]', hostInjector: [formControlBinding], - properties: ['control: ng-form-control', 'model: ng-model'], + properties: ['form: ng-form-control', 'model: ng-model'], events: ['ngModel'], lifecycle: [onChange] }) export class FormControlDirective extends ControlDirective { - control: Control; + form: Control; ngModel: EventEmitter; _added: boolean; model: any; @@ -65,14 +65,18 @@ export class FormControlDirective extends ControlDirective { onChange(c) { if (!this._added) { - setUpControl(this.control, this); - this.control.updateValidity(); + setUpControl(this.form, this); + this.form.updateValidity(); this._added = true; } if (StringMapWrapper.contains(c, "model")) { - this.control.updateValue(this.model); + this.form.updateValue(this.model); } } + get control(): Control { return this.form; } + + get path(): List { return []; } + viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); } } diff --git a/modules/angular2/src/forms/directives/form_directive.ts b/modules/angular2/src/forms/directives/form_directive.ts index 9a4ed0cc83..bb7d9e8596 100644 --- a/modules/angular2/src/forms/directives/form_directive.ts +++ b/modules/angular2/src/forms/directives/form_directive.ts @@ -1,9 +1,11 @@ import {ControlDirective} from './control_directive'; import {ControlGroupDirective} from './control_group_directive'; +import {Control} from '../model'; export interface FormDirective { addControl(dir: ControlDirective): void; removeControl(dir: ControlDirective): void; + getControl(dir: ControlDirective): Control; addControlGroup(dir: ControlGroupDirective): void; removeControlGroup(dir: ControlGroupDirective): void; updateModel(dir: ControlDirective, value: any): void; diff --git a/modules/angular2/src/forms/directives/form_model_directive.ts b/modules/angular2/src/forms/directives/form_model_directive.ts index 651209779e..c604a5eb8c 100644 --- a/modules/angular2/src/forms/directives/form_model_directive.ts +++ b/modules/angular2/src/forms/directives/form_model_directive.ts @@ -81,6 +81,8 @@ export class FormModelDirective extends ControlContainerDirective implements For ListWrapper.push(this.directives, dir); } + getControl(dir: ControlDirective): Control { return this.form.find(dir.path); } + removeControl(dir: ControlDirective): void { ListWrapper.remove(this.directives, dir); } addControlGroup(dir: ControlGroupDirective) {} diff --git a/modules/angular2/src/forms/directives/select_control_value_accessor.ts b/modules/angular2/src/forms/directives/select_control_value_accessor.ts index 35242e9df9..e424b5ebb1 100644 --- a/modules/angular2/src/forms/directives/select_control_value_accessor.ts +++ b/modules/angular2/src/forms/directives/select_control_value_accessor.ts @@ -1,5 +1,4 @@ -import {ElementRef, Directive} from 'angular2/angular2'; -import {Renderer} from 'angular2/src/render/api'; +import {Directive} from 'angular2/angular2'; import {ControlDirective} from './control_directive'; import {ControlValueAccessor} from './control_value_accessor'; @@ -23,24 +22,29 @@ import {ControlValueAccessor} from './control_value_accessor'; 'input': 'onChange($event.target.value)', 'blur': 'onTouched()' }, - hostProperties: {'value': 'value'} + hostProperties: { + 'value': 'value', + 'cd.control?.untouched == true': 'class.ng-untouched', + 'cd.control?.touched == true': 'class.ng-touched', + 'cd.control?.pristine == true': 'class.ng-pristine', + 'cd.control?.dirty == true': 'class.ng-dirty', + 'cd.control?.valid == true': 'class.ng-valid', + 'cd.control?.valid == false': 'class.ng-invalid' + } }) export class SelectControlValueAccessor implements ControlValueAccessor { value = null; onChange: Function; onTouched: Function; - constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { + constructor(private cd: ControlDirective) { this.onChange = (_) => {}; this.onTouched = (_) => {}; this.value = ''; cd.valueAccessor = this; } - writeValue(value) { - this._renderer.setElementProperty(this._elementRef.parentView.render, - this._elementRef.boundElementIndex, 'value', value) - } + writeValue(value) { this.value = value; } registerOnChange(fn): void { this.onChange = fn; } registerOnTouched(fn): void { this.onTouched = fn; } diff --git a/modules/angular2/src/forms/directives/template_driven_form_directive.ts b/modules/angular2/src/forms/directives/template_driven_form_directive.ts index a13fe54372..7ca98c8028 100644 --- a/modules/angular2/src/forms/directives/template_driven_form_directive.ts +++ b/modules/angular2/src/forms/directives/template_driven_form_directive.ts @@ -43,6 +43,8 @@ export class TemplateDrivenFormDirective extends ControlContainerDirective imple }); } + getControl(dir: ControlDirective): Control { return this.form.find(dir.path); } + removeControl(dir: ControlDirective): void { this._later(_ => { var c = this._findContainer(dir.path); diff --git a/modules/angular2/test/forms/directives_spec.ts b/modules/angular2/test/forms/directives_spec.ts index d1422a665b..3aae715160 100644 --- a/modules/angular2/test/forms/directives_spec.ts +++ b/modules/angular2/test/forms/directives_spec.ts @@ -172,7 +172,7 @@ export function main() { controlDir.valueAccessor = new DummyControlValueAccessor(); control = new Control(null); - controlDir.control = control; + controlDir.form = control; }); it("should set up validator", () => { diff --git a/modules/angular2/test/forms/integration_spec.ts b/modules/angular2/test/forms/integration_spec.ts index 82290406a1..9c0c99a91e 100644 --- a/modules/angular2/test/forms/integration_spec.ts +++ b/modules/angular2/test/forms/integration_spec.ts @@ -17,6 +17,7 @@ import { xit } from 'angular2/test_lib'; +import {DOM} from 'angular2/src/dom/dom_adapter'; import {TestBed} from 'angular2/src/test_lib/test_bed'; import {NgIf} from 'angular2/directives'; @@ -561,6 +562,7 @@ export function main() { .then((view) => { view.detectChanges(); tick(); + view.detectChanges(); var input = view.querySelector("input"); expect(input.value).toEqual("oldValue"); @@ -599,6 +601,105 @@ export function main() { flushMicrotasks(); }))); }); + + + describe("setting status classes", () => { + it("should work with single fields", + inject([TestBed], fakeAsync(tb => { + var form = new Control("", Validators.required); + var ctx = MyComp.create({form: form}); + + var t = `
`; + + tb.createView(MyComp, {context: ctx, html: t}) + .then((view) => { + view.detectChanges(); + + var input = view.querySelector("input"); + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-untouched", "ng-pristine", "ng-invalid"]); + + dispatchEvent(input, "blur"); + view.detectChanges(); + + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-pristine", "ng-invalid", "ng-touched"]); + + input.value = "updatedValue"; + dispatchEvent(input, "change"); + view.detectChanges(); + + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-touched", "ng-dirty", "ng-valid"]); + tick(); + }); + flushMicrotasks(); + }))); + + it("should work with complex model-driven forms", + inject([TestBed], fakeAsync(tb => { + var form = new ControlGroup({"name": new Control("", Validators.required)}); + var ctx = MyComp.create({form: form}); + + var t = + `
`; + + tb.createView(MyComp, {context: ctx, html: t}) + .then((view) => { + view.detectChanges(); + + var input = view.querySelector("input"); + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-untouched", "ng-pristine", "ng-invalid"]); + + dispatchEvent(input, "blur"); + view.detectChanges(); + + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-pristine", "ng-invalid", "ng-touched"]); + + input.value = "updatedValue"; + dispatchEvent(input, "change"); + view.detectChanges(); + + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-touched", "ng-dirty", "ng-valid"]); + tick(); + }); + flushMicrotasks(); + }))); + + it("should work with ng-model", + inject([TestBed], fakeAsync(tb => { + var ctx = MyComp.create({name: ""}); + + var t = `
`; + + tb.createView(MyComp, {context: ctx, html: t}) + .then((view) => { + view.detectChanges(); + + var input = view.querySelector("input"); + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-untouched", "ng-pristine", "ng-invalid"]); + + dispatchEvent(input, "blur"); + view.detectChanges(); + + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-pristine", "ng-invalid", "ng-touched"]); + + input.value = "updatedValue"; + dispatchEvent(input, "change"); + view.detectChanges(); + + expect(DOM.classList(input)) + .toEqual(["ng-binding", "ng-touched", "ng-dirty", "ng-valid"]); + tick(); + }); + flushMicrotasks(); + }))); + }); }); } diff --git a/modules/angular2/test/forms/model_spec.ts b/modules/angular2/test/forms/model_spec.ts index 54b24ddf08..ea3deb17c0 100644 --- a/modules/angular2/test/forms/model_spec.ts +++ b/modules/angular2/test/forms/model_spec.ts @@ -203,16 +203,14 @@ export function main() { }); describe("dirty", () => { - var c,g; + var c, g; beforeEach(() => { c = new Control('value'); g = new ControlGroup({"one": c}); }); - it("should be false after creating a control", () => { - expect(g.dirty).toEqual(false); - }); + it("should be false after creating a control", () => { expect(g.dirty).toEqual(false); }); it("should be false after changing the value of the control", () => { c.markAsDirty(); @@ -442,16 +440,14 @@ export function main() { }); describe("dirty", () => { - var c,a; + var c, a; beforeEach(() => { c = new Control('value'); a = new ControlArray([c]); }); - it("should be false after creating a control", () => { - expect(a.dirty).toEqual(false); - }); + it("should be false after creating a control", () => { expect(a.dirty).toEqual(false); }); it("should be false after changing the value of the control", () => { c.markAsDirty();