Initial implementation of Observable + Bindable + Label

This commit is contained in:
atanasovg
2014-06-05 13:53:09 +03:00
parent 3b944e1ba5
commit 1be0f92429
19 changed files with 359 additions and 58 deletions

View File

@@ -152,15 +152,31 @@
<TypeScriptCompile Include="text\text.ios.ts">
<DependentUpon>text.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\core\bindable.ts" />
<TypeScriptCompile Include="ui\core\index.ts" />
<TypeScriptCompile Include="ui\core\proxy.ts" />
<TypeScriptCompile Include="ui\text-view\text-view.android.ts">
<DependentUpon>text-view.d.ts</DependentUpon>
<TypeScriptCompile Include="ui\core\view.android.ts">
<DependentUpon>view.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-view\text-view.common.ts" />
<TypeScriptCompile Include="ui\text-view\text-view.d.ts" />
<TypeScriptCompile Include="ui\text-view\text-view.ios.ts">
<DependentUpon>text-view.d.ts</DependentUpon>
<TypeScriptCompile Include="ui\core\view.ios.ts">
<DependentUpon>view.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\label\index.ts" />
<TypeScriptCompile Include="ui\label\label.d.ts" />
<TypeScriptCompile Include="ui\label\label.android.ts">
<DependentUpon>label.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\label\label.ios.ts">
<DependentUpon>label.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-input\index.ts" />
<TypeScriptCompile Include="ui\text-input\text-input.android.ts">
<DependentUpon>text-input.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-input\text-input.ios.ts">
<DependentUpon>text-input.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-input\text-input.d.ts" />
<TypeScriptCompile Include="utils\module-merge.ts" />
<TypeScriptCompile Include="utils\utils_android.ts" />
<TypeScriptCompile Include="utils\utils_ios.ts" />
@@ -223,7 +239,7 @@
<TypeScriptCompile Include="libjs.d.ts" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="ui\core\view.ts" />
<TypeScriptCompile Include="ui\core\view.d.ts" />
<TypeScriptCompile Include="ui\core\observable.ts" />
<Content Include="_references.ts" />
<Content Include="image-source\Readme.md" />

View File

