Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 91 additions & 35 deletions modules/angular2/src/forms/directives.js
Original file line number Diff line number Diff line change
@@ -1,81 +1,134 @@
import {TemplateConfig, Component, Decorator, NgElement, Ancestor} from 'angular2/core';
import {TemplateConfig, Component, Decorator, NgElement, Ancestor, onChange} from 'angular2/core';
import {DOM} from 'angular2/src/facade/dom';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang';
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {ControlGroup, Control} from './model';

class ControlGroupDirectiveBase {
addDirective(directive):void {}
findControl(name:string):Control { return null; }
}

@CONST()
export class ControlValueAccessor {
readValue(el){}
writeValue(el, value):void {}
}

@CONST()
class DefaultControlValueAccessor extends ControlValueAccessor {
constructor() {
super();
}

readValue(el) {
return el.value;
}

writeValue(el, value):void {
el.value = value;
}
}

@CONST()
class CheckboxControlValueAccessor extends ControlValueAccessor {
constructor() {
super();
}

readValue(el):boolean {
return el.checked;
}

writeValue(el, value:boolean):void {
el.checked = value;
}
}

var controlValueAccessors = {
"checkbox" : new CheckboxControlValueAccessor(),
"text" : new DefaultControlValueAccessor()
};

function controlValueAccessorFor(controlType:string):ControlValueAccessor {
var accessor = StringMapWrapper.get(controlValueAccessors, controlType);
if (isPresent(accessor)) {
return accessor;
} else {
return StringMapWrapper.get(controlValueAccessors, "text");
}
}


export class ControlDirectiveBase {
_groupDecorator:ControlGroupDirectiveBase;
_el:NgElement;
_controlName:string;

controlName:string;
type:string;
valueAccessor:ControlValueAccessor;

constructor(groupDecorator, el:NgElement) {
this._groupDecorator = groupDecorator;
this._el = el;
DOM.on(el.domElement, "change", (_) => this._updateControl());
}

set controlName(name:string) {
this._controlName = name;
_initialize() {
if (isBlank(this.valueAccessor)) {
this.valueAccessor = controlValueAccessorFor(this.type);
}
this._groupDecorator.addDirective(this);
this._updateDOM();
this._updateDomValue();
DOM.on(this._el.domElement, "change", (_) => this._updateControlValue());
}

get controlName() {
return this._controlName;
_updateDomValue() {
this.valueAccessor.writeValue(this._el.domElement, this._control().value);
}

//TODO:vsavkin: Remove it once change detection lifecycle callbacks are available
isInitialized():boolean {
return isPresent(this._controlName);
}

_updateDOM() {
// remove it once all DOM write go through a queue
if (this.isInitialized()) {
var inputElement:any = this._el.domElement;
inputElement.value = this._control().value;
}
}

_updateControl() {
var inputElement:any = this._el.domElement;
this._control().value = inputElement.value;
_updateControlValue() {
this._control().value = this.valueAccessor.readValue(this._el.domElement);
}

_control() {
return this._groupDecorator.findControl(this._controlName);
return this._groupDecorator.findControl(this.controlName);
}
}


@Decorator({
lifecycle: [onChange],
selector: '[control-name]',
bind: {
'control-name' : 'controlName'
'control-name' : 'controlName',
'type' : 'type'
}
})
export class ControlNameDirective extends ControlDirectiveBase {
constructor(@Ancestor() groupDecorator:ControlGroupDirective, el:NgElement) {
super(groupDecorator, el);
}

onChange(_) {
this._initialize();
}
}

@Decorator({
lifecycle: [onChange],
selector: '[control]',
bind: {
'control' : 'controlName'
'control' : 'controlName',
'type' : 'type'
}
})
export class ControlDirective extends ControlDirectiveBase {
constructor(@Ancestor() groupDecorator:NewControlGroupDirective, el:NgElement) {
super(groupDecorator, el);
}

onChange(_) {
this._initialize();
}
}

@Decorator({
Expand All @@ -95,7 +148,7 @@ export class ControlGroupDirective extends ControlGroupDirectiveBase {

set controlGroup(controlGroup:ControlGroup) {
this._controlGroup = controlGroup;
ListWrapper.forEach(this._directives, (cd) => cd._updateDOM());
ListWrapper.forEach(this._directives, (cd) => cd._updateDomValue());
}

addDirective(c:ControlNameDirective) {
Expand Down Expand Up @@ -144,10 +197,8 @@ export class NewControlGroupDirective extends ControlGroupDirectiveBase {

_createControlGroup():ControlGroup {
var controls = ListWrapper.reduce(this._directives, (memo, cd) => {
if (cd.isInitialized()) {
var initControlValue = this._initData[cd.controlName];
memo[cd.controlName] = new Control(initControlValue);
}
var initControlValue = this._initData[cd.controlName];
memo[cd.controlName] = new Control(initControlValue);
return memo;
}, {});
return new ControlGroup(controls);
Expand All @@ -157,3 +208,8 @@ export class NewControlGroupDirective extends ControlGroupDirectiveBase {
return this._controlGroup.value;
}
}

export var FormDirectives = [
ControlGroupDirective, ControlNameDirective,
ControlDirective, NewControlGroupDirective
];
92 changes: 74 additions & 18 deletions modules/angular2/test/forms/integration_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_str
import {Injector} from 'angular2/di';
import {DOM} from 'angular2/src/facade/dom';

import {Component, TemplateConfig} from 'angular2/core';
import {ControlDirective, ControlNameDirective, ControlGroupDirective, NewControlGroupDirective,
Control, ControlGroup} from 'angular2/forms';
import {Component, Decorator, TemplateConfig} from 'angular2/core';
import {ControlGroupDirective, ControlNameDirective,
ControlDirective, NewControlGroupDirective,
Control, ControlGroup, ControlValueAccessor} from 'angular2/forms';

import {TemplateLoader} from 'angular2/src/core/compiler/template_loader';
import {XHRMock} from 'angular2/src/mock/xhr_mock';
Expand All @@ -36,19 +37,14 @@ export function main() {
});
}

function formComponent(view) {
// TODO: vsavkin remove when view variables work
return view.elementInjectors[0].getComponent();
}

describe("integration tests", () => {
it("should initialize DOM elements with the given form object", (done) => {
var ctx = new MyComp(new ControlGroup({
"login": new Control("loginValue")
}));

var t = `<div [control-group]="form">
<input [control-name]="'login'">
<input type="text" control-name="login">
</div>`;

compile(MyComp, t, ctx, (view) => {
Expand All @@ -65,7 +61,7 @@ export function main() {
var ctx = new MyComp(form);

var t = `<div [control-group]="form">
<input [control-name]="'login'">
<input type="text" control-name="login">
</div>`;

compile(MyComp, t, ctx, (view) => {
Expand All @@ -86,7 +82,7 @@ export function main() {
var ctx = new MyComp(form);

var t = `<div [control-group]="form">
<input [control-name]="'login'">
<input type="text" control-name="login">
</div>`;

compile(MyComp, t, ctx, (view) => {
Expand All @@ -108,7 +104,7 @@ export function main() {
}), "one");

var t = `<div [control-group]="form">
<input [control-name]="name">
<input type="text" [control-name]="name">
</div>`;

compile(MyComp, t, ctx, (view) => {
Expand All @@ -123,11 +119,51 @@ export function main() {
});
});

describe("different control types", () => {
it("should support type=checkbox", (done) => {
var ctx = new MyComp(new ControlGroup({"checkbox": new Control(true)}));

var t = `<div [control-group]="form">
<input type="checkbox" control-name="checkbox">
</div>`;

compile(MyComp, t, ctx, (view) => {
var input = queryView(view, "input")
expect(input.checked).toBe(true);

input.checked = false;
dispatchEvent(input, "change");

expect(ctx.form.value).toEqual({"checkbox" : false});
done();
});
});

it("should support custom value accessors", (done) => {
var ctx = new MyComp(new ControlGroup({"name": new Control("aa")}));

var t = `<div [control-group]="form">
<input type="text" control-name="name" wrapped-value>
</div>`;

compile(MyComp, t, ctx, (view) => {
var input = queryView(view, "input")
expect(input.value).toEqual("!aa!");

input.value = "!bb!";
dispatchEvent(input, "change");

expect(ctx.form.value).toEqual({"name" : "bb"});
done();
});
});
});

describe("declarative forms", () => {
it("should initialize dom elements", (done) => {
var t = `<div [new-control-group]="{'login': 'loginValue', 'password':'passValue'}">
<input id="login" [control]="'login'">
<input id="password" [control]="'password'">
<input type="text" id="login" control="login">
<input type="password" id="password" control="password">
</div>`;

compile(MyComp, t, new MyComp(), (view) => {
Expand All @@ -142,8 +178,8 @@ export function main() {
});

it("should update the control group values on DOM change", (done) => {
var t = `<div [new-control-group]="{'login': 'loginValue'}">
<input [control]="'login'">
var t = `<div #form [new-control-group]="{'login': 'loginValue'}">
<input type="text" control="login">
</div>`;

compile(MyComp, t, new MyComp(), (view) => {
Expand All @@ -152,7 +188,8 @@ export function main() {
input.value = "updatedValue";
dispatchEvent(input, "change");

expect(formComponent(view).value).toEqual({'login': 'updatedValue'});
var form = view.contextWithLocals.get("form");
expect(form.value).toEqual({'login': 'updatedValue'});
done();
});
});
Expand All @@ -166,7 +203,7 @@ export function main() {
template: new TemplateConfig({
inline: "",
directives: [ControlGroupDirective, ControlNameDirective,
ControlDirective, NewControlGroupDirective]
ControlDirective, NewControlGroupDirective, WrappedValue]
})
})
class MyComp {
Expand All @@ -178,3 +215,22 @@ class MyComp {
this.name = name;
}
}

class WrappedValueAccessor extends ControlValueAccessor {
readValue(el){
return el.value.substring(1, el.value.length - 1);
}

writeValue(el, value):void {
el.value = `!${value}!`;
}
}

@Decorator({
selector:'[wrapped-value]'
})
class WrappedValue {
constructor(cd:ControlNameDirective) {
cd.valueAccessor = new WrappedValueAccessor();
}
}