|
|
import { id as mid } from "module";
|
|
|
import { TraceSource } from "@implab/core-amd/log/TraceSource";
|
|
|
import { argumentNotNull } from "@implab/core-amd/safe";
|
|
|
import { getScope, render } from "./render";
|
|
|
import { RenditionBase } from "./RenditionBase";
|
|
|
import { Scope } from "./Scope";
|
|
|
import { Subscribable } from "../observable";
|
|
|
import { Cancellation } from "@implab/core-amd/Cancellation";
|
|
|
import { collectNodes, destroy, isDocumentFragmentNode, isMounted, placeAt, startupWidgets } from "./traits";
|
|
|
|
|
|
const trace = TraceSource.get(mid);
|
|
|
|
|
|
export class WatchRendition<T> extends RenditionBase<Node> {
|
|
|
private readonly _component: (arg: T) => unknown;
|
|
|
|
|
|
private readonly _node: Node;
|
|
|
|
|
|
private readonly _scope = new Scope();
|
|
|
|
|
|
private readonly _subject: Subscribable<T>;
|
|
|
|
|
|
private _renderJob?: { value: T };
|
|
|
|
|
|
private _ct = Cancellation.none;
|
|
|
|
|
|
constructor(component: (arg: T) => unknown, subject: Subscribable<T>) {
|
|
|
super();
|
|
|
argumentNotNull(component, "component");
|
|
|
|
|
|
this._component = component;
|
|
|
|
|
|
this._subject = subject;
|
|
|
|
|
|
this._node = document.createComment("[Watch]");
|
|
|
}
|
|
|
|
|
|
protected _create() {
|
|
|
const scope = getScope();
|
|
|
scope.own(() => {
|
|
|
this._scope.destroy();
|
|
|
destroy(this._node);
|
|
|
});
|
|
|
scope.own(this._subject.subscribe({ next: this._onValue }));
|
|
|
this._ct = new Cancellation(cancel => scope.own(cancel));
|
|
|
}
|
|
|
|
|
|
private readonly _onValue = (value: T) => {
|
|
|
if (!this._renderJob) {
|
|
|
// schedule a new job
|
|
|
this._renderJob = { value };
|
|
|
this._render().catch(e => trace.error(e));
|
|
|
} else {
|
|
|
// update existing job
|
|
|
this._renderJob = { value };
|
|
|
}
|
|
|
};
|
|
|
|
|
|
private async _render() {
|
|
|
// fork
|
|
|
await Promise.resolve();
|
|
|
// don't render destroyed rendition
|
|
|
if (this._ct.isRequested())
|
|
|
return;
|
|
|
|
|
|
// remove all previous content
|
|
|
this._scope.clean();
|
|
|
|
|
|
// render the new node
|
|
|
const node = render(
|
|
|
this._renderJob ? this._component(this._renderJob.value) : undefined,
|
|
|
this._scope
|
|
|
);
|
|
|
|
|
|
// get actual content
|
|
|
const pending = isDocumentFragmentNode(node) ?
|
|
|
collectNodes(node.childNodes) :
|
|
|
[node];
|
|
|
|
|
|
placeAt(node, this._node, "after");
|
|
|
|
|
|
if (isMounted(this._node))
|
|
|
pending.forEach(n => startupWidgets(n));
|
|
|
|
|
|
if (pending.length)
|
|
|
this._scope.own(() => pending.forEach(destroy));
|
|
|
|
|
|
this._renderJob = undefined;
|
|
|
}
|
|
|
|
|
|
protected _getDomNode() {
|
|
|
if (!this._node)
|
|
|
throw new Error("The instance of the widget isn't created");
|
|
|
return this._node;
|
|
|
}
|
|
|
|
|
|
|
|
|
}
|
|
|
|