@@ -20,7 +20,7 @@ var dir = context.getFilesDir();
// ```
// Tracking the current Activity
// ``` JavaScript
if (androidApp.currentActivity === androidApp.startActivity) {
if (androidApp.foregroundActivity === androidApp.foregroundActivity) {
//// We are currently in the main (start) activity of the application
}
// ```
@@ -29,7 +29,7 @@ if (androidApp.currentActivity === androidApp.startActivity) {
export var testAndroidApplicationInitialized = function () {
TKUnit.assert(app.android, "Android application not initialized.");
TKUnit.assert(app.android.context, "Android context not initialized.");
TKUnit.assert(app.android.currentActivity, "Android currentActivity not initialized.");
TKUnit.assert(app.android.foregroundActivity, "Android currentActivity not initialized.");
TKUnit.assert(app.android.startActivity, "Android startActivity not initialized.");
TKUnit.assert(app.android.nativeApp, "Android nativeApp not initialized.");
TKUnit.assert(app.android.packageName, "Android packageName not initialized.");

View File

@@ -1,4 +1,5 @@
import appModule = require("application/application-common");
import dts = require("application");
// merge the exports of the application_common file with the exports of this file
declare var exports;
@@ -21,11 +22,17 @@ var initEvents = function () {
androidApp.onActivityCreated(activity, bundle);
}
}
androidApp.currentContext = activity;
},
onActivityDestroyed: function (activity: any) {
// Clear the current activity reference to prevent leak
if (activity === androidApp.currentActivity) {
androidApp.currentActivity = undefined;
if (activity === androidApp.foregroundActivity) {
androidApp.foregroundActivity = undefined;
}
if (activity === androidApp.currentContext) {
androidApp.currentContext = undefined;
}
if (activity === androidApp.startActivity) {
@@ -42,7 +49,7 @@ var initEvents = function () {
gc();
},
onActivityPaused: function (activity: any) {
if (activity === androidApp.currentActivity) {
if (activity === androidApp.foregroundActivity) {
if (exports.onSuspend) {
exports.onSuspend();
}
@@ -53,7 +60,7 @@ var initEvents = function () {
}
},
onActivityResumed: function (activity: any) {
if (activity === androidApp.currentActivity) {
if (activity === androidApp.foregroundActivity) {
if (exports.onResume) {
exports.onResume();
}
@@ -69,7 +76,7 @@ var initEvents = function () {
}
},
onActivityStarted: function (activity: any) {
androidApp.currentActivity = activity;
androidApp.foregroundActivity = activity;
if (androidApp.onActivityStarted) {
androidApp.onActivityStarted(activity);
@@ -98,12 +105,14 @@ export var init = function (nativeApp: android.app.Application) {
initialized = true;
}
class AndroidApplication {
class AndroidApplication implements dts.AndroidApplication {
public nativeApp: android.app.Application;
public context: android.content.Context;
public currentActivity: android.app.Activity;
public currentContext: android.content.Context;
public foregroundActivity: android.app.Activity;
public startActivity: android.app.Activity;
public packageName: string;
public getActivity: (intent: android.content.Intent) => any;
public onActivityCreated: (activity: android.app.Activity, bundle: android.os.Bundle) => void;
public onActivityDestroyed: (activity: android.app.Activity) => void;
@@ -119,6 +128,7 @@ class AndroidApplication {
this.nativeApp = nativeApp;
this.packageName = nativeApp.getPackageName();
this.context = nativeApp.getApplicationContext();
this.getActivity = undefined;
}
public init() {

View File

@@ -64,7 +64,12 @@ declare module "application" {
/**
* The currently active (loaded) android.app.Activity. This property is automatically updated upon Activity events.
*/
currentActivity: android.app.Activity;
foregroundActivity: android.app.Activity;
/**
* The currently active (loaded) Context. This is typically the top-level Activity that is just created.
*/
currentContext: android.content.Context;
/**
* The main (start) Activity for the application.

115
ui/core/bindable.ts Normal file
View File

@@ -0,0 +1,115 @@
import observable = require("ui/core/observable");
export interface BindingOptions {
sourceProperty: string;
targetProperty: string;
twoWay?: boolean;
}
export class Bindable extends observable.Observable {
private _bindings = {};
public bind(source: observable.Observable, options: BindingOptions) {
var binding: Binding = this._bindings[options.targetProperty];
if (binding) {
binding.unbind();
}
binding = new Binding(this, options);
this._bindings[options.targetProperty] = binding;
binding.bind(source);
}
public unbind(options: BindingOptions) {
var binding: Binding = this._bindings[options.targetProperty];
if (binding) {
binding.unbind();
delete this._bindings[options.targetProperty];
}
}
public updateTwoWayBinding(propertyName: string, value: any) {
var binding: Binding = this._bindings[propertyName];
if (binding) {
binding.updateTwoWay(value);
}
}
public getBinding(propertyName: string) {
return this._bindings[propertyName];
}
private clearBinding(binding: Binding) {
binding.unbind();
}
}
class Binding {
options: BindingOptions;
updating = false;
source: observable.Observable;
target: Bindable;
constructor(target: Bindable, options: BindingOptions) {
this.target = target;
this.options = options;
}
public bind(obj: observable.Observable) {
this.source = obj;
this.updateTarget(this.source.getProperty(this.options.sourceProperty));
var that = this;
var callback = function (data: observable.PropertyChangeData) {
that.onSourcePropertyChanged(data);
}
this.source.addObserver(observable.Observable.propertyChangeEvent, callback);
}
public unbind() {
if (!this.source) {
return;
}
this.source.removeObserver(observable.Observable.propertyChangeEvent, this.onSourcePropertyChanged);
this.source = undefined;
this.target = undefined;
}
public updateTwoWay(value: any) {
if (this.options.twoWay) {
this.updateSource(value);
}
}
public onSourcePropertyChanged(data: observable.PropertyChangeData) {
if (data.propertyName !== this.options.sourceProperty) {
return;
}
this.updateTarget(data.value);
}
private updateTarget(value: any) {
if (this.updating) {
return;
}
this.updating = true;
this.target.setProperty(this.options.targetProperty, value);
this.updating = false;
}
private updateSource(value: any) {
if (this.updating) {
return;
}
this.updating = true;
this.source.setProperty(this.options.sourceProperty, value);
this.updating = false;
}
}

1
ui/core/index.ts Normal file
View File

@@ -0,0 +1 @@


View File

@@ -11,8 +11,7 @@ export interface ChangeData {
export interface PropertyChangeData extends ChangeData {
propertyName: string;
oldValue: any;
newValue: any;
value: any;
cancel?: boolean;
}
@@ -23,33 +22,33 @@ export class Observable {
// true to track the Changing phase, false otherwise
private _trackChanging = false;
public bind(eventName: string, callback: (data: ChangeData) => void) {
public addObserver(eventName: string, callback: (data: ChangeData) => void) {
this.verifyCallback(callback);
var list = this.getEventList(eventName, true);
list.push(callback);
}
public setProperty(name: string, value: any) {
// TODO: Parameter validation
if (!(name in this._observers)) {
// no observers to notify for the PropertyChange event
public removeObserver(eventName: string, callback: any) {
var list = this.getEventList(eventName, false);
if (!list) {
return;
}
// create data for the change
var data: PropertyChangeData = {
eventName: Observable.propertyChangeEvent,
propertyName: name,
sender: this,
oldValue: this[name],
newValue: value,
cancel: false
};
var index = list.indexOf(callback);
if (index >= 0) {
list.splice(index, 1);
}
}
if (this._trackChanging) {
public setProperty(name: string, value: any) {
// TODO: Parameter validation
// create data for the change
var data = this.createPropertyChangeData(name, value);
if (this.hasObservers(Observable.propertyChangeEvent) && this._trackChanging) {
data.phase = ChangePhase.Changing;
this.notifyObservers(data);
this.notify(data);
if (data.cancel) {
// change is canceled by an observer
// TODO: define some priority, e.g. if someone cancels the change should others be able to override this cancelation?
@@ -58,9 +57,8 @@ export class Observable {
}
data.phase = ChangePhase.Changed;
this.notifyObservers(data);
this.setPropertyCore(data);
this.notify(data);
}
public getProperty(name: string): any {
@@ -71,8 +69,36 @@ export class Observable {
* This method is intended to be overriden by inheritors to specify additional
*/
public setPropertyCore(data: PropertyChangeData) {
// get the new value from the data since some observer may have it modified - e.g. validation scenario
this[data.eventName] = data.newValue;
this[data.propertyName] = data.value;
}
// The method will return true if the change is accepted, false otherwise
public notify(data: ChangeData) {
var observers = this.getEventList(data.eventName);
if (!observers) {
return;
}
var i,
callback;
for (i = 0; i < observers.length; i++) {
callback = observers[i];
callback(data);
}
}
public hasObservers(eventName: string) {
return eventName in this._observers;
}
public createPropertyChangeData(name: string, value: any): PropertyChangeData {
return {
eventName: Observable.propertyChangeEvent,
propertyName: name,
sender: this,
value: value,
cancel: false
};
}
private getEventList(eventName: string, createIfNeeded?: boolean): Array<(data: ChangeData) => void> {
@@ -94,19 +120,4 @@ export class Observable {
throw new TypeError("Callback must be a valid function.");
}
}
// The method will return true if the change is accepted, false otherwise
private notifyObservers(data: ChangeData) {
var observers = this.getEventList(data.eventName);
if (!observers) {
return;
}
var i,
callback;
for (i = 0; i < observers.length; i++) {
callback = observers[i];
callback(data);
}
}
}

View File

@@ -1,12 +1,16 @@
import observable = require("ui/core/observable");
import bindable = require("ui/core/bindable");
export class ProxyObject extends observable.Observable {
export class ProxyObject extends bindable.Bindable {
public setPropertyCore(data: observable.PropertyChangeData) {
super.setPropertyCore(data);
this.setNativeProperty(data);
}
public setNativeProperty(data: observable.PropertyChangeData) {
// TODO:
// inheritors will override this method to provide specific implementation
}
public onNativePropertyChanged(data: observable.PropertyChangeData) {
}
}

11
ui/core/view.android.ts Normal file
View File

@@ -0,0 +1,11 @@
import proxy = require("ui/core/proxy");
export class View extends proxy.ProxyObject {
public addToParent(parent: android.view.ViewGroup) {
var nativeInstance: android.view.View = this["android"];
if (nativeInstance) {
// TODO: Check for existing parent
parent.addView(nativeInstance);
}
}
}

5
ui/core/view.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import proxy = require("ui/core/proxy");
export declare class View extends proxy.ProxyObject {
addToParent: (parent: any) => void;
}

1
ui/core/view.ios.ts Normal file
View File

@@ -0,0 +1 @@


2
ui/label/index.ts Normal file
View File

@@ -0,0 +1,2 @@
declare var module, require;
module.exports = require("ui/label/label");

55
ui/label/label.android.ts Normal file
View File

@@ -0,0 +1,55 @@
import observable = require("ui/core/observable");
import view = require("ui/core/view");
import application = require("application");
export class Label extends view.View {
private static textProperty = "text";
private _android: android.widget.TextView;
constructor() {
super();
// TODO: Verify that this is always true
var context = application.android.currentContext;
if (!context) {
// TODO: Delayed loading?
}
this._android = new android.widget.TextView(context);
var that = this;
var textWatcher = new android.text.TextWatcher({
beforeTextChanged: function (text: string, start: number, count: number, after: number) {
},
onTextChanged: function (text: string, start: number, before: number, count: number) {
},
afterTextChanged: function (editable: android.text.IEditable) {
//if (that.hasObservers(observable.Observable.propertyChangeEvent)) {
// var data = that.createPropertyChangeData(TextView.textProperty, that.text);
// that.notify(data);
//}
that.updateTwoWayBinding("text", editable.toString());
}
});
this._android.addTextChangedListener(textWatcher);
}
get android(): android.widget.TextView {
return this._android;
}
get text(): string {
return this._android.getText().toString();
}
set text(value: string) {
this.setProperty(Label.textProperty, value);
}
public setNativeProperty(data: observable.PropertyChangeData) {
// TODO: Will this be a gigantic if-else switch?
if (data.propertyName === Label.textProperty) {
this._android.setText(data.value);
} else if (true) {
}
}
}

5
ui/label/label.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module "ui/label" {
class Label {
text: string;
}
}

5
ui/label/label.ios.ts Normal file
View File

@@ -0,0 +1,5 @@
import view = require("ui/core/view");
export class Label extends view.View {
}

2
ui/text-input/index.ts Normal file
View File

@@ -0,0 +1,2 @@
declare var module, require;
module.exports = require("ui/text-input/text-input");

View File

@@ -0,0 +1,51 @@
import observable = require("ui/core/observable");
import view = require("ui/core/view");
import application = require("application");
export class TextInput extends view.View {
private _android: android.widget.EditText;
private static textProperty = "text";
constructor() {
super();
var context = application.android.currentContext;
this._android = new android.widget.EditText(context);
// TODO: This code is same for Label, extract base class or derive from Label
var that = this;
var textWatcher = new android.text.TextWatcher({
beforeTextChanged: function (text: string, start: number, count: number, after: number) {
},
onTextChanged: function (text: string, start: number, before: number, count: number) {
},
afterTextChanged: function (editable: android.text.IEditable) {
//if (that.hasObservers(observable.Observable.propertyChangeEvent)) {
// var data = that.createPropertyChangeData(TextView.textProperty, that.text);
// that.notify(data);
//}
that.updateTwoWayBinding("text", editable.toString());
}
});
this._android.addTextChangedListener(textWatcher);
}
get android(): android.widget.EditText {
return this._android;
}
get text(): string {
return this._android.getText().toString();
}
set text(value: string) {
this.setProperty(TextInput.textProperty, value);
}
public setNativeProperty(data: observable.PropertyChangeData) {
// TODO: Will this be a gigantic if-else switch?
if (data.propertyName === TextInput.textProperty) {
this._android.setText(data.value, android.widget.TextView.BufferType.EDITABLE);
} else if (true) {
}
}
}

1
ui/text-input/text-input.d.ts vendored Normal file
View File

@@ -0,0 +1 @@


View File

@@ -0,0 +1 @@