Added support for binding to multiple observable objects (properties).

This commit is contained in:
Nedyalko Nikolov
2015-08-25 15:48:25 +03:00
parent 5ef2fea8d8
commit a57d52a304
4 changed files with 396 additions and 72 deletions

View File

@ -634,3 +634,257 @@ export function test_UpdatingNestedPropertyViaBinding() {
TKUnit.assertEqual(parentViewModel.get("name"), expectedValue2);
TKUnit.assertEqual(testElement2.get("targetProperty"), expectedValue2);
}
class Person extends observable.Observable {
private _firstName: string;
private _lastName: string;
public get FirstName(): string {
return this._firstName;
}
public set FirstName(value: string) {
if (this._firstName !== value) {
this._firstName = value;
this.notifyPropertyChange("FirstName", value);
}
}
public get LastName(): string {
return this._lastName;
}
public set LastName(value: string) {
if (this._lastName !== value) {
this._lastName = value;
this.notifyPropertyChange("LastName", value);
}
}
}
class Activity extends observable.Observable {
private _text: string;
private _owner: Person;
public get Text(): string {
return this._text;
}
public set Text(value: string) {
if (this._text !== value) {
this._text = value;
this.notifyPropertyChange("Text", value);
}
}
public get Owner(): Person {
return this._owner;
}
public set Owner(value: Person) {
if (this._owner !== value) {
this._owner = value;
this.notifyPropertyChange("Owner", value);
}
}
constructor(text: string, firstName: string, lastName: string) {
super();
this._text = text;
var owner = new Person();
owner.FirstName = firstName;
owner.LastName = lastName;
this.Owner = owner;
}
}
export function test_NestedPropertiesBinding() {
var expectedValue = "Default Text";
var viewModel = new observable.Observable();
viewModel.set("activity", new Activity(expectedValue, "Default First Name", "Default Last Name"));
var target1 = new bindable.Bindable();
target1.bind({
sourceProperty: "activity.Text",
targetProperty: "targetProperty",
twoWay: true
}, viewModel);
TKUnit.assertEqual(target1.get("targetProperty"), expectedValue);
var newExpectedValue = "Alabala";
var act = new Activity(newExpectedValue, "Default First Name", "Default Last Name");
viewModel.set("activity", act);
TKUnit.assertEqual(target1.get("targetProperty"), newExpectedValue);
}
export function test_NestedPropertiesBindingTwoTargets() {
var expectedText = "Default Text";
var expectedFirstName = "Default First Name";
var expectedLastName = "Default Last Name";
var viewModel = new observable.Observable();
viewModel.set("activity", new Activity(expectedText, expectedFirstName, expectedLastName));
var target1 = new bindable.Bindable();
target1.bind({
sourceProperty: "activity.Text",
targetProperty: "targetProperty",
twoWay: true
}, viewModel);
var target2 = new bindable.Bindable();
target2.bind({
sourceProperty: "activity.Owner.FirstName",
targetProperty: "targetProp",
twoWay: true
}, viewModel);
TKUnit.assertEqual(target1.get("targetProperty"), expectedText);
TKUnit.assertEqual(target2.get("targetProp"), expectedFirstName);
var newExpectedText = "Alabala";
var newExpectedFirstName = "First Tralala";
var newExpectedLastName = "Last Tralala";
var act = new Activity(newExpectedText, newExpectedFirstName, newExpectedLastName);
viewModel.set("activity", act);
TKUnit.assertEqual(target1.get("targetProperty"), newExpectedText);
TKUnit.assertEqual(target2.get("targetProp"), newExpectedFirstName);
}
export function test_NestedPropertiesBindingTwoTargetsAndSecondChange() {
var expectedText = "Default Text";
var expectedFirstName = "Default First Name";
var expectedLastName = "Default Last Name";
var viewModel = new observable.Observable();
viewModel.set("activity", new Activity(expectedText, expectedFirstName, expectedLastName));
var target1 = new bindable.Bindable();
target1.bind({
sourceProperty: "activity.Text",
targetProperty: "targetProperty",
twoWay: true
}, viewModel);
var target2 = new bindable.Bindable();
target2.bind({
sourceProperty: "activity.Owner.FirstName",
targetProperty: "targetProp",
twoWay: true
}, viewModel);
TKUnit.assertEqual(target1.get("targetProperty"), expectedText);
TKUnit.assertEqual(target2.get("targetProp"), expectedFirstName);
var newExpectedText = "Alabala";
var newExpectedFirstName = "First Tralala";
var newExpectedLastName = "Last Tralala";
var act = new Activity(newExpectedText, newExpectedFirstName, newExpectedLastName);
viewModel.set("activity", act);
TKUnit.assertEqual(target1.get("targetProperty"), newExpectedText);
TKUnit.assertEqual(target2.get("targetProp"), newExpectedFirstName);
var secondExpectedText = "Second expected text";
var secondExpectedFirstName = "Second expected first name";
var secondExpectedLastName = "Second expected last name";
var act1 = new Activity(secondExpectedText, secondExpectedFirstName, secondExpectedLastName);
viewModel.set("activity", act1);
TKUnit.assertEqual(target1.get("targetProperty"), secondExpectedText);
TKUnit.assertEqual(target2.get("targetProp"), secondExpectedFirstName);
}
export function test_NestedPropertiesBindingTwoTargetsAndRegularChange() {
var expectedText = "Default Text";
var expectedFirstName = "Default First Name";
var expectedLastName = "Default Last Name";
var viewModel = new observable.Observable();
viewModel.set("activity", new Activity(expectedText, expectedFirstName, expectedLastName));
var target1 = new bindable.Bindable();
target1.bind({
sourceProperty: "activity.Text",
targetProperty: "targetProperty",
twoWay: true
}, viewModel);
var target2 = new bindable.Bindable();
target2.bind({
sourceProperty: "activity.Owner.FirstName",
targetProperty: "targetProp",
twoWay: true
}, viewModel);
TKUnit.assertEqual(target1.get("targetProperty"), expectedText);
TKUnit.assertEqual(target2.get("targetProp"), expectedFirstName);
var newExpectedText = "Alabala";
var newExpectedFirstName = "First Tralala";
var newExpectedLastName = "Last Tralala";
var act = new Activity(newExpectedText, newExpectedFirstName, newExpectedLastName);
viewModel.set("activity", act);
TKUnit.assertEqual(target1.get("targetProperty"), newExpectedText);
TKUnit.assertEqual(target2.get("targetProp"), newExpectedFirstName);
var newAct = viewModel.get("activity");
var secondExpectedText = "Second expected text";
newAct.Text = secondExpectedText;
var secondExpectedFirstName = "Second expected First Name";
newAct.Owner.FirstName = secondExpectedFirstName;
TKUnit.assertEqual(target1.get("targetProperty"), secondExpectedText);
TKUnit.assertEqual(target2.get("targetProp"), secondExpectedFirstName);
}
export function test_NestedPropertiesBindingTwoTargetsAndReplacingSomeNestedObject() {
var expectedText = "Default Text";
var expectedFirstName = "Default First Name";
var expectedLastName = "Default Last Name";
var viewModel = new observable.Observable();
viewModel.set("activity", new Activity(expectedText, expectedFirstName, expectedLastName));
var target1 = new bindable.Bindable();
target1.bind({
sourceProperty: "activity.Text",
targetProperty: "targetProperty",
twoWay: true
}, viewModel);
var target2 = new bindable.Bindable();
target2.bind({
sourceProperty: "activity.Owner.FirstName",
targetProperty: "targetProp",
twoWay: true
}, viewModel);
TKUnit.assertEqual(target1.get("targetProperty"), expectedText);
TKUnit.assertEqual(target2.get("targetProp"), expectedFirstName);
var newExpectedText = "Alabala";
var newExpectedFirstName = "First Tralala";
var newExpectedLastName = "Last Tralala";
var act = new Activity(newExpectedText, newExpectedFirstName, newExpectedLastName);
viewModel.set("activity", act);
TKUnit.assertEqual(target1.get("targetProperty"), newExpectedText);
TKUnit.assertEqual(target2.get("targetProp"), newExpectedFirstName);
var secondExpectedFirstName = "Second expected first name";
var newPerson = new Person();
newPerson.FirstName = secondExpectedFirstName;
newPerson.LastName = "Last Name";
var act1 = viewModel.get("activity");
(<Activity>act1).Owner = newPerson;
TKUnit.assertEqual(target2.get("targetProp"), secondExpectedFirstName);
}

