diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj index 8471c20df..79200f9ac 100644 --- a/CrossPlatformModules.csproj +++ b/CrossPlatformModules.csproj @@ -94,6 +94,9 @@ main-page.xml + + binding_tests.xml + @@ -491,6 +494,7 @@ + @@ -1456,7 +1460,7 @@ False - + \ No newline at end of file diff --git a/apps/tests/app/binding_tests.ts b/apps/tests/app/binding_tests.ts new file mode 100644 index 000000000..1ac9cb323 --- /dev/null +++ b/apps/tests/app/binding_tests.ts @@ -0,0 +1,61 @@ +import pageModule = require("ui/page"); +//import stackLayoutModule = require("ui/layouts/stack-layout"); +//import textFieldModule = require("ui/text-field"); +import observableModule = require("data/observable"); +import bindableModule = require("ui/core/bindable"); +//import enums = require("ui/enums"); +import trace = require("trace"); +trace.setCategories(trace.categories.Test); +trace.enable(); + +export function pageLoaded(args: observableModule.EventData) { + var page: pageModule.Page = args.object; + var model = new observableModule.Observable(); + //var model = page.bindingContext; + model.set("paramProperty", "%%%"); + var toUpperConverter: bindableModule.ValueConverter = { + toModel: function (value, param1) { + return param1 + value.toLowerCase(); + }, + toView: function (value, param1) { + return value.toUpperCase(); + } + }; + model.set("toUpper", toUpperConverter); + model.set("testProperty", "Alabala"); + + page.bindingContext = model; +} + +//export function createPage() { +// var stackLayout = new stackLayoutModule.StackLayout(); +// var firstTextField = new textFieldModule.TextField(); +// firstTextField.updateTextTrigger = enums.UpdateTextTrigger.textChanged; +// var secondTextField = new textFieldModule.TextField(); +// secondTextField.updateTextTrigger = enums.UpdateTextTrigger.textChanged; + +// var model = new observableModule.Observable(); + +// var bindOptions: bindableModule.BindingOptions = { +// sourceProperty: "testProperty", +// targetProperty: "text", +// twoWay: true, +// expression: "testProperty | toUpper('$$$')" +// }; + +// firstTextField.bind(bindOptions, model); +// secondTextField.bind({ +// sourceProperty: "testProperty", +// targetProperty: "text", +// twoWay: true +// }, model); + +// stackLayout.addChild(firstTextField); +// stackLayout.addChild(secondTextField); + +// var page = new pageModule.Page(); +// page.on("loaded", pageLoaded); +// page.content = stackLayout; +// page.bindingContext = model; +// return page; +//} \ No newline at end of file diff --git a/apps/tests/app/binding_tests.xml b/apps/tests/app/binding_tests.xml new file mode 100644 index 000000000..66f6fd7d5 --- /dev/null +++ b/apps/tests/app/binding_tests.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/js-libs/polymer-expressions/polymer-expressions.d.ts b/js-libs/polymer-expressions/polymer-expressions.d.ts index 9b07524e1..a76dfbf3c 100644 --- a/js-libs/polymer-expressions/polymer-expressions.d.ts +++ b/js-libs/polymer-expressions/polymer-expressions.d.ts @@ -5,7 +5,13 @@ declare module "js-libs/polymer-expressions" { } class Expression { - getValue(model); + /** + * Evaluates a value for an expression. + * @param model - Context of the expression. + * @param isBackConvert - Denotes if the convertion is forward (from model to ui) or back (ui to model). + * @param changedModel - A property bag which contains all changed properties (in case of two way binding). + */ + getValue(model, isBackConvert, changedModel); } } diff --git a/js-libs/polymer-expressions/polymer-expressions.js b/js-libs/polymer-expressions/polymer-expressions.js index ae35e51e2..3c92c8606 100644 --- a/js-libs/polymer-expressions/polymer-expressions.js +++ b/js-libs/polymer-expressions/polymer-expressions.js @@ -53,10 +53,17 @@ var Path = require("js-libs/polymer-expressions/path-parser").Path; if (!this.valueFn_) { var name = this.name; var path = this.path; - this.valueFn_ = function (model, observer) { + this.valueFn_ = function (model, observer, changedModel) { if (observer) observer.addPath(model, path); + if (changedModel) { + var result = path.getValueFrom(changedModel); + if (result !== undefined) { + return result; + } + } + return path.getValueFrom(model); } } @@ -185,8 +192,8 @@ var Path = require("js-libs/polymer-expressions/path-parser").Path; // object. if (toModelDirection) { fn = fn.toModel; - } else if (typeof fn.toDOM == 'function') { - fn = fn.toDOM; + } else if (typeof fn.toView == 'function') { + fn = fn.toView; } if (typeof fn != 'function') { @@ -394,11 +401,10 @@ var Path = require("js-libs/polymer-expressions/path-parser").Path; } Expression.prototype = { - getValue: function (model, observer, filterRegistry) { - var value = getFn(this.expression)(model, observer, filterRegistry); + getValue: function (model, isBackConvert, changedModel, observer) { + var value = getFn(this.expression)(model, observer, changedModel); for (var i = 0; i < this.filters.length; i++) { - value = this.filters[i].transform(model, observer, filterRegistry, - false, [value]); + value = this.filters[i].transform(model, observer, model, isBackConvert, [value]); } return value; diff --git a/text/span-common.ts b/text/span-common.ts index 1e009cf96..f2acb5ca1 100644 --- a/text/span-common.ts +++ b/text/span-common.ts @@ -47,19 +47,24 @@ export class Span extends bindable.Bindable implements definition.Span, view.App } } + private _getColorValue(value: any): colorModule.Color { + var result; + if (types.isString(value) && (value).indexOf("#") === 0) { + result = new colorModule.Color(value); + } + else { + result = value; + } + return result; + } + get foregroundColor(): colorModule.Color { return this._foregroundColor; } set foregroundColor(value: colorModule.Color) { - var foreColor; - if (types.isString(value) && (value).indexOf("#") === 0) { - foreColor = new colorModule.Color(value); - } - else { - foreColor = value; - } - if (this._foregroundColor !== foreColor) { - this._foregroundColor = foreColor; + var convertedColor = this._getColorValue(value); + if (this._foregroundColor !== convertedColor) { + this._foregroundColor = convertedColor; this.updateAndNotify(); } } @@ -68,8 +73,9 @@ export class Span extends bindable.Bindable implements definition.Span, view.App return this._backgroundColor; } set backgroundColor(value: colorModule.Color) { - if (this._backgroundColor !== value) { - this._backgroundColor = value; + var convertedColor = this._getColorValue(value); + if (this._backgroundColor !== convertedColor) { + this._backgroundColor = convertedColor; this.updateAndNotify(); } } diff --git a/ui/builder/component-builder.ts b/ui/builder/component-builder.ts index 92f4eb51f..e50199b9f 100644 --- a/ui/builder/component-builder.ts +++ b/ui/builder/component-builder.ts @@ -77,7 +77,7 @@ export function getComponentModule(elementName: string, namespace: string, attri // Get the event handler from instance.bindingContext. var propertyChangeHandler = (args: observable.PropertyChangeData) => { if (args.propertyName === "bindingContext") { - var handler = instance.bindingContext && instance.bindingContext[getPropertyNameFromBinding(attrValue)]; + var handler = instance.bindingContext && instance.bindingContext[getBindingExpressionFromAttribute(attrValue)]; // Check if the handler is function and add it to the instance for specified event name. if (types.isFunction(handler)) { instance.on(attr, handler, instance.bindingContext); @@ -157,10 +157,10 @@ function isKnownEvent(name: string, exports: any): boolean { } function getBinding(instance: view.View, name: string, value: string): bindable.BindingOptions { - return { targetProperty: name, sourceProperty: getPropertyNameFromBinding(value), twoWay: true }; + return bindable.Bindable._getBindingOptions(name, getBindingExpressionFromAttribute(value)); } -function getPropertyNameFromBinding(value: string): string { +function getBindingExpressionFromAttribute(value: string): string { return value.replace("{{", "").replace("}}", "").trim(); } diff --git a/ui/core/bindable.d.ts b/ui/core/bindable.d.ts index 8a70303a1..87828ff89 100644 --- a/ui/core/bindable.d.ts +++ b/ui/core/bindable.d.ts @@ -17,6 +17,27 @@ * True to establish a two-way binding, false otherwise. A two-way binding will synchronize both the source and the target property values regardless of which one initiated the change. */ twoWay?: boolean; + /** + * An expression used for calculations (convertions) based on the value of the property. + */ + expression?: string; + } + + /** + * An interface which defines methods need to create binding value converter. + */ + export interface ValueConverter { + /** + * A method that will be executed when a value (of the binding property) should be converted to the observable model. + * For example: user types in a text field, but our business logic requires to store data in a different manner (e.g. in lower case). + * @param params - An array of parameters where first element is the value of the property and next elements are parameters send to converter. + */ + toModel: (...params: any[]) => any; + /** + * A method that will be executed when a value should be converted to the UI view. For example we have a date object which should be displayed to the end user in a specific date format. + * @param params - An array of parameters where first element is the value of the property and next elements are parameters send to converter. + */ + toView: (...params: any[]) => any; } /** @@ -45,6 +66,7 @@ unbind(property: string); //@private + static _getBindingOptions(name: string, bindingExpression: string): BindingOptions; _updateTwoWayBinding(propertyName: string, value: any); _onBindingContextChanged(oldValue: any, newValue: any); //@endprivate diff --git a/ui/core/bindable.ts b/ui/core/bindable.ts index e701e0995..755c0b602 100644 --- a/ui/core/bindable.ts +++ b/ui/core/bindable.ts @@ -6,6 +6,8 @@ import types = require("utils/types"); import trace = require("trace"); import polymerExpressions = require("js-libs/polymer-expressions"); +var expressionSymbolsRegex = /[ \+\-\*%\?:<>=!\|&\(\)\[\]]/; + var bindingContextProperty = new dependencyObservable.Property( "bindingContext", "Bindable", @@ -105,7 +107,7 @@ export class Bindable extends dependencyObservable.DependencyObservable implemen } trace.write( - "Binding target: " + binding.target.get() + + "Binding target: " + binding.target.get() + " targetProperty: " + binding.options.targetProperty + " to the changed context: " + newValue, trace.categories.Binding); binding.unbind(); @@ -114,6 +116,31 @@ export class Bindable extends dependencyObservable.DependencyObservable implemen } } } + + private static extractPropertyNameFromExpression(expression: string): string { + var firstExpressionSymbolIndex = expression.search(expressionSymbolsRegex); + if (firstExpressionSymbolIndex > -1) { + return expression.substr(0, firstExpressionSymbolIndex); + } + else { + return expression; + } + } + + public static _getBindingOptions(name: string, bindingExpression: string): definition.BindingOptions { + var result: definition.BindingOptions; + result = { + targetProperty: name, + sourceProperty: "" + }; + if (types.isString(bindingExpression)) { + var params = bindingExpression.split(","); + result.sourceProperty = Bindable.extractPropertyNameFromExpression(params[0]); + result.expression = params[1]; + result.twoWay = params[2] ? params[2].toLowerCase() === "true" : true; + } + return result; + } } export class Binding { @@ -142,16 +169,16 @@ export class Binding { if (typeof (obj) === "number") { obj = new Number(obj); } - + if (typeof (obj) === "boolean") { obj = new Boolean(obj); } - + if (typeof (obj) === "string") { obj = new String(obj); } /* tslint:enable */ - + this.source = new WeakRef(obj); this.updateTarget(this.getSourceProperty()); @@ -188,34 +215,52 @@ export class Binding { public updateTwoWay(value: any) { if (this.options.twoWay) { - this.updateSource(value); + if (this._isExpression(this.options.expression)) { + var changedModel = {}; + changedModel[this.options.sourceProperty] = value; + this.updateSource(this._getExpressionValue(this.options.expression, true, changedModel)); + } + else { + this.updateSource(value); + } } } private _isExpression(expression: string): boolean { - return expression.indexOf(" ") !== -1; + if (expression) { + var result = expression.indexOf(" ") !== -1; + return result; + } + else { + return false; + } } - private _getExpressionValue(expression: string): any { - var exp = polymerExpressions.PolymerExpressions.getExpression(expression); - if (exp) { - return exp.getValue(this.source && this.source.get && this.source.get() || global); + private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any { + try { + var exp = polymerExpressions.PolymerExpressions.getExpression(expression); + if (exp) { + var context = this.source && this.source.get && this.source.get() || global; + return exp.getValue(context, isBackConvert, changedModel); + } + return undefined; + } + catch (e) { + return undefined; } - - return undefined; } public onSourcePropertyChanged(data: observable.PropertyChangeData) { - if (this._isExpression(this.options.sourceProperty)) { - this.updateTarget(this._getExpressionValue(this.options.sourceProperty)); + if (this._isExpression(this.options.expression)) { + this.updateTarget(this._getExpressionValue(this.options.expression, false, undefined)); } else if (data.propertyName === this.options.sourceProperty) { this.updateTarget(data.value); } } private getSourceProperty() { - if (this._isExpression(this.options.sourceProperty)) { - return this._getExpressionValue(this.options.sourceProperty); + if (this._isExpression(this.options.expression)) { + return this._getExpressionValue(this.options.expression, false, undefined); } if (!this.sourceOptions) {