##// END OF EJS Templates
Corrected Scope.own() to cleanup the supplied object immediately when the scope is disposed already
Corrected Scope.own() to cleanup the supplied object immediately when the scope is disposed already

File last commit:

r122:fb2ea4d6aaba v1.6.3 default
r131:c7d9ad82b374 v1.8.1 default
Show More
render.ts
198 lines | 5.1 KiB | video/mp2t | TypeScriptLexer
import { TraceSource } from "@implab/core-amd/log/TraceSource";
import { isPromise } from "@implab/core-amd/safe";
import { id as mid } from "module";
import { IScope, Scope } from "./Scope";
import { isNode, isRendition, isWidget } from "./traits";
const trace = TraceSource.get(mid);
interface Context {
readonly scope: IScope;
readonly hooks?: (() => void)[];
}
let _context: Context = {
scope: Scope.dummy
};
let _renderCount = 0;
let _renderId = 1;
let _renderedHooks: (() => void)[] = [];
const guard = (cb: () => unknown) => {
try {
const result = cb();
if (isPromise(result)) {
const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
result.then(warn, warn);
}
} catch (e) {
trace.error(e);
}
};
/**
*
* @param scope
* @returns
*/
export const beginRender = (scope = getScope()) => {
const prev = _context;
_renderCount++;
const renderId = _renderId++;
trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
if (_renderCount === 1)
onRendering();
_context = {
scope,
hooks: []
};
return endRender(prev, _context, renderId);
};
/**
* Method for a deferred rendering. Returns a promise with `beginRender()` function.
* Call to `scheduleRender` will save the current context, and will increment pending
* operations counter.
*
* @example
*
* const begin = await scheduleRender();
* const end = begin();
* try {
* // do some DOM manipulations
* } finally {
* end();
* }
*
* @param scope
* @returns
*/
export const scheduleRender = async (scope = getScope()) => {
_renderCount++;
const renderId = _renderId ++;
trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
if (_renderCount === 1)
onRendering();
await Promise.resolve();
return () => {
trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
const prev = _context;
_context = {
scope,
hooks: []
};
return endRender(prev, _context, renderId);
};
};
/**
* Completes render operation
*/
const endRender = (prev: Context, current: Context, renderId: number) => () => {
if (_context !== current)
trace.error("endRender mismatched beginRender call");
const { hooks } = _context;
if (hooks)
hooks.forEach(guard);
_renderCount--;
_context = prev;
trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
if (_renderCount === 0)
onRendered();
};
// called when the first beginRender is called for this iteration
const onRendering = () => {
setTimeout(() => {
if (_renderCount !== 0)
trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
});
};
// called when all render operations are complete
const onRendered = () => {
_renderedHooks.forEach(guard);
_renderedHooks = [];
};
export const whenRendered = () => new Promise<void>((resolve) => {
if (_renderCount)
_renderedHooks.push(resolve);
else
resolve();
});
export const renderHook = (hook: () => void) => {
const { hooks } = _context;
if (hooks)
hooks.push(hook);
else
guard(hook);
};
export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
const { hooks, scope } = _context;
if (hooks)
hooks.push(() => ref(value));
else
guard(() => ref(value));
scope.own(() => ref(undefined));
};
/** Returns the current scope */
export const getScope = () => _context.scope;
/** Schedules the rendition to be rendered to the DOM Node
* @param rendition The rendition to be rendered
* @param scope The scope
*/
export const render = (rendition: unknown, scope = Scope.dummy) => {
const complete = beginRender(scope);
try {
return getItemDom(rendition);
} finally {
complete();
}
};
const emptyFragment = document.createDocumentFragment();
/** Renders DOM element for different types of the argument. */
export const getItemDom = (v: unknown) => {
if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
// primitive types converted to the text nodes
return document.createTextNode(v.toString());
} else if (isNode(v)) {
// nodes are kept as is
return v;
} else if (isRendition(v)) {
// renditions are instantiated
return v.getDomNode();
} else if (isWidget(v)) {
// widgets are converted to it's markup
return v.domNode;
} else if (typeof v === "boolean" || v === null || v === undefined) {
// null | undefined | boolean are removed
return emptyFragment;
} else if (v instanceof Array) {
// arrays will be translated to document fragments
const fragment = document.createDocumentFragment();
v.map(item => getItemDom(item))
.forEach(node => fragment.appendChild(node));
return fragment;
} else {
// bug: explicit error otherwise
throw new Error(`Invalid parameter: ${String(v)}`);
}
};