View File

@ -99,6 +99,11 @@ declare module "data/observable" {
*/
notify(data: EventData): void;
/**
* Notifies all the registered listeners for the property change event.
*/
notifyPropertyChange(propertyName: string, newValue: any): void;
/**
* Checks whether a listener is registered for the specified event name.
* @param eventName The name of the event to check for.

View File

@ -91,6 +91,10 @@ export class Observable implements definition.Observable {
}
}
public notifyPropertyChange(propertyName: string, newValue: any) {
this.notify(this._createPropertyChangeData(propertyName, newValue));
}
public set(name: string, value: any) {
// TODO: Parameter validation
if (this[name] === value) {
@ -122,8 +126,8 @@ export class Observable implements definition.Observable {
var i;
var entry: ListenerEntry;
for (i = 0; i < observers.length; i++) {
var observersLength = observers.length;
for (i = 0; i < observersLength; i++) {
entry = observers[i];
if (entry.thisArg) {
entry.callback.apply(entry.thisArg, [data]);

View File

@ -133,9 +133,13 @@ export class Binding {
source: WeakRef<Object>;
target: WeakRef<Bindable>;
private propertyChangeListeners = {};
private sourceOptions: { instance: WeakRef<any>; property: any };
private targetOptions: { instance: WeakRef<any>; property: any };
private sourcePropertiesArray: Array<string>;
constructor(target: Bindable, options: definition.BindingOptions) {
this.target = new WeakRef(target);
this.options = options;
@ -160,20 +164,70 @@ export class Binding {
}
/* tslint:enable */
this.source = new WeakRef(obj);
this.updateTarget(this.getSourceProperty());
this.updateTarget(this.getSourcePropertyValue());
if (!this.sourceOptions) {
this.sourceOptions = this.resolveOptions(this.source, this.options.sourceProperty);
this.sourceOptions = this.resolveOptions(this.source, this.getSourceProperties());
}
if (this.sourceOptions) {
var sourceOptionsInstance = this.sourceOptions.instance.get();
if (sourceOptionsInstance instanceof observable.Observable) {
this.addPropertyChangeListeners(this.source, this.getSourceProperties());
}
private getSourceProperties(): Array<string> {
if (!this.sourcePropertiesArray) {
this.sourcePropertiesArray = Binding.getProperties(this.options.sourceProperty);
}
return this.sourcePropertiesArray;
}
private static getProperties(property: string) {
return property.split(".");
}
private resolveObjectsAndProperties(source: Object, propsArray: Array<string>) {
var result = [];
var i;
var propsArrayLength = propsArray.length;
var currentObject = source;
var objProp = "";
var currentObjectChanged = false;
for (i = 0; i < propsArrayLength; i++) {
objProp = propsArray[i];
if (propsArray[i] === bc.bindingValueKey) {
currentObjectChanged = true;
}
if (propsArray[i] === bc.parentValueKey || propsArray[i].indexOf(bc.parentsValueKey) === 0) {
var parentView = this.getParentView(this.target.get(), propsArray[i]).view;
if (parentView) {
currentObject = parentView.bindingContext;
}
currentObjectChanged = true;
}
result.push({ instance: currentObject, property: objProp });
if (!currentObjectChanged) {
currentObject = currentObject[propsArray[i]];
currentObjectChanged = false;
}
}
return result;
}
private addPropertyChangeListeners(source: WeakRef<Object>, sourceProperty: Array<string>) {
var objectsAndProperties = this.resolveObjectsAndProperties(source.get(), sourceProperty)
var objectsAndPropertiesLength = objectsAndProperties.length;
if (objectsAndPropertiesLength > 0) {
var i;
for (i = 0; i < objectsAndPropertiesLength; i++) {
var prop = objectsAndProperties[i].property;
var currentObject = objectsAndProperties[i].instance;
if (!this.propertyChangeListeners[prop] && currentObject instanceof observable.Observable) {
weakEvents.addWeakEventListener(
sourceOptionsInstance,
currentObject,
observable.Observable.propertyChangeEvent,
this.onSourcePropertyChanged,
this);
this.propertyChangeListeners[prop] = currentObject;
}
}
}
}
@ -183,14 +237,15 @@ export class Binding {
return;
}
if (this.sourceOptions) {
var sourceOptionsInstance = this.sourceOptions.instance.get();
if (sourceOptionsInstance) {
weakEvents.removeWeakEventListener(sourceOptionsInstance,
var i;
var propertyChangeListenersKeys = Object.keys(this.propertyChangeListeners);
for (i = 0; i < propertyChangeListenersKeys.length; i++) {
weakEvents.removeWeakEventListener(
this.propertyChangeListeners[propertyChangeListenersKeys[i]],
observable.Observable.propertyChangeEvent,
this.onSourcePropertyChanged,
this);
}
delete this.propertyChangeListeners[propertyChangeListenersKeys[i]];
}
if (this.source) {
@ -203,6 +258,7 @@ export class Binding {
if (this.targetOptions) {
this.targetOptions = undefined;
}
this.sourcePropertiesArray = undefined;
}
private prepareExpressionForUpdate(): string {
@ -281,7 +337,6 @@ export class Binding {
public onSourcePropertyChanged(data: observable.PropertyChangeData) {
if (this.options.expression) {
//this.prepareContextForExpression(this.source.get(), this.options.expression);
var expressionValue = this._getExpressionValue(this.options.expression, false, undefined);
if (expressionValue instanceof Error) {
trace.write((<Error>expressionValue).message, trace.categories.Binding, trace.messageType.error);
@ -289,10 +344,49 @@ export class Binding {
else {
this.updateTarget(expressionValue);
}
} else if (data.propertyName === this.sourceOptions.property) {
} else {
var propIndex = this.getSourceProperties().indexOf(data.propertyName);
if (propIndex > -1) {
var props = this.getSourceProperties().slice(propIndex + 1);
var propsLength = props.length;
if (propsLength > 0) {
var value = data.value;
var i;
for (i = 0; i < propsLength; i++) {
value = value[props[i]];
}
this.updateTarget(value);
}
else if (data.propertyName === this.sourceOptions.property) {
this.updateTarget(data.value);
}
}
}
var sourceProps = Binding.getProperties(this.options.sourceProperty);
var sourcePropsLength = sourceProps.length;
var changedPropertyIndex = sourceProps.indexOf(data.propertyName);
if (changedPropertyIndex > -1) {
var probablyChangedObject = this.propertyChangeListeners[sourceProps[changedPropertyIndex + 1]];
if (probablyChangedObject &&
probablyChangedObject !== data.object[sourceProps[changedPropertyIndex]]) {
// remove all weakevent listeners after change, because changed object replaces object that is hooked for
// propertyChange event
for (i = sourcePropsLength - 1; i > changedPropertyIndex; i--) {
weakEvents.removeWeakEventListener(
this.propertyChangeListeners[sourceProps[i]],
observable.Observable.propertyChangeEvent,
this.onSourcePropertyChanged,
this);
delete this.propertyChangeListeners[sourceProps[i]];
}
//var newProps = this.options.sourceProperty.substr(this.options.sourceProperty.indexOf(data.propertyName) + data.propertyName.length + 1);
var newProps = sourceProps.slice(changedPropertyIndex + 1);
// add new weakevent listeners
this.addPropertyChangeListeners(new WeakRef(data.object[sourceProps[changedPropertyIndex]]), newProps);
}
}
}
private prepareContextForExpression(model, expression) {
var parentViewAndIndex;
@ -317,11 +411,10 @@ export class Binding {
}
}
private getSourceProperty() {
private getSourcePropertyValue() {
if (this.options.expression) {
var changedModel = {};
changedModel[bc.bindingValueKey] = this.source.get();
//this.prepareContextForExpression(this.source.get(), this.options.expression);
var expressionValue = this._getExpressionValue(this.options.expression, false, changedModel);
if (expressionValue instanceof Error) {
trace.write((<Error>expressionValue).message, trace.categories.Binding, trace.messageType.error);
@ -332,7 +425,7 @@ export class Binding {
}
if (!this.sourceOptions) {
this.sourceOptions = this.resolveOptions(this.source, this.options.sourceProperty);
this.sourceOptions = this.resolveOptions(this.source, this.getSourceProperties());
}
var value;
@ -359,7 +452,7 @@ export class Binding {
}
if (!this.targetOptions) {
this.targetOptions = this.resolveOptions(this.target, this.options.targetProperty);
this.targetOptions = this.resolveOptions(this.target, Binding.getProperties(this.options.targetProperty));
}
this.updateOptions(this.targetOptions, value);
@ -371,7 +464,7 @@ export class Binding {
}
if (!this.sourceOptions) {
this.sourceOptions = this.resolveOptions(this.source, this.options.sourceProperty);
this.sourceOptions = this.resolveOptions(this.source, this.getSourceProperties());
}
this.updateOptions(this.sourceOptions, value);
@ -412,53 +505,21 @@ export class Binding {
return { view: result, index: index };
}
private resolveOptions(obj: WeakRef<any>, property: string): { instance: any; property: any } {
var options;
if (property === bc.bindingValueKey) {
options = {
instance: obj,
property: property
};
return options;
}
if (types.isString(property) && property.indexOf(".") !== -1) {
var properties = property.split(".");
var i: number;
var currentObject = obj.get();
for (i = 0; i < properties.length - 1; i++) {
if (properties[i] === bc.bindingValueKey) {
continue;
}
if (properties[i] === bc.parentValueKey || properties[i].indexOf(bc.parentsValueKey) === 0) {
var parentView = this.getParentView(this.target.get(), properties[i]).view;
if (parentView) {
currentObject = parentView.bindingContext;
}
continue;
}
currentObject = currentObject[properties[i]];
}
if (!types.isNullOrUndefined(currentObject)) {
options = {
instance: new WeakRef(currentObject),
property: properties[properties.length - 1]
private resolveOptions(obj: WeakRef<any>, properties: Array<string>): { instance: any; property: any } {
var objectsAndProperties = this.resolveObjectsAndProperties(obj.get(), properties);
if (objectsAndProperties.length > 0) {
var resolvedObj = objectsAndProperties[objectsAndProperties.length - 1].instance;
var prop = objectsAndProperties[objectsAndProperties.length - 1].property;
return {
instance: new WeakRef(resolvedObj),
property: prop
}
}
} else {
options = {
instance: obj,
property: property
else {
return null;
}
}
return options;
}
private updateOptions(options: { instance: WeakRef<any>; property: any }, value: any) {
var optionsInstance;
if (options && options.instance) {