@web-atoms/core
Version:
553 lines (495 loc) • 19.2 kB
text/typescript
import type { App } from "../App";
import { parsePath, parsePathLists } from "./ExpressionParser";
import { IValueConverter } from "./IValueConverter";
import { CancelToken, ignoreValue } from "./types";
export interface IAtomComponent {
element: any;
viewModel: any;
localViewModel: any;
data: any;
app: App;
runAfterInit(f: () => void): void;
setLocalValue(e: any, name: string, value: any): void;
bindEvent(e: any, name: string, handler: any);
bind(e: any, name: string, path: any, twoWays: boolean, converter: any, source?: any);
}
const isEvent = /^event/i;
/**
* Bindings needs to be cloned...
*/
export type bindingFunction<T extends IAtomComponent = IAtomComponent> = (control: T, e?: any) => any;
export type asyncBindingFunction<TR, T extends IAtomComponent = IAtomComponent>
= (control: T, e: any, cancelToken: CancelToken) => Promise<TR>;
export type bindingFunctionCommand<T extends IAtomComponent = IAtomComponent> = (control: T, e?: any) => (p) => void;
// function oneTime(name: string, b: Bind, control: IAtomComponent, e: any) {
// control.runAfterInit(() => {
// control.setLocalValue(e, name, b.sourcePath(control, e));
// });
// }
// function event(name: string, b: Bind, control: IAtomComponent, e: any) {
// control.runAfterInit(() => {
// if (isEvent.test(name)) {
// name = name.substr(5);
// name = (name[0].toLowerCase() + name.substr(1));
// }
// control.bindEvent(e, name, (e1) => {
// return (b.sourcePath as any)(control, e1);
// });
// });
// }
// function oneWay(name: string, b: Bind, control: IAtomComponent, e: any, creator: any) {
// if (b.pathList) {
// control.bind(e, name, b.pathList , false, () => {
// // tslint:disable-next-line: ban-types
// return (b.sourcePath as Function).call(creator, control, e);
// });
// return;
// }
// if (b.combined) {
// const a = {
// // it is `this`
// t: creator,
// // it is first parameter
// x: control
// };
// control.bind(e, name, b.combined , false, () => {
// // tslint:disable-next-line: ban-types
// return (b.sourcePath as Function).call(creator, control, e);
// }, a);
// return;
// }
// if (b.thisPathList) {
// control.bind(e, name, b.thisPathList , false, () => {
// // tslint:disable-next-line: ban-types
// return (b.sourcePath as Function).call(creator, control, e);
// }, creator);
// return;
// }
// }
// function twoWays(name: string, b: Bind, control: IAtomComponent, e: any, creator: any) {
// control.bind(e,
// name,
// b.thisPathList || b.pathList, (b.eventList as any) || true, null, b.thisPathList ? creator : undefined);
// }
function twoWaysConvert(name: string, b: Bind, control: IAtomComponent, e: any, creator: any) {
control.bind(e,
name,
b.thisPathList || b.pathList, (b.eventList as any) || true, null, b.thisPathList ? creator : undefined);
}
// function presenter(name: string, b: Bind, control: IAtomComponent, e: any) {
// const n = b.name || name;
// let c = control.element as any;
// while (c) {
// if (c.atomControl && c.atomControl[n] !== undefined) {
// break;
// }
// c = c._logicalParent || c.parentElement;
// }
// ((c && c.atomControl) || control)[n] = e;
// }
export interface IData<T> extends IAtomComponent {
data: T;
}
export interface IVM<T> extends IAtomComponent {
viewModel: T;
}
export interface ILVM<T> extends IAtomComponent {
localViewModel: T;
}
export interface IBinder<T extends IAtomComponent> {
presenter(name?: string): Bind;
event(handler: (control: T, e?: CustomEvent) => void): any;
/**
* Bind the expression one time
* @param path Lambda Expression for binding
* @param now Default value to set immediately
*/
oneTime(path: bindingFunction<T>, now?: any): Bind;
/**
* Bind the expression one way
* @param path Lambda Expression for binding
* @param now Default value to set immediately
*/
oneWay(path: bindingFunction<T>, now?: any): Bind;
/**
* Setup two way binding with given expression
* @param path Lambda Expression for binding
* @param events events on auto refresh
*/
twoWays(path: bindingFunction<T>, events?: string[]): Bind;
}
export const bindSymbol = Symbol("Bind");
export default class Bind {
public static forControl<C extends IAtomComponent>(): IBinder<C> {
return Bind as any;
}
public static forData<D>(): IBinder<IData<D>> {
return Bind as any;
}
public static forViewModel<D>(): IBinder<IVM<D>> {
return Bind as any;
}
public static forLocalViewModel<D>(): IBinder<ILVM<D>> {
return Bind as any;
}
public static presenter(name?: string | ((c: any) => any)): any {
return {
[bindSymbol](cn: string, control: IAtomComponent, e: any, creator: any) {
if (typeof name === "function") {
name(control);
return;
}
const n = name || cn;
let c = control.element as any;
while (c) {
if (c.atomControl && c.atomControl[n] !== undefined) {
break;
}
c = c._logicalParent || c.parentElement;
}
((c && c.atomControl) || control)[n] = e;
}
};
}
// tslint:disable-next-line: ban-types
public static event<T extends IAtomComponent = IAtomComponent>(
sourcePath: (control: T, e?: CustomEvent) => void): any {
return {
[bindSymbol](name: string, control: IAtomComponent, e: any) {
control.runAfterInit(() => {
if (isEvent.test(name)) {
name = name.substring(5);
if (name.startsWith("-")) {
name = name.substring(1).replace(/-([a-z])/g, (g) => g[1].toUpperCase());
} else {
name = (name[0].toLowerCase() + name.substring(1));
}
}
control.bindEvent(e, name, (e1) => {
return (sourcePath as any)(control, e1);
});
});
}
};
}
/**
* Bind the expression one time
* @param sourcePath Lambda Expression for binding
* @param now Default value to set immediately
*/
public static oneTime<T extends IAtomComponent = IAtomComponent>(
sourcePath: bindingFunction<T>,
now?: any): any {
return {
[bindSymbol](name: string, control: IAtomComponent, e: any) {
control.runAfterInit(() => {
control.setLocalValue(e, name, sourcePath(control as any, e));
});
if (typeof now !== "undefined") {
control.setLocalValue(e, name, now);
}
}
};
}
/**
* Bind the expression one time
* @param sourcePath Lambda Expression for binding
* @param now Default value to set immediately
*/
public static oneTimeAsync<TR, T extends IAtomComponent = IAtomComponent>(
sourcePath: asyncBindingFunction<TR, T>,
now?: any): Promise<TR> {
return {
[bindSymbol](name: string, control: IAtomComponent, e: any) {
control.runAfterInit(() => {
(control.app as any).runAsync(async () => {
const value = await sourcePath(control as any, e, new CancelToken());
control.setLocalValue(e, name, value);
});
});
if (typeof now !== "undefined") {
control.setLocalValue(e, name, now);
}
}
} as any;
}
/**
* Bind the expression one way with source, you cannot reference
* `this` inside this context, it will not watch `this`
* @param source source to watch
* @param path Lambda Expression for binding
* @param now Default value to set immediately
*/
public static source<T>(
source: T,
path: (x: { control: IAtomComponent, source: T }) => any,
now?: any): any {
const lists = parsePath(path, false).map((x) => ["this", ... x]);
return {
[bindSymbol](name: string, control: IAtomComponent, e: any, creator: any) {
const self = { control, source };
control.bind(e, name, lists, false, () => {
return path.call(self, self);
}, self);
if (typeof now !== "undefined") {
control.setLocalValue(e, name, now);
}
}
};
}
public static oneWayAsync<TR, T extends IAtomComponent = IAtomComponent>(
sourcePath: asyncBindingFunction<TR, T>,
{
watchDelayInMS = 250,
default: defaultValue
}: {
watchDelayInMS?: number,
default?: any
} = {
}
): Promise<TR> {
let pathList;
let combined;
let thisPathList;
if (Array.isArray(sourcePath)) {
pathList = sourcePath as any;
} else {
const lists = parsePathLists(sourcePath);
if (lists.combined.length) {
combined = lists.combined;
}
if (lists.pathList.length) {
pathList = lists.pathList;
}
if (lists.thisPath.length) {
thisPathList = lists.thisPath;
}
}
if (!(combined || pathList || thisPathList)) {
throw new Error(`Failed to setup binding for ${sourcePath}, parsing failed`);
}
return {
[bindSymbol](name: string, control: IAtomComponent, e: any, creator: any) {
let bindingSource;
let finalPathList = pathList;
if (combined) {
bindingSource = {
t: creator,
x: control
};
finalPathList = combined;
} else if (thisPathList) {
finalPathList = thisPathList;
bindingSource = creator;
}
const asyncState = {
token: 0,
cancelToken: undefined
};
control.bind(e, name, finalPathList, false, () => {
const app = control.app;
asyncState.cancelToken?.cancel();
asyncState.cancelToken = undefined;
asyncState.token = app.setTimeoutAsync(async () => {
if (asyncState.cancelToken?.cancelled) {
return;
}
asyncState.token = undefined;
asyncState.cancelToken?.cancel();
const ct = asyncState.cancelToken = new CancelToken();
const value = await sourcePath.call(creator, control, e, ct);
if (!ct.cancelled) {
control.setLocalValue(e, name, value );
}
}, watchDelayInMS, asyncState.token);
return ignoreValue;
}, bindingSource);
if (typeof defaultValue !== "undefined") {
control.setLocalValue(e, name, defaultValue);
}
}
} as any;
}
/**
* Bind the expression one way
* @param sourcePath Lambda Expression for binding
* @param now Default value to set immediately
*/
public static oneWay<T extends IAtomComponent = IAtomComponent>(
sourcePath: bindingFunction<T>,
now?: any): any {
let pathList;
let combined;
let thisPathList;
if (Array.isArray(sourcePath)) {
pathList = sourcePath as any;
} else {
const lists = parsePathLists(sourcePath);
if (lists.combined.length) {
combined = lists.combined;
}
if (lists.pathList.length) {
pathList = lists.pathList;
}
if (lists.thisPath.length) {
thisPathList = lists.thisPath;
}
}
if (!(combined || pathList || thisPathList)) {
throw new Error(`Failed to setup binding for ${sourcePath}, parsing failed`);
}
return {
[bindSymbol](name: string, control: IAtomComponent, e: any, creator: any) {
if (pathList) {
control.bind(e, name, pathList, false, () => {
return sourcePath.call(creator, control, e);
});
if (typeof now !== "undefined") {
control.setLocalValue(e, name, now);
}
return;
}
if (combined) {
const a = {
t: creator,
x: control
};
control.bind(e, name, combined, false, () => {
return sourcePath.call(creator, control, e);
}, a);
if (typeof now !== "undefined") {
control.setLocalValue(e, name, now);
}
return;
}
control.bind(e, name, thisPathList, false, () => {
return sourcePath.call(creator, control, e);
}, creator);
if (typeof now !== "undefined") {
control.setLocalValue(e, name, now);
}
}
};
}
/**
* Setup two way binding with given expression
* @param sourcePath Lambda Expression for binding
* @param events events on auto refresh
* @param converter IValueConverter for value conversion
*/
public static twoWays<T extends IAtomComponent = IAtomComponent>(
sourcePath: bindingFunction<T>,
events?: string[],
converter?: IValueConverter): any {
let pathList;
// let combined;
let thisPathList;
if (Array.isArray(sourcePath)) {
pathList = sourcePath as any;
} else {
const lists = parsePathLists(sourcePath);
if (lists.combined.length) {
// combined = lists.combined;
throw new Error("Cannot have combined binding for two ways");
}
if (lists.pathList.length) {
pathList = lists.pathList;
}
if (lists.thisPath.length) {
thisPathList = lists.thisPath;
}
}
if (!(thisPathList || pathList)) {
throw new Error(`Failed to setup twoWay binding on ${sourcePath}`);
}
return {
[bindSymbol](name: string, control: IAtomComponent, e: any, creator: any) {
control.bind(e, name,
thisPathList || pathList,
(events as any) || true,
converter,
thisPathList ? creator : undefined);
}
};
}
/**
* Bind the expression one way with source, you cannot reference
* `this` inside this context, it will not watch `this`
* @param source source to watch
* @param path Lambda Expression for binding
* @param now Default value to set immediately
*/
public static sourceTwoWays<T>(
source: T,
path: (x: { control: IAtomComponent, source: T }) => any,
events: string[] = ["input", "cut", "paste", "change"]): any {
const lists = parsePath(path, false).map((x) => ["this", ... x]);
return {
[bindSymbol](name: string, control: IAtomComponent, e: any, creator: any) {
const self = { control, source };
control.bind(e, name, lists, events as any, lists, self);
}
};
}
// public static twoWaysConvert<T extends IAtomComponent = IAtomComponent>(
// sourcePath: bindingFunction<T>): Bind {
// const b = new Bind(twoWays, sourcePath, null, events);
// if (!(b.thisPathList || b.pathList)) {
// throw new Error(`Failed to setup twoWay binding on ${sourcePath}`);
// }
// return b;
// }
/**
* Use this for HTML only, this will fire two way binding
* as soon as the input/textarea box is updated
* @param sourcePath binding lambda expression
* @param converter Optional value converter
*/
public static twoWaysImmediate<T extends IAtomComponent = IAtomComponent>(
sourcePath: bindingFunction<T>,
converter?: IValueConverter): any {
return this.twoWays(sourcePath, ["change", "input", "paste", "cut"], converter);
// const b = new Bind(twoWays, sourcePath, null,
// ["change", "input", "paste"]);
// if (!(b.thisPathList || b.pathList)) {
// throw new Error(`Failed to setup twoWay binding on ${sourcePath}`);
// }
// return b;
}
public readonly sourcePath: bindingFunction;
public readonly pathList: string[][];
public readonly thisPathList: string[][];
public readonly combined: string[][];
constructor(
public readonly setupFunction: ((name: string, b: Bind, c: IAtomComponent, e: any, self?: any) => void),
sourcePath: bindingFunction,
public readonly name?: string,
public readonly eventList?: string[]
) {
this.sourcePath = sourcePath;
this[bindSymbol] = true;
if (!this.sourcePath) {
return;
}
if (Array.isArray(this.sourcePath)) {
this.pathList = this.sourcePath as any;
// this.setupFunction = null;
} else {
const lists = parsePathLists(this.sourcePath);
if (lists.combined.length) {
this.combined = lists.combined;
}
if (lists.pathList.length) {
this.pathList = lists.pathList;
}
if (lists.thisPath.length) {
this.thisPathList = lists.thisPath;
}
// if (setupFunction === oneWay) {
// if (!(this.combined || this.pathList || this.thisPathList)) {
// throw new Error(`Failed to setup binding for ${this.sourcePath}, parsing failed`);
// }
// }
}
}
}