UNPKG

@web-atoms/core

Version:
254 lines (211 loc) • 7.32 kB
import { AtomBinder } from "./AtomBinder"; import { parsePath } from "./ExpressionParser"; import { IDisposable, PathList } from "./types"; export class ObjectProperty { public target: object; public name: string; public watcher: IDisposable; constructor(name: string) { this.name = name; } public toString(): string { return this.name; } } /** * * * @export * @class AtomWatcher * @implements {IDisposable} * @template T */ export class AtomWatcher<T> implements IDisposable { /** * If path was given as an array of string property path, you can use this `func` that will be executed * when any of property is updated. * * You must manually invoke evaluate after setting this property. * * @memberof AtomWatcher */ public func: (...values: any[]) => any; public funcText: string; public path: ObjectProperty[][]; public target: any; private runEvaluate: () => any; private forValidation: boolean; private isExecuting: boolean = false; /** * Creates an instance of AtomWatcher. * * let w = new AtomWatcher(this, x => x.data.fullName = `${x.data.firstName} ${x.data.lastName}`); * * You must dispose `w` in order to avoid memory leaks. * Above method will set fullName whenever, data or its firstName,lastName property is modified. * * AtomWatcher will assign null if any expression results in null in single property path. * * In order to avoid null, you can rewrite above expression as, * * let w = new AtomWatcher(this, * x => { * if(x.data.firstName && x.data.lastName){ * x.data.fullName = `${x.data.firstName} ${x.data.lastName}` * } * }); * * @param {T} target - Target on which watch will be set to observe given set of properties * @param {(PathList[] | ((x:T) => any))} path - Path is either lambda expression or array of * property path to watch, if path was lambda, it will be executed when any of * members will modify * @param {Function} onChanged - This function will be executed when any member in path is updated * @memberof AtomWatcher */ constructor( target: T, path: PathList[] | (() => any) , onChanged: (...v: any[]) => any, private source?: any ) { this.target = target; this.forValidation = true; if (path instanceof Function) { const f: () => any = path; path = parsePath(path); this.func = onChanged || f; this.funcText = f.toString(); } else { this.func = onChanged; } this.runEvaluate = () => { this.evaluate(); }; (this.runEvaluate as any).watcher = this; this.path = path.map( (x) => x.map( (y) => new ObjectProperty(y) ) ); if (!this.path.length) { // tslint:disable-next-line:no-debugger debugger; // tslint:disable-next-line:no-console console.warn("There is nothing to watch, do not use one way binding without any binding expression"); } } public toString(): string { return this.func.toString(); } /** * This will dispose and unregister all watchers * * @memberof AtomWatcher */ public dispose(): void { if (!this.path) { return; } for (const p of this.path) { for (const op of p) { if (op.watcher) { op.watcher.dispose(); op.watcher = null; op.target = null; } } } // tslint:disable-next-line:no-string-literal // this["disposedPath"] = this.path; this.func = null; // this.path.length = 0; this.path = null; this.source = null; } /** * Initialize the path targets * @param evaluate if true, evaluate entire watch expression and run onChange method */ public init(evaluate?: boolean): void { if (evaluate) { this.evaluate(true); } else { for (const iterator of this.path) { this.evaluatePath(this.target, iterator); } } } private evaluatePath(target: any, path: ObjectProperty[]): any { // console.log(`\tevaluatePath: ${path.map(op=>op.name).join(", ")}`); let newTarget: any = null; for (const p of path) { if (this.source && p.name === "this") { target = this.source; continue; } newTarget = target[p.name]; if (!p.target) { if (p.watcher) { p.watcher.dispose(); } p.watcher = AtomBinder.watch(target, p.name, this.runEvaluate); } else if (p.target !== target) { if (p.watcher) { p.watcher.dispose(); } p.watcher = AtomBinder.watch(target, p.name, this.runEvaluate); } p.target = target; target = newTarget; if (newTarget === undefined || newTarget === null) { break; } } return newTarget; } /** * * * @param {boolean} [force] * @returns {*} * @memberof AtomWatcher */ private evaluate(force?: boolean): any { if (!this.path) { // this watcher may have been disposed... // tslint:disable-next-line:no-console console.warn(`Watcher is not disposed properly, please watch for any memory leak`); return; } if (this.isExecuting) { return; } const disposeWatchers: IDisposable[] = []; this.isExecuting = true; try { const values: any[] = []; const logs: string[][] = []; for (const p of this.path) { values.push(this.evaluatePath(this.target, p)); } // if (force === true) { // this.forValidation = false; // } // if (this.forValidation) { // const x: boolean = true; // if (values.find( (x1) => x1 ? true : false)) { // this.forValidation = false; // } else { // return; // } // } try { this.func.apply(this.target, values); } catch (e) { // tslint:disable-next-line:no-console console.warn(e); } } finally { this.isExecuting = false; for (const d of disposeWatchers) { d.dispose(); } } } }