render.ts
112 lines
| 3.0 KiB
| video/mp2t
|
TypeScriptLexer
cin
|
r101 | import { TraceSource } from "@implab/core-amd/log/TraceSource"; | ||
import { isPromise } from "@implab/core-amd/safe"; | ||||
import { id as mid } from "module"; | ||||
cin
|
r102 | import { IScope, Scope } from "./Scope"; | ||
import { isNode, isRendition, isWidget } from "./traits"; | ||||
cin
|
r101 | |||
const trace = TraceSource.get(mid); | ||||
cin
|
r98 | |||
cin
|
r102 | interface Context { | ||
scope: IScope; | ||||
cin
|
r98 | |||
cin
|
r102 | hooks?: (() => void)[]; | ||
} | ||||
cin
|
r101 | |||
cin
|
r102 | let _context: Context = { | ||
scope: Scope.dummy | ||||
} | ||||
cin
|
r101 | |||
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); | ||||
} | ||||
cin
|
r98 | } | ||
cin
|
r102 | export const beginRender = (scope: IScope = getScope()) => { | ||
const prev = _context; | ||||
_context = { | ||||
scope, | ||||
hooks: [] | ||||
}; | ||||
return endRender(prev); | ||||
cin
|
r101 | } | ||
/** | ||||
* Completes render operation | ||||
*/ | ||||
cin
|
r102 | const endRender = (prev: Context) => () => { | ||
const { hooks } = _context; | ||||
if (hooks) | ||||
cin
|
r101 | hooks.forEach(guard); | ||
cin
|
r102 | |||
_context = prev; | ||||
cin
|
r101 | } | ||
export const renderHook = (hook: () => void) => { | ||||
cin
|
r102 | const { hooks } = _context; | ||
if (hooks) | ||||
cin
|
r101 | hooks.push(hook); | ||
else | ||||
guard(hook); | ||||
cin
|
r98 | } | ||
cin
|
r102 | 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)); | ||||
} | ||||
cin
|
r98 | /** Returns the current scope */ | ||
cin
|
r102 | export const getScope = () => _context.scope; | ||
cin
|
r98 | |||
/** Schedules the rendition to be rendered to the DOM Node | ||||
* @param rendition The rendition to be rendered | ||||
* @param scope The scope | ||||
*/ | ||||
cin
|
r102 | export const render = (rendition: unknown, scope = Scope.dummy) => { | ||
const complete = beginRender(scope); | ||||
cin
|
r98 | try { | ||
cin
|
r102 | return getItemDom(rendition); | ||
cin
|
r98 | } finally { | ||
cin
|
r102 | complete(); | ||
cin
|
r98 | } | ||
} | ||||
/** Renders DOM element for different types of the argument. */ | ||||
cin
|
r101 | export const getItemDom = (v: unknown) => { | ||
cin
|
r98 | 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 | ||||
cin
|
r101 | return v.getDomNode(); | ||
cin
|
r98 | } else if (isWidget(v)) { | ||
// widgets are converted to it's markup | ||||
return v.domNode; | ||||
} else if (typeof v === "boolean" || v === null || v === undefined) { | ||||
cin
|
r102 | // null | undefined | boolean are removed | ||
return document.createDocumentFragment(); | ||||
cin
|
r98 | } else if (v instanceof Array) { | ||
// arrays will be translated to document fragments | ||||
const fragment = document.createDocumentFragment(); | ||||
cin
|
r101 | v.map(item => getItemDom(item)) | ||
cin
|
r98 | .forEach(node => fragment.appendChild(node)); | ||
return fragment; | ||||
} else { | ||||
// bug: explicit error otherwise | ||||
throw new Error("Invalid parameter: " + v); | ||||
} | ||||
} | ||||