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