web-atoms-mvvm
Version:
MVVM, REST Json Service, Message Subscriptions for Web Atoms
539 lines (441 loc) • 13.4 kB
text/typescript
/// <reference path="__di.ts" />
/**
* Atom helper class
* @class Atom
*/
declare class Atom {
static pageQuery: { [key:string]: any };
/**
* Set this true to return mock in RestServices
* @static
* @type {boolean}
* @memberof Atom
*/
static designMode: boolean;
/**
* Set this true to return mock in test mode
* @static
* @type {boolean}
* @memberof Atom
*/
static testMode: boolean;
/**
* Refreshes bindings for specified property of the target
* @static
* @param {*} target
* @param {string} property
* @memberof Atom
*/
static refresh(target:any, property: string):void;
/**
* Display given error message to user, this must be set by
* the app developer
* @static
* @param {string} msg
* @memberof Atom
*/
static showError(msg: string): void;
/**
* [Obsolete] do not use, this will retrieve value at given path for target
* @static
* @param {*} target
* @param {string} path
* @returns {*}
* @memberof Atom
*/
static get(target: any, path: string): any;
/**
* [Obsolete] do not use, this will set value at given path for target
* @static
* @param {*} target
* @param {string} path
* @param {*} value
* @memberof Atom
*/
static set(target: any, path: string, value: any): void;
/**
* Schedules given call in next available callLater slot
* @static
* @param {()=>void} f
* @memberof Atom
*/
static post (f:()=>void): void;
/**
* Schedules given call in next available callLater slot and also returns
* promise that can be awaited, calling `Atom.postAsync` inside `Atom.postAsync`
* will create deadlock
* @static
* @param {()=>Promise<any>} f
* @returns {Promise<any>}
* @memberof Atom
*/
static postAsync(f:()=>Promise<any>): Promise<any>;
/**
* Invokes given function and disposes given object after execution
* @static
* @param {WebAtoms.AtomDisposable} d
* @param {()=>void} f
* @memberof Atom
*/
static using(d:WebAtoms.AtomDisposable, f:()=>void): void;
/**
* Invokes given function and disposes given object after execution asynchronously
* @static
* @param {WebAtoms.AtomDisposable} d
* @param {()=>Promise<any>} f
* @returns {Promise<any>}
* @memberof Atom
*/
static usingAsync(d:WebAtoms.AtomDisposable, f:()=>Promise<any>): Promise<any>;
/**
* Sets up watch and returns disposable to destroy watch
* @static
* @param {*} item
* @param {string} property
* @param {()=>void} f
* @returns {WebAtoms.AtomDisposable}
* @memberof Atom
*/
static watch(item:any, property:string, f:()=>void):WebAtoms.AtomDisposable;
/**
* await for delay for given number of milliseconds
* @static
* @param {number} n
* @param {WebAtoms.CancelToken} [ct]
* @returns {Promise<any>}
* @memberof Atom
*/
static delay(n:number, ct?:WebAtoms.CancelToken): Promise<any>;
/**
* Version
* @static
* @type {{
* text: string,
* major: number,
* minor: number,
* build: number
* }}
* @memberof Atom
*/
static version: {
text: string,
major: number,
minor: number,
build: number
};
/**
* Current time in milliseconds
* @static
* @returns {number}
* @memberof Atom
*/
static time():number;
/**
* Combine and prepare given url from fragments
* @static
* @param {string} url
* @param {*} queryString
* @param {*} hash
* @returns {string}
* @memberof Atom
*/
static url(url: string, queryString:any, hash?:any): string;
/**
* Creates secure version of the given url with fragments
* @static
* @param {string} url
* @param {...string[]} padding
* @returns {string}
* @memberof Atom
*/
static secureUrl(url: string, ... padding: string[]): string;
/**
* Creates bindable proxy for given object
* @static
* @param {*} e
* @returns {*}
* @memberof Atom
*/
static bindable<T>(e:T):T;
/**
* Creates an element enclosing given control name or class
*
* @static
* @param {((string | WebAtoms.AtomControlType))} name
* @param {string} tagName
* @returns {HTMLElement}
* @memberof Atom
*/
static controlToElement(name:(string | WebAtoms.AtomControlType),tagName?:string):HTMLElement;
}
declare class AtomDate {
static zoneOffsetMinutes: number;
static zoneOffset: number;
static toLocalTime(d:Date): string;
static setTime(d:Date, time: string): Date;
static toMMDDYY(d:Date): string;
static toShortDateString(d:Date | string): string;
static toDateTimeString(d:Date | string): string;
static toTimeString(d:Date | string): string;
static smartDate(d:Date | string): string;
static smartDateUTC(d:Date | string): string;
static jsonDate(d: Date | string): {
Year: number, Month: number, Date: number, Hours: number, Minutes: number, Seconds: number, Offset: number};
static toUTC(d: Date | string): Date;
static parse(d:any): Date;
static monthList:Array<{ label: string, value: number }>;
}
declare class AtomPhone {
static toSmallPhoneString(v:string): string;
static toPhoneString(v:string): string;
}
declare class AtomUri {
constructor(v:string);
host: string;
protocol: string;
port: number;
path: string;
query: {[s:string]: string};
hash: {[s:string]: string};
}
if(location) {
Atom.designMode = /file/i.test(location.protocol);
}
/**
* This decorator will mark given property as bindable, it will define
* getter and setter, and in the setter, it will refresh the property.
*
* class Customer{
*
* @bindableProperty
* firstName:string;
*
* }
*
* @param {*} target
* @param {string} key
*/
function bindableProperty(target: any, key: string):void {
// property value
var _val:any = this[key];
var keyName:string = "_" + key;
this[keyName] = _val;
// property getter
var getter:()=>any = function ():any {
// console.log(`Get: ${key} => ${_val}`);
return this[keyName];
};
// property setter
var setter:(v:any) => void = function (newVal:any):void {
// console.log(`Set: ${key} => ${newVal}`);
// debugger;
var oldValue:any = this[keyName];
// tslint:disable-next-line:triple-equals
if(oldValue == newVal) {
return;
}
this[keyName] = newVal;
// only if this is not an AtomControl...
if(!(this._element && this._element.atomControl === this)) {
var c:any = this._$_supressRefresh;
if(!(c && c[key])) {
Atom.refresh(this, key);
}
}
if(this.onPropertyChanged) {
this.onPropertyChanged(key);
}
};
// delete property
if (delete this[key]) {
// create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
// tslint:disable-next-line:no-string-literal
if(target.constructor.prototype["get_atomParent"]) {
target["get_" + key] = getter;
target["set_" + key] = setter;
}
}
}
Atom.bindable = (e:any):any => {
if(!e) {
return e;
}
if(e instanceof Array) {
throw new TypeError("Invalid object, try to use AtomList instead of Atom.bindable");
}
if(typeof e === "string" || e.constructor === String) {
return e;
}
if(typeof e === "number" || e.constructor === Number) {
return e;
}
if(e.constructor === Date) {
return e;
}
var self:any = e;
if(e._$_isBindable) {
return e;
}
var keys: string[] = Object.keys(e);
e._$_isBindable = true;
for(var key of keys) {
var k:string = key;
var v:any = e[key];
var vk:string = `_${key}`;
e[vk] = v;
delete e[key];
Object.defineProperty(e, key, {
get: function():any { return this[vk]; },
set: function(v:any):void {
this[vk] = v;
Atom.refresh(this,k);
},
enumerable: true
});
}
return e;
};
Atom.controlToElement = (type:(string | WebAtoms.AtomControlType),tagName:string = "div"): HTMLElement => {
if(!type) {
return undefined;
}
var name:string = "";
if(!(typeof type === "string")) {
// tslint:disable-next-line:no-string-literal
name = type["__typeName"];
} else {
name = type;
}
var div:HTMLElement = document.createElement("div");
div.setAttribute("atom-type", name);
return div;
};
namespace WebAtoms {
/**
*
*
* @export
* @class CancelToken
*/
export class CancelToken {
listeners:Array<()=>void> = [];
private _cancelled:boolean;
get cancelled():boolean {
return this._cancelled;
}
cancel():void {
this._cancelled = true;
for(var fx of this.listeners) {
fx();
}
}
reset():void {
this._cancelled = false;
this.listeners.length = 0;
}
registerForCancel(f:()=>void):void {
if(this._cancelled) {
f();
this.cancel();
return;
}
this.listeners.push(f);
}
}
export class AtomModel {
public refresh(name: string): void {
Atom.refresh(this, name);
}
}
/**
* Though you can directly call methods of view model in binding expression,
* but we recommend using AtomCommand for two reasons.
*
* First one, it has enabled bindable property, which can be used to enable/disable UI.
* AtomButton already has `command` and `commandParameter` property which automatically
* binds execution and disabling the UI.
*
* Second one, it has busy bindable property, which can be used to display a busy indicator
* when corresponding action is a promise and it is yet not resolved.
*
* @export
* @class AtomCommand
* @extends {AtomModel}
* @template T
*/
export class AtomCommand<T> extends AtomModel {
public readonly isMVVMAtomCommand: boolean = true;
private _enabled: boolean = true;
/**
*
*
* @type {boolean}
* @memberof AtomCommand
*/
get enabled(): boolean {
return this._enabled;
}
set enabled(v: boolean) {
this._enabled = v;
this.refresh("enabled");
}
private _busy: boolean = false;
/**
*
*
* @type {boolean}
* @memberof AtomCommand
*/
get busy(): boolean {
return this._busy;
}
set busy(v:boolean) {
this._busy = v;
this.refresh("busy");
}
private action: (p: T) => any;
public execute: (p:T) => any;
private executeAction(p:T): any {
if(this._busy) {
return;
}
this.busy = true;
var result:any = this.action(p);
if (result) {
if(result.catch) {
result.catch((error) => {
this.busy = false;
if(error !== "cancelled") {
console.error(error);
Atom.showError(error);
}
});
return;
}
if(result.then) {
result.then(()=> {
this.busy = false;
});
return;
}
}
this.busy = false;
}
constructor(
action: (p: T) => any) {
super();
this.action = action;
this.execute = (p:T) => {
if (this.enabled) {
this.executeAction(p);
}
};
}
}
}