##// 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:

r118:e07418577cbc v1.6.1 default
r131:c7d9ad82b374 v1.8.1 default
Show More
WatchForRendition.ts
208 lines | 6.5 KiB | video/mp2t | TypeScriptLexer
/ djx / src / main / ts / tsx / WatchForRendition.ts
import { id as mid } from "module";
import { TraceSource } from "@implab/core-amd/log/TraceSource";
import { argumentNotNull } from "@implab/core-amd/safe";
import { getScope, render, scheduleRender } from "./render";
import { RenditionBase } from "./RenditionBase";
import { Scope } from "./Scope";
import { Cancellation } from "@implab/core-amd/Cancellation";
import { collectNodes, destroy as safeDestroy, isDocumentFragmentNode, isElementNode, isMounted, placeAt, startupWidgets } from "./traits";
import { IDestroyable } from "@implab/core-amd/interfaces";
import { play } from "../play";
import * as fx from "dojo/fx";
import { isSubscribable, Subscribable } from "../observable";
import { isDjObservableResults, OrderedUpdate } from "../store";
const trace = TraceSource.get(mid);
interface ItemRendition {
nodes: Node[];
scope: IDestroyable;
destroy(): void;
}
interface RenderTask<T> extends OrderedUpdate<T> {
animate: boolean;
}
export interface AnimationAttrs {
animate?: boolean;
animateIn?: (nodes: Node[]) => Promise<void>;
animateOut?: (nodes: Node[]) => Promise<void>;
}
export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
subject: T[] | Subscribable<OrderedUpdate<T>> | undefined | null;
component: (arg: T, index: number) => unknown;
}
const noop = () => { };
const fadeIn = (nodes: Node[]) => Promise.all(nodes
.filter(isElementNode)
.map(el => play(fx.fadeIn({ node: el as HTMLElement })))
).then(noop);
const fadeOut = (nodes: Node[]) => Promise.all(nodes
.filter(isElementNode)
.map(el => play(fx.fadeOut({ node: el as HTMLElement })))
).then(noop);
export class WatchForRendition<T> extends RenditionBase<Node> {
private readonly _component: (arg: T, index: number) => unknown;
private readonly _node: Node;
private readonly _itemRenditions: ItemRendition[] = [];
private readonly _subject: T[] | Subscribable<OrderedUpdate<T>>;
private readonly _renderTasks: RenderTask<T>[] = [];
private readonly _animate: boolean;
private readonly _animateIn: (nodes: Node[]) => Promise<void>;
private readonly _animateOut: (nodes: Node[]) => Promise<void>;
private _ct = Cancellation.none;
constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
super();
argumentNotNull(component, "component");
this._component = component;
this._subject = subject ?? [];
this._node = document.createComment("[WatchFor]");
this._animate = !!animate;
this._animateIn = animateIn ?? fadeIn;
this._animateOut = animateOut ?? fadeOut;
}
protected _create() {
const scope = getScope();
scope.own(() => {
this._itemRenditions.forEach(safeDestroy);
safeDestroy(this._node);
});
const result = this._subject;
if (result) {
if (isSubscribable<OrderedUpdate<T>>(result)) {
let animate = false;
const subscription = result.subscribe({
next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
});
scope.own(subscription);
animate = this._animate;
} else {
if (isDjObservableResults<T>(result))
scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
for (let i = 0, n = result.length; i < n; i++)
this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate });
}
}
this._ct = new Cancellation(cancel => scope.own(cancel));
}
private readonly _onItemUpdated = (item: RenderTask<T>) => {
if (!this._renderTasks.length) {
// schedule a new job
this._renderTasks.push(item);
this._render().catch(e => trace.error(e));
} else {
// update existing job
this._renderTasks.push(item);
}
};
private async _render() {
// fork
const beginRender = await scheduleRender();
const endRender = beginRender();
try {
// don't render destroyed rendition
if (this._ct.isRequested())
return;
this._renderTasks.forEach(this._onRenderItem);
this._renderTasks.length = 0;
} finally {
endRender();
}
}
private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
const animate = _animate && prevIndex !== newIndex;
if (prevIndex > -1) {
// if we need to delete previous rendition
const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1);
if (animate) {
this._animateOut(nodes)
.then(destroy)
.catch(e => trace.error(e));
} else {
destroy();
}
}
if (newIndex > -1) {
// if we need to create the new rendition
// 1. create a new scope for rendering a content
const scope = new Scope();
// 2. render the content
const itemNode = render(this._component(item, newIndex), scope);
// 3. track nodes
const nodes = isDocumentFragmentNode(itemNode) ?
collectNodes(itemNode.childNodes) :
[itemNode];
// 5. insert node at the correct position
const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] };
if (beforeNode)
placeAt(itemNode, beforeNode, "before");
else
placeAt(itemNode, this._node, "before");
// 6. store information about rendition
this._itemRenditions.splice(newIndex, 0, {
scope,
nodes,
destroy: () => {
scope.destroy();
nodes.forEach(safeDestroy);
}
});
// 7. startup widgets if needed
if (isMounted(this._node))
nodes.forEach(n => startupWidgets(n));
// 8. optionally play the animation
if (animate)
this._animateIn(nodes).catch(e => trace.error(e));
}
};
protected _getDomNode() {
if (!this._node)
throw new Error("The instance of the rendition isn't created");
return this._node;
}
}