UNPKG

web-atoms-mvvm

Version:

MVVM, REST Json Service, Message Subscriptions for Web Atoms

520 lines (439 loc) 15.8 kB
/// <reference path="atom-device.ts"/> /// <reference path="atom-command.ts"/> namespace WebAtoms { type VMSubscription = { channel: string, action: AtomAction, disposable: AtomDisposable }; /** * * * @export * @class AtomViewModel * @extends {AtomModel} */ export class AtomViewModel extends AtomModel { private disposables: Array<AtomDisposable>; private subscriptions: VMSubscription[]; private _channelPrefix: string = ""; public get channelPrefix(): string { return this._channelPrefix; } public set channelPrefix(v: string) { this._channelPrefix = v; var temp: VMSubscription[] = this.subscriptions; if(temp) { this.subscriptions = []; for(var s of temp) { s.disposable.dispose(); } for(var s1 of temp) { this.subscribe(s.channel,s.action); } } Atom.refresh(this,"channelPrefix"); } private _isReady:boolean = false; public get isReady():boolean { return this._isReady; } constructor() { super(); AtomDevice.instance.runAsync(() => this.privateInit()); } private async privateInit():Promise<any> { try { await Atom.postAsync(async () => { this.runDecoratorInits(); // this.registerWatchers(); }); await Atom.postAsync(async ()=> { await this.init(); this.onReady(); }); if(this.postInit) { for(var i of this.postInit) { i(); } this.postInit = null; } } finally { this._isReady = true; } } public async waitForReady():Promise<any> { while(!this._isReady) { await Atom.delay(100); } } // tslint:disable-next-line:no-empty protected onReady():void {} postInit:Array<Function>; private runDecoratorInits():void { var v:any = this.constructor.prototype; if(!v) { return; } var ris: Function[] = v._$_inits; if(ris) { for(var ri of ris) { ri.call(this, this); } } } private validations:AtomWatcher<AtomViewModel>[] = []; /** * Internal method, do not use, instead use errors.hasErrors() * * @memberof AtomViewModel */ validate():void { for(var v of this.validations) { v.evaluate(true); } } /** * Adds validation expression to be executed when any bindable expression is updated. * * `target` must always be set to `this`. * * this.addValidation(() => { * this.errors.nameError = this.data.firstName ? "" : "Name cannot be empty"; * }); * * Only difference here is, validation will not kick in first time, where else watch will * be invoked as soon as it is setup. * * Validation will be invoked when any bindable property in given expression is updated. * * Validation can be invoked explicitly only by calling `errors.hasErrors()`. * * @protected * @template T * @param {() => any} ft * @returns {AtomDisposable} * @memberof AtomViewModel */ protected addValidation(...fts:(() => any)[]): AtomDisposable { var ds: Array<AtomDisposable> = []; for(var ft of fts) { var d:AtomWatcher<any> = new AtomWatcher<any>(this,ft, false, true); this.validations.push(d); this.registerDisposable(d); ds.push(d); } return new DisposableAction(()=> { this.disposables = this.disposables.filter( f => !ds.find(fd => f === fd) ); for(var dispsoable of ds) { dispsoable.dispose(); } }); } /** * Execute given expression whenever any bindable expression changes * in the expression. * * For correct generic type resolution, target must always be `this`. * * this.watch(() => { * if(!this.data.fullName){ * this.data.fullName = `${this.data.firstName} ${this.data.lastName}`; * } * }); * * @protected * @template T * @param {() => any} ft * @returns {AtomDisposable} * @memberof AtomViewModel */ protected watch(...fts:(() => any)[]): AtomDisposable { var dfd:AtomDisposable[] = []; for(var ft of fts) { var d:AtomWatcher<any> = new AtomWatcher<any>(this,ft, this._isReady ); // debugger; this.registerDisposable(d); dfd.push(d); if(!this._isReady) { this.postInit = this.postInit || []; this.postInit.push(() => { d.runEvaluate(); }); } } return new DisposableAction(()=> { this.disposables = this.disposables.filter( f => ! dfd.find(fd => f === fd) ); for(var disposable of dfd) { disposable.dispose(); } }); } /** * Register a disposable to be disposed when view model will be disposed. * * @protected * @param {AtomDisposable} d * @memberof AtomViewModel */ public registerDisposable(d:AtomDisposable):void { this.disposables = this.disposables || []; this.disposables.push(d); } // tslint:disable-next-line:no-empty protected onPropertyChanged(name:string): void {} /** * Register listener for given message. * * @protected * @template T * @param {string} msg * @param {(data: T) => void} a * @memberof AtomViewModel */ protected onMessage<T>(msg: string, a: (data: T) => void):void { console.warn("Do not use onMessage, instead use @receive decorator..."); var action: AtomAction = (m, d) => { a(d as T); }; var sub:AtomDisposable = AtomDevice.instance.subscribe( this.channelPrefix + msg, action); this.registerDisposable(sub); } /** * Broadcast given data to channel (msg) * * @param {string} msg * @param {*} data * @memberof AtomViewModel */ public broadcast(msg: string, data: any):void { AtomDevice.instance.broadcast(this.channelPrefix + msg, data); } private subscribe(channel: string, c: (ch:string, data:any) => void): void { var sub:AtomDisposable = AtomDevice.instance.subscribe( this.channelPrefix + channel, c); this.subscriptions = this.subscriptions || []; this.subscriptions.push({ channel: channel, action: c, disposable: sub }); } /** * Put your asynchronous initializations here * * @returns {Promise<any>} * @memberof AtomViewModel */ // tslint:disable-next-line:no-empty public async init(): Promise<any> { } /** * dispose method will becalled when attached view will be disposed or * when a new view model will be assigned to view, old view model will be disposed. * * @memberof AtomViewModel */ public dispose():void { if(this.disposables) { for(let d of this.disposables) { d.dispose(); } } if(this.subscriptions) { for(let d of this.subscriptions) { d.disposable.dispose(); } this.subscriptions = null; } } } /** * This view model should be used with WindowService to create and open window. * * This view model has `close` and `cancel` methods. `close` method will * close the window and will resolve the given result in promise. `cancel` * will reject the given promise. * * @example * * var windowService = WebAtoms.DI.resolve(WindowService); * var result = await * windowService.openWindow( * "Namespace.WindowName", * new WindowNameViewModel()); * * * * class NewTaskWindowViewModel extends AtomWindowViewModel{ * * .... * save(){ * * // close and send result * this.close(task); * * } * .... * * } * * @export * @class AtomWindowViewModel * @extends {AtomViewModel} */ export class AtomWindowViewModel extends AtomViewModel { // init(): Promise<any> { // if(!Atom.testMode) { // if(this._windowName) { // return; // } // } // return super.init(); // } // windowInit(): Promise<any> { // return super.init(); // } /** * windowName will be set to generated html tag id, you can use this * to mock AtomWindowViewModel in testing. * * When window is closed or cancelled, view model only broadcasts * `atom-window-close:${this.windowName}`, you can listen for * such message. * * @type {string} * @memberof AtomWindowViewModel */ public get windowName(): string { return this._windowName; } public set windowName(v:string) { this._windowName = v; Atom.refresh(this, "windowName"); } _windowName: string; /** * This will broadcast `atom-window-close:windowName`. * WindowService will close the window on receipt of such message and * it will resolve the promise with given result. * * this.close(someResult); * * @param {*} [result] * @memberof AtomWindowViewModel */ close(result?:any):void { // tslint:disable-next-line:no-string-literal this["_channelPrefix"] = ""; this.broadcast(`atom-window-close:${this.windowName}`,result); } /** * This will broadcast `atom-window-cancel:windowName` * WindowService will cancel the window on receipt of such message and * it will reject the promise with "cancelled" message. * * this.cancel(); * * @memberof AtomWindowViewModel */ cancel():void { // tslint:disable-next-line:no-string-literal this["_channelPrefix"] = ""; this.broadcast(`atom-window-cancel:${this.windowName}`,null); } } export class AtomPageViewModel extends AtomViewModel { pageId: string; closeWarning: string; async cancel(): Promise<any> { if(!this.closeWarning) { this.broadcast(`pop-page:${this.pageId}`,null); return; } if( await WindowService.instance.confirm(this.closeWarning,"Are you sure?")) { this.broadcast(`pop-page:${this.pageId}`,null); } } } } type viewModelInit = (vm:WebAtoms.AtomViewModel) => void; function registerInit(target:WebAtoms.AtomViewModel, fx: viewModelInit ):void { var t:any = target as any; var inits:viewModelInit[] = t._$_inits = t._$_inits || []; inits.push(fx); } /** * Receive messages for given channel * @param {(string | RegExp)} channel * @returns {Function} */ function receive(...channel:string[]):Function { return function(target:WebAtoms.AtomViewModel, key: string | symbol):void { registerInit(target, vm => { var fx:Function = (vm as any)[key]; var a: WebAtoms.AtomAction = (ch:string, d: any): void => { fx.call(vm, ch, d ); }; // tslint:disable-next-line:no-string-literal var s:Function = vm["subscribe"]; for(var c of channel) { s.call(vm,c, a); } }); }; } function bindableReceive(...channel: string[]): Function { return function(target:WebAtoms.AtomViewModel, key:string):void { var bp:any = bindableProperty(target, key); registerInit(target, vm => { var fx:WebAtoms.AtomAction = (cx:string, m:any) => { vm[key] = m; }; // tslint:disable-next-line:no-string-literal var s:Function = vm["subscribe"]; for(var c of channel) { s.call(vm, c, fx); } }); return bp; }; } function bindableBroadcast(...channel: string[]): Function { return function(target:WebAtoms.AtomViewModel, key:string):void { var bp:any = bindableProperty(target, key); registerInit(target, vm => { var fx:(t:any) => any = (t:any):any => { var v:any = vm[key]; for(var c of channel) { vm.broadcast(c, v); } }; var d:WebAtoms.AtomWatcher<any> = new WebAtoms.AtomWatcher<any>(vm,[ key], false ); d.func = fx; // tslint:disable-next-line:no-string-literal var f: Function = d["evaluatePath"]; // tslint:disable-next-line:no-string-literal for(var p of d.path) { f.call(d, vm, p); } vm.registerDisposable(d); }); return bp; }; } function watch(target:WebAtoms.AtomViewModel, key: string | symbol, descriptor:any):void { registerInit(target, vm => { // tslint:disable-next-line:no-string-literal var vfx: Function = vm["watch"]; vfx.call(vm,vm[key]); }); } function validate(target:WebAtoms.AtomViewModel, key: string | symbol, descriptor:any):void { registerInit(target, vm => { // tslint:disable-next-line:no-string-literal var vfx: Function = vm["addValidation"]; // tslint:disable-next-line:no-string-literal vfx.call(vm,vm["key"]); }); }