feat(forms): added support for status classes

This commit is contained in:
vsavkin 2015-06-03 11:56:01 -07:00
parent 96cadcc29e
commit 3baf815d76
13 changed files with 167 additions and 38 deletions

View File

@ -105,7 +105,10 @@ class BindingRecordsCreator {
if (directiveRecord.callOnCheck) { if (directiveRecord.callOnCheck) {
ListWrapper.push(bindings, BindingRecord.createDirectiveOnCheck(directiveRecord)); ListWrapper.push(bindings, BindingRecord.createDirectiveOnCheck(directiveRecord));
} }
}
for (var i = 0; i < directiveBinders.length; i++) {
var directiveBinder = directiveBinders[i];
// host properties // host properties
MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => { MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => {
var dirIndex = new DirectiveIndex(boundElementIndex, i); var dirIndex = new DirectiveIndex(boundElementIndex, i);

View File

@ -1,5 +1,4 @@
import {ElementRef, Directive} from 'angular2/angular2'; import {Directive} from 'angular2/angular2';
import {Renderer} from 'angular2/src/render/api';
import {ControlDirective} from './control_directive'; import {ControlDirective} from './control_directive';
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
@ -18,23 +17,28 @@ import {ControlValueAccessor} from './control_value_accessor';
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)', 'blur': 'onTouched()'}, 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 { export class CheckboxControlValueAccessor implements ControlValueAccessor {
checked: boolean; checked: boolean;
onChange: Function; onChange: Function;
onTouched: Function; onTouched: Function;
constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { constructor(private cd: ControlDirective) {
this.onChange = (_) => {}; this.onChange = (_) => {};
this.onTouched = (_) => {}; this.onTouched = (_) => {};
cd.valueAccessor = this; cd.valueAccessor = this;
} }
writeValue(value) { writeValue(value) { this.checked = value; }
this._renderer.setElementProperty(this._elementRef.parentView.render,
this._elementRef.boundElementIndex, 'checked', value)
}
registerOnChange(fn): void { this.onChange = fn; } registerOnChange(fn): void { this.onChange = fn; }
registerOnTouched(fn): void { this.onTouched = fn; } registerOnTouched(fn): void { this.onTouched = fn; }

View File

@ -1,5 +1,6 @@
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
import {Validators} from '../validators'; import {Validators} from '../validators';
import {Control} from '../model';
/** /**
* A directive that bind a [ng-control] object to a DOM element. * A directive that bind a [ng-control] object to a DOM element.
@ -12,6 +13,7 @@ export class ControlDirective {
validator: Function; validator: Function;
get path(): List<string> { return null; } get path(): List<string> { return null; }
get control(): Control { return null; }
constructor() { this.validator = Validators.nullValidator; } constructor() { this.validator = Validators.nullValidator; }
viewToModelUpdate(newValue: any): void {} viewToModelUpdate(newValue: any): void {}

View File

@ -7,6 +7,7 @@ import {FORWARD_REF, Binding, Inject} from 'angular2/di';
import {ControlContainerDirective} from './control_container_directive'; import {ControlContainerDirective} from './control_container_directive';
import {ControlDirective} from './control_directive'; import {ControlDirective} from './control_directive';
import {controlPath} from './shared'; import {controlPath} from './shared';
import {Control} from '../model';
const controlNameBinding = const controlNameBinding =
CONST_EXPR(new Binding(ControlDirective, {toAlias: FORWARD_REF(() => ControlNameDirective)})); CONST_EXPR(new Binding(ControlDirective, {toAlias: FORWARD_REF(() => ControlNameDirective)}));
@ -84,6 +85,7 @@ export class ControlNameDirective extends ControlDirective {
} }
} }
onDestroy() { this.formDirective.removeControl(this); } onDestroy() { this.formDirective.removeControl(this); }
viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); } viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); }
@ -91,4 +93,6 @@ export class ControlNameDirective extends ControlDirective {
get path(): List<string> { return controlPath(this.name, this._parent); } get path(): List<string> { return controlPath(this.name, this._parent); }
get formDirective(): any { return this._parent.formDirective; } get formDirective(): any { return this._parent.formDirective; }
get control(): Control { return this.formDirective.getControl(this); }
} }

View File

@ -1,5 +1,4 @@
import {ElementRef, Directive} from 'angular2/angular2'; import {Directive} from 'angular2/angular2';
import {Renderer} from 'angular2/src/render/api';
import {ControlDirective} from './control_directive'; import {ControlDirective} from './control_directive';
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
@ -24,24 +23,30 @@ import {ControlValueAccessor} from './control_value_accessor';
'input': 'onChange($event.target.value)', 'input': 'onChange($event.target.value)',
'blur': 'onTouched()' '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 { export class DefaultValueAccessor implements ControlValueAccessor {
value = null; value = null;
onChange: Function; onChange: Function;
onTouched: Function; onTouched: Function;
constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { constructor(private cd: ControlDirective) {
this.onChange = (_) => {}; this.onChange = (_) => {};
this.onTouched = (_) => {}; this.onTouched = (_) => {};
cd.valueAccessor = this; cd.valueAccessor = this;
} }
writeValue(value) { writeValue(value) { this.value = value; }
this._renderer.setElementProperty(this._elementRef.parentView.render,
this._elementRef.boundElementIndex, 'value', value)
}
registerOnChange(fn): void { this.onChange = fn; } registerOnChange(fn): void { this.onChange = fn; }
registerOnTouched(fn): void { this.onTouched = fn; } registerOnTouched(fn): void { this.onTouched = fn; }
} }

View File

@ -47,12 +47,12 @@ const formControlBinding =
@Directive({ @Directive({
selector: '[ng-form-control]', selector: '[ng-form-control]',
hostInjector: [formControlBinding], hostInjector: [formControlBinding],
properties: ['control: ng-form-control', 'model: ng-model'], properties: ['form: ng-form-control', 'model: ng-model'],
events: ['ngModel'], events: ['ngModel'],
lifecycle: [onChange] lifecycle: [onChange]
}) })
export class FormControlDirective extends ControlDirective { export class FormControlDirective extends ControlDirective {
control: Control; form: Control;
ngModel: EventEmitter; ngModel: EventEmitter;
_added: boolean; _added: boolean;
model: any; model: any;
@ -65,14 +65,18 @@ export class FormControlDirective extends ControlDirective {
onChange(c) { onChange(c) {
if (!this._added) { if (!this._added) {
setUpControl(this.control, this); setUpControl(this.form, this);
this.control.updateValidity(); this.form.updateValidity();
this._added = true; this._added = true;
} }
if (StringMapWrapper.contains(c, "model")) { if (StringMapWrapper.contains(c, "model")) {
this.control.updateValue(this.model); this.form.updateValue(this.model);
} }
} }
get control(): Control { return this.form; }
get path(): List<string> { return []; }
viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); } viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); }
} }

View File

@ -1,9 +1,11 @@
import {ControlDirective} from './control_directive'; import {ControlDirective} from './control_directive';
import {ControlGroupDirective} from './control_group_directive'; import {ControlGroupDirective} from './control_group_directive';
import {Control} from '../model';
export interface FormDirective { export interface FormDirective {
addControl(dir: ControlDirective): void; addControl(dir: ControlDirective): void;
removeControl(dir: ControlDirective): void; removeControl(dir: ControlDirective): void;
getControl(dir: ControlDirective): Control;
addControlGroup(dir: ControlGroupDirective): void; addControlGroup(dir: ControlGroupDirective): void;
removeControlGroup(dir: ControlGroupDirective): void; removeControlGroup(dir: ControlGroupDirective): void;
updateModel(dir: ControlDirective, value: any): void; updateModel(dir: ControlDirective, value: any): void;

View File

@ -81,6 +81,8 @@ export class FormModelDirective extends ControlContainerDirective implements For
ListWrapper.push(this.directives, dir); ListWrapper.push(this.directives, dir);
} }
getControl(dir: ControlDirective): Control { return <Control>this.form.find(dir.path); }
removeControl(dir: ControlDirective): void { ListWrapper.remove(this.directives, dir); } removeControl(dir: ControlDirective): void { ListWrapper.remove(this.directives, dir); }
addControlGroup(dir: ControlGroupDirective) {} addControlGroup(dir: ControlGroupDirective) {}

View File

@ -1,5 +1,4 @@
import {ElementRef, Directive} from 'angular2/angular2'; import {Directive} from 'angular2/angular2';
import {Renderer} from 'angular2/src/render/api';
import {ControlDirective} from './control_directive'; import {ControlDirective} from './control_directive';
import {ControlValueAccessor} from './control_value_accessor'; import {ControlValueAccessor} from './control_value_accessor';
@ -23,24 +22,29 @@ import {ControlValueAccessor} from './control_value_accessor';
'input': 'onChange($event.target.value)', 'input': 'onChange($event.target.value)',
'blur': 'onTouched()' '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 { export class SelectControlValueAccessor implements ControlValueAccessor {
value = null; value = null;
onChange: Function; onChange: Function;
onTouched: Function; onTouched: Function;
constructor(cd: ControlDirective, private _elementRef: ElementRef, private _renderer: Renderer) { constructor(private cd: ControlDirective) {
this.onChange = (_) => {}; this.onChange = (_) => {};
this.onTouched = (_) => {}; this.onTouched = (_) => {};
this.value = ''; this.value = '';
cd.valueAccessor = this; cd.valueAccessor = this;
} }
writeValue(value) { writeValue(value) { this.value = value; }
this._renderer.setElementProperty(this._elementRef.parentView.render,
this._elementRef.boundElementIndex, 'value', value)
}
registerOnChange(fn): void { this.onChange = fn; } registerOnChange(fn): void { this.onChange = fn; }
registerOnTouched(fn): void { this.onTouched = fn; } registerOnTouched(fn): void { this.onTouched = fn; }

View File

@ -43,6 +43,8 @@ export class TemplateDrivenFormDirective extends ControlContainerDirective imple
}); });
} }
getControl(dir: ControlDirective): Control { return <Control>this.form.find(dir.path); }
removeControl(dir: ControlDirective): void { removeControl(dir: ControlDirective): void {
this._later(_ => { this._later(_ => {
var c = this._findContainer(dir.path); var c = this._findContainer(dir.path);

View File

@ -172,7 +172,7 @@ export function main() {
controlDir.valueAccessor = new DummyControlValueAccessor(); controlDir.valueAccessor = new DummyControlValueAccessor();
control = new Control(null); control = new Control(null);
controlDir.control = control; controlDir.form = control;
}); });
it("should set up validator", () => { it("should set up validator", () => {

View File

@ -17,6 +17,7 @@ import {
xit xit
} from 'angular2/test_lib'; } from 'angular2/test_lib';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {TestBed} from 'angular2/src/test_lib/test_bed'; import {TestBed} from 'angular2/src/test_lib/test_bed';
import {NgIf} from 'angular2/directives'; import {NgIf} from 'angular2/directives';
@ -561,6 +562,7 @@ export function main() {
.then((view) => { .then((view) => {
view.detectChanges(); view.detectChanges();
tick(); tick();
view.detectChanges();
var input = view.querySelector("input"); var input = view.querySelector("input");
expect(input.value).toEqual("oldValue"); expect(input.value).toEqual("oldValue");
@ -599,6 +601,105 @@ export function main() {
flushMicrotasks(); 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 = `<div><input type="text" [ng-form-control]="form"></div>`;
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 =
`<form [ng-form-model]="form"><input type="text" ng-control="name"></form>`;
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 = `<div><input [(ng-model)]="name" required></div>`;
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();
})));
});
}); });
} }

View File

@ -203,16 +203,14 @@ export function main() {
}); });
describe("dirty", () => { describe("dirty", () => {
var c,g; var c, g;
beforeEach(() => { beforeEach(() => {
c = new Control('value'); c = new Control('value');
g = new ControlGroup({"one": c}); g = new ControlGroup({"one": c});
}); });
it("should be false after creating a control", () => { it("should be false after creating a control", () => { expect(g.dirty).toEqual(false); });
expect(g.dirty).toEqual(false);
});
it("should be false after changing the value of the control", () => { it("should be false after changing the value of the control", () => {
c.markAsDirty(); c.markAsDirty();
@ -442,16 +440,14 @@ export function main() {
}); });
describe("dirty", () => { describe("dirty", () => {
var c,a; var c, a;
beforeEach(() => { beforeEach(() => {
c = new Control('value'); c = new Control('value');
a = new ControlArray([c]); a = new ControlArray([c]);
}); });
it("should be false after creating a control", () => { it("should be false after creating a control", () => { expect(a.dirty).toEqual(false); });
expect(a.dirty).toEqual(false);
});
it("should be false after changing the value of the control", () => { it("should be false after changing the value of the control", () => {
c.markAsDirty(); c.markAsDirty();