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) {
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);

View File

@ -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; }

View File

@ -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<string> { return null; }
get control(): Control { return null; }
constructor() { this.validator = Validators.nullValidator; }
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 {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<string> { return controlPath(this.name, this._parent); }
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 {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; }
}

View File

@ -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<string> { return []; }
viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); }
}

View File

@ -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;

View File

@ -81,6 +81,8 @@ export class FormModelDirective extends ControlContainerDirective implements For
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); }
addControlGroup(dir: ControlGroupDirective) {}

View File

@ -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; }

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 {
this._later(_ => {
var c = this._findContainer(dir.path);

View File

@ -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", () => {

View File

@ -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 = `<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", () => {
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();