@web-atoms/core
Version:
254 lines (211 loc) • 7.32 kB
text/typescript
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();
}
}
}
}