From cdb1e822162a2f36d3a9a5f36edd56e9edeefe68 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 3 Feb 2015 07:27:09 -0800 Subject: [PATCH] feat(forms): initial implementation of forms --- karma-dart.conf.js | 3 +- modules/core/core.js | 1 + modules/facade/src/dom.dart | 2 + modules/facade/src/dom.es6 | 3 + modules/forms/forms.js | 2 + modules/forms/pubspec.yaml | 14 +++ modules/forms/src/decorators.js | 73 ++++++++++++++ modules/forms/src/model.js | 25 +++++ modules/forms/test/integration_spec.js | 134 +++++++++++++++++++++++++ modules/forms/test/model_spec.js | 21 ++++ modules/test_lib/src/test_lib.dart | 4 - modules/test_lib/src/test_lib.es6 | 4 - modules/test_lib/src/utils.js | 20 ++++ 13 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 modules/forms/forms.js create mode 100644 modules/forms/pubspec.yaml create mode 100644 modules/forms/src/decorators.js create mode 100644 modules/forms/src/model.js create mode 100644 modules/forms/test/integration_spec.js create mode 100644 modules/forms/test/model_spec.js diff --git a/karma-dart.conf.js b/karma-dart.conf.js index ec47c1aa66..ecb758cb36 100644 --- a/karma-dart.conf.js +++ b/karma-dart.conf.js @@ -49,7 +49,8 @@ module.exports = function(config) { '/packages/di': 'http://localhost:9877/base/modules/di', '/packages/directives': 'http://localhost:9877/base/modules/directives', '/packages/facade': 'http://localhost:9877/base/modules/facade', - '/packages/test_lib': 'http://localhost:9877/base/modules/test_lib', + '/packages/forms': 'http://localhost:9877/base/modules/forms', + '/packages/test_lib': 'http://localhost:9877/base/modules/test_lib' }, preprocessors: { diff --git a/modules/core/core.js b/modules/core/core.js index 827a4c4c5a..d2a16431ba 100644 --- a/modules/core/core.js +++ b/modules/core/core.js @@ -1,4 +1,5 @@ export * from './src/annotations/annotations'; +export * from './src/annotations/visibility'; export * from './src/compiler/interfaces'; export * from './src/annotations/template_config'; diff --git a/modules/facade/src/dom.dart b/modules/facade/src/dom.dart index 4dd1c03644..b8607afa6f 100644 --- a/modules/facade/src/dom.dart +++ b/modules/facade/src/dom.dart @@ -41,6 +41,8 @@ class DOM { } static MouseEvent createMouseEvent(String eventType) => new MouseEvent(eventType, canBubble: true); + static createEvent(eventType) => + new Event(eventType, canBubble: true); static String getInnerHTML(Element el) => el.innerHtml; static String getOuterHTML(Element el) => el.outerHtml; static void setInnerHTML(Element el, String value) { diff --git a/modules/facade/src/dom.es6 b/modules/facade/src/dom.es6 index b5452612a0..134dc2ff1c 100644 --- a/modules/facade/src/dom.es6 +++ b/modules/facade/src/dom.es6 @@ -32,6 +32,9 @@ export class DOM { evt.initEvent(eventType, true, true); return evt; } + static createEvent(eventType) { + return new Event(eventType, true); + } static getInnerHTML(el) { return el.innerHTML; } diff --git a/modules/forms/forms.js b/modules/forms/forms.js new file mode 100644 index 0000000000..0b49600a6a --- /dev/null +++ b/modules/forms/forms.js @@ -0,0 +1,2 @@ +export * from './src/model'; +export * from './src/decorators'; diff --git a/modules/forms/pubspec.yaml b/modules/forms/pubspec.yaml new file mode 100644 index 0000000000..3f3070733f --- /dev/null +++ b/modules/forms/pubspec.yaml @@ -0,0 +1,14 @@ +name: forms +environment: + sdk: '>=1.4.0' +dependencies: + core: + path: ../core + di: + path: ../di + facade: + path: ../facade +dev_dependencies: + test_lib: + path: ../test_lib + guinness: ">=0.1.16 <0.2.0" diff --git a/modules/forms/src/decorators.js b/modules/forms/src/decorators.js new file mode 100644 index 0000000000..7f36e31c32 --- /dev/null +++ b/modules/forms/src/decorators.js @@ -0,0 +1,73 @@ +import {Decorator, NgElement, Ancestor} from 'core/core'; +import {DOM} from 'facade/src/dom'; +import {isPresent} from 'facade/src/lang'; +import {ListWrapper} from 'facade/src/collection'; +import {ControlGroup, Control} from './model'; + +@Decorator({ + selector: '[control-name]', + bind: { + 'control-name' : 'controlName' + } +}) +export class ControlDecorator { + _groupDecorator:ControlGroupDecorator; + _el:NgElement; + _controlName:String; + + constructor(@Ancestor() groupDecorator:ControlGroupDecorator, el:NgElement) { + this._groupDecorator = groupDecorator; + groupDecorator.addControlDecorator(this); + + this._el = el; + DOM.on(el.domElement, "change", (_) => this._updateControl()); + } + + set controlName(name:string) { + this._controlName = name; + this._updateDOM(); + } + + _updateDOM() { + // remove it once all DOM write go throuh a queue + if (isPresent(this._controlName)) { + this._el.domElement.value = this._control().value; + } + } + + _updateControl() { + this._control().value = this._el.domElement.value; + } + + _control() { + return this._groupDecorator.findControl(this._controlName); + } +} + +@Decorator({ + selector: '[control-group]', + bind: { + 'control-group' : 'controlGroup' + } +}) +export class ControlGroupDecorator { + _controlGroup:ControlGroup; + _controlDecorators:List; + + constructor() { + this._controlDecorators = ListWrapper.create(); + } + + set controlGroup(controlGroup:ControlGroup) { + this._controlGroup = controlGroup; + ListWrapper.forEach(this._controlDecorators, (cd) => cd._updateDOM()); + } + + addControlDecorator(c:ControlDecorator) { + ListWrapper.push(this._controlDecorators, c); + } + + findControl(name:string):Control { + return this._controlGroup.controls[name]; + } +} diff --git a/modules/forms/src/model.js b/modules/forms/src/model.js new file mode 100644 index 0000000000..2f3acfcfec --- /dev/null +++ b/modules/forms/src/model.js @@ -0,0 +1,25 @@ +import {StringMapWrapper} from 'facade/src/collection'; + +export class Control { + value:any; + + constructor(value:any) { + this.value = value; + } +} + +export class ControlGroup { + controls; + + constructor(controls) { + this.controls = controls; + } + + get value() { + var res = {}; + StringMapWrapper.forEach(this.controls, (control, name) => { + res[name] = control.value; + }); + return res; + } +} diff --git a/modules/forms/test/integration_spec.js b/modules/forms/test/integration_spec.js new file mode 100644 index 0000000000..6f0a2c35c4 --- /dev/null +++ b/modules/forms/test/integration_spec.js @@ -0,0 +1,134 @@ +import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, + el, queryView, dispatchEvent} from 'test_lib/test_lib'; + +import {Lexer, Parser, ChangeDetector, dynamicChangeDetection} from 'change_detection/change_detection'; +import {Compiler, CompilerCache} from 'core/src/compiler/compiler'; +import {DirectiveMetadataReader} from 'core/src/compiler/directive_metadata_reader'; +import {Injector} from 'di/di'; +import {DOM} from 'facade/src/dom'; + +import {Component, TemplateConfig} from 'core/core'; +import {ControlDecorator, ControlGroupDecorator, Control, ControlGroup} from 'forms/forms'; + +export function main() { + function detectChanges(view) { + view.changeDetector.detectChanges(); + } + + function compile(componentType, template, context, callback) { + var compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), + new Parser(new Lexer()), new CompilerCache()); + + compiler.compile(componentType, el(template)).then((pv) => { + var view = pv.instantiate(null); + view.hydrate(new Injector([]), null, context); + detectChanges(view); + callback(view); + }); + } + + var compiler; + + beforeEach(() => { + compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), + new Parser(new Lexer()), new CompilerCache()); + }); + + 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 = `
+ +
`; + + compile(MyComp, t, ctx, (view) => { + var input = queryView(view, "input") + expect(input.value).toEqual("loginValue"); + done(); + }); + }); + + it("should update the control group values on DOM change", (done) => { + var form = new ControlGroup({ + "login": new Control("oldValue") + }); + var ctx = new MyComp(form); + + var t = `
+ +
`; + + compile(MyComp, t, ctx, (view) => { + var input = queryView(view, "input") + + input.value = "updatedValue"; + dispatchEvent(input, "change"); + + expect(form.value).toEqual({"login": "updatedValue"}); + done(); + }); + }); + + it("should update DOM elements when rebinding the control group", (done) => { + var form = new ControlGroup({ + "login": new Control("oldValue") + }); + var ctx = new MyComp(form); + + var t = `
+ +
`; + + compile(MyComp, t, ctx, (view) => { + ctx.form = new ControlGroup({ + "login": new Control("newValue") + }); + detectChanges(view); + + var input = queryView(view, "input") + expect(input.value).toEqual("newValue"); + done(); + }); + }); + + it("should update DOM element when rebinding the control name", (done) => { + var ctx = new MyComp(new ControlGroup({ + "one": new Control("one"), + "two": new Control("two") + }), "one"); + + var t = `
+ +
`; + + compile(MyComp, t, ctx, (view) => { + var input = queryView(view, "input") + expect(input.value).toEqual("one"); + + ctx.name = "two"; + detectChanges(view); + + expect(input.value).toEqual("two"); + done(); + }); + }); + }); +} + +@Component({ + template: new TemplateConfig({ + directives: [ControlGroupDecorator, ControlDecorator] + }) +}) +class MyComp { + form:ControlGroup; + name:string; + + constructor(form, name = null) { + this.form = form; + this.name = name; + } +} \ No newline at end of file diff --git a/modules/forms/test/model_spec.js b/modules/forms/test/model_spec.js new file mode 100644 index 0000000000..ae283347e7 --- /dev/null +++ b/modules/forms/test/model_spec.js @@ -0,0 +1,21 @@ +import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el} from 'test_lib/test_lib'; +import {ControlGroup, Control} from 'forms/forms'; + +export function main() { + describe("ControlGroup", () => { + describe("value", () => { + it("should be the reduced value of the child controls", () => { + var g = new ControlGroup({ + "one": new Control("111"), + "two": new Control("222") + }); + expect(g.value).toEqual({"one": "111", "two": "222"}) + }); + + it("should be empty when there are no child controls", () => { + var g = new ControlGroup({}); + expect(g.value).toEqual({}) + }); + }); + }); +} \ No newline at end of file diff --git a/modules/test_lib/src/test_lib.dart b/modules/test_lib/src/test_lib.dart index c9f353fece..986480ebd8 100644 --- a/modules/test_lib/src/test_lib.dart +++ b/modules/test_lib/src/test_lib.dart @@ -69,8 +69,4 @@ _handleAsync(fn) { } return fn; -} - -el(String html) { - return DOM.createTemplate(html).content.firstChild; } \ No newline at end of file diff --git a/modules/test_lib/src/test_lib.es6 b/modules/test_lib/src/test_lib.es6 index 74081eeb05..976ab59447 100644 --- a/modules/test_lib/src/test_lib.es6 +++ b/modules/test_lib/src/test_lib.es6 @@ -161,8 +161,4 @@ function elementText(n) { if (hasNodes(n)) return elementText(DOM.childNodesAsList(n)); return n.textContent; -} - -export function el(html) { - return DOM.createTemplate(html).content.firstChild; } \ No newline at end of file diff --git a/modules/test_lib/src/utils.js b/modules/test_lib/src/utils.js index 9348e8a1e7..8a0fc2ee6c 100644 --- a/modules/test_lib/src/utils.js +++ b/modules/test_lib/src/utils.js @@ -1,4 +1,6 @@ import {List, ListWrapper} from 'facade/src/collection'; +import {DOM} from 'facade/src/dom'; +import {isPresent} from 'facade/src/lang'; export class Log { _result:List; @@ -21,3 +23,21 @@ export class Log { return ListWrapper.join(this._result, "; "); } } + +export function queryView(view, selector) { + for (var i = 0; i < view.nodes.length; ++i) { + var res = DOM.querySelector(view.nodes[i], selector); + if (isPresent(res)) { + return res; + } + } + return null; +} + +export function dispatchEvent(element, eventType) { + DOM.dispatchEvent(element, DOM.createEvent(eventType)); +} + +export function el(html) { + return DOM.firstChild(DOM.createTemplate(html).content); +}