##// END OF EJS Templates
added whenRendered() method to wait for pending oprations to complete
cin -
r118:e07418577cbc v1.6.1 default
parent child
Show More
@@ -9,6 +9,7
9 9 "cSpell.words": [
10 10 "dijit",
11 11 "djbase",
12 "djclass"
12 "djclass",
13 "Unsubscribable"
13 14 ]
14 15 } No newline at end of file
@@ -54,10 +54,10 export interface Unsubscribable {
54 54 unsubscribe(): void;
55 55 }
56 56
57 export const isUnsubsribable = (v: unknown): v is Unsubscribable =>
57 export const isUnsubscribable = (v: unknown): v is Unsubscribable =>
58 58 v !== null && v !== undefined && typeof (v as Unsubscribable).unsubscribe === "function";
59 59
60 export const isSubsribable = <T = unknown>(v: unknown): v is Subscribable<T> =>
60 export const isSubscribable = <T = unknown>(v: unknown): v is Subscribable<T> =>
61 61 v !== null && v !== undefined && typeof (v as Subscribable<unknown>).subscribe === "function";
62 62
63 63 export interface Subscribable<T> {
@@ -25,18 +25,18 interface DjObservableResults<T> {
25 25 };
26 26 }
27 27
28 interface Queryable<T, A extends unknown[]> {
29 query(...args: A): PromiseOrValue<T[]>;
28 interface Queryable<T, Q, O> {
29 query(query?: Q, options?: O): PromiseOrValue<T[]>;
30 30 }
31 31
32 export const isObservableResults = <T>(v: object): v is DjObservableResults<T> =>
32 export const isDjObservableResults = <T>(v: object): v is DjObservableResults<T> =>
33 33 v && (typeof (v as { observe?: unknown; }).observe === "function");
34 34
35 export const query = <T, A extends unknown[]>(store: Queryable<T, A>, includeUpdates = true) =>
36 (...args: A) => {
35 export const query = <T, Q, O>(store: Queryable<T, Q, O>, includeUpdates = true) =>
36 (query?: Q, options?: O & { observe: boolean }) => {
37 37 return observe<OrderedUpdate<T>>(({ next, complete, error, isClosed }) => {
38 38 try {
39 const results = store.query(...args);
39 const results = store.query(query, options);
40 40 if (isPromise(results)) {
41 41 results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })))
42 42 .then(undefined, error);
@@ -44,7 +44,7 export const query = <T, A extends unkno
44 44 results.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }));
45 45 }
46 46
47 if (!isClosed() && isObservableResults<T>(results)) {
47 if (!isClosed() && (options?.observe !== false) && isDjObservableResults<T>(results)) {
48 48 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }), includeUpdates);
49 49 return () => h.remove();
50 50 } else {
@@ -102,7 +102,7 export function watch(
102 102 }
103 103 }
104 104
105 export const watchFor = <T>(source: T[] | Subscribable<OrderedUpdate<T>>, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
105 export const watchFor = <T>(source: T[] | Subscribable<OrderedUpdate<T>> | null | undefined, render: (item: T, index: number) => unknown, opts: AnimationAttrs = {}) => {
106 106 return new WatchForRendition({
107 107 ...opts,
108 108 subject: source,
@@ -1,6 +1,6
1 1 import { IDestroyable, IRemovable } from "@implab/core-amd/interfaces";
2 2 import { isDestroyable, isRemovable } from "@implab/core-amd/safe";
3 import { isUnsubsribable, Unsubscribable } from "../observable";
3 import { isUnsubscribable, Unsubscribable } from "../observable";
4 4
5 5 export interface IScope {
6 6 own(target: (() => void) | IDestroyable | IRemovable | Unsubscribable): void;
@@ -18,7 +18,7 export class Scope implements IDestroyab
18 18 this._cleanup.push(() => target.destroy());
19 19 } else if (isRemovable(target)) {
20 20 this._cleanup.push(() => target.remove());
21 } else if (isUnsubsribable(target)) {
21 } else if (isUnsubscribable(target)) {
22 22 this._cleanup.push(() => target.unsubscribe());
23 23 }
24 24 }
@@ -1,7 +1,7
1 1 import { id as mid } from "module";
2 2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render } from "./render";
4 import { getScope, render, scheduleRender } from "./render";
5 5 import { RenditionBase } from "./RenditionBase";
6 6 import { Scope } from "./Scope";
7 7 import { Cancellation } from "@implab/core-amd/Cancellation";
@@ -9,8 +9,8 import { collectNodes, destroy as safeDe
9 9 import { IDestroyable } from "@implab/core-amd/interfaces";
10 10 import { play } from "../play";
11 11 import * as fx from "dojo/fx";
12 import { isSubsribable, Subscribable } from "../observable";
13 import { isObservableResults, OrderedUpdate } from "../store";
12 import { isSubscribable, Subscribable } from "../observable";
13 import { isDjObservableResults, OrderedUpdate } from "../store";
14 14
15 15 const trace = TraceSource.get(mid);
16 16
@@ -35,7 +35,7 export interface AnimationAttrs {
35 35 }
36 36
37 37 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
38 subject: T[] | Subscribable<OrderedUpdate<T>>;
38 subject: T[] | Subscribable<OrderedUpdate<T>> | undefined | null;
39 39
40 40 component: (arg: T, index: number) => unknown;
41 41 }
@@ -76,11 +76,10 export class WatchForRendition<T> extend
76 76 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
77 77 super();
78 78 argumentNotNull(component, "component");
79 argumentNotNull(subject, "component");
80 79
81 80 this._component = component;
82 81
83 this._subject = subject;
82 this._subject = subject ?? [];
84 83
85 84 this._node = document.createComment("[WatchFor]");
86 85 this._animate = !!animate;
@@ -98,7 +97,7 export class WatchForRendition<T> extend
98 97 const result = this._subject;
99 98
100 99 if (result) {
101 if (isSubsribable<OrderedUpdate<T>>(result)) {
100 if (isSubscribable<OrderedUpdate<T>>(result)) {
102 101 let animate = false;
103 102 const subscription = result.subscribe({
104 103 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
@@ -106,7 +105,7 export class WatchForRendition<T> extend
106 105 scope.own(subscription);
107 106 animate = this._animate;
108 107 } else {
109 if (isObservableResults<T>(result))
108 if (isDjObservableResults<T>(result))
110 109 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
111 110
112 111 for (let i = 0, n = result.length; i < n; i++)
@@ -129,13 +128,18 export class WatchForRendition<T> extend
129 128
130 129 private async _render() {
131 130 // fork
132 await Promise.resolve();
133 // don't render destroyed rendition
134 if (this._ct.isRequested())
135 return;
131 const beginRender = await scheduleRender();
132 const endRender = beginRender();
133 try {
134 // don't render destroyed rendition
135 if (this._ct.isRequested())
136 return;
136 137
137 this._renderTasks.forEach(this._onRenderItem);
138 this._renderTasks.length = 0;
138 this._renderTasks.forEach(this._onRenderItem);
139 this._renderTasks.length = 0;
140 } finally {
141 endRender();
142 }
139 143 }
140 144
141 145 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
@@ -198,7 +202,7 export class WatchForRendition<T> extend
198 202
199 203 protected _getDomNode() {
200 204 if (!this._node)
201 throw new Error("The instance of the widget isn't created");
205 throw new Error("The instance of the rendition isn't created");
202 206 return this._node;
203 207 }
204 208 }
@@ -1,7 +1,7
1 1 import { id as mid } from "module";
2 2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render } from "./render";
4 import { getItemDom, getScope, scheduleRender } from "./render";
5 5 import { RenditionBase } from "./RenditionBase";
6 6 import { Scope } from "./Scope";
7 7 import { Subscribable } from "../observable";
@@ -56,35 +56,36 export class WatchRendition<T> extends R
56 56 };
57 57
58 58 private async _render() {
59 // fork
60 await Promise.resolve();
61 // don't render destroyed rendition
62 if (this._ct.isRequested())
63 return;
59 const beginRender = await scheduleRender(this._scope);
60 const endRender = beginRender();
61 try {
62 // don't render destroyed rendition
63 if (this._ct.isRequested())
64 return;
64 65
65 // remove all previous content
66 this._scope.clean();
66 // remove all previous content
67 this._scope.clean();
67 68
68 // render the new node
69 const node = render(
70 this._renderJob ? this._component(this._renderJob.value) : undefined,
71 this._scope
72 );
69 // render the new node
70 const node = getItemDom(this._renderJob ? this._component(this._renderJob.value) : undefined);
73 71
74 // get actual content
75 const pending = isDocumentFragmentNode(node) ?
76 collectNodes(node.childNodes) :
77 [node];
72 // get actual content
73 const pending = isDocumentFragmentNode(node) ?
74 collectNodes(node.childNodes) :
75 [node];
78 76
79 placeAt(node, this._node, "after");
77 placeAt(node, this._node, "after");
80 78
81 if (isMounted(this._node))
82 pending.forEach(n => startupWidgets(n));
79 if (isMounted(this._node))
80 pending.forEach(n => startupWidgets(n));
83 81
84 if (pending.length)
85 this._scope.own(() => pending.forEach(destroy));
82 if (pending.length)
83 this._scope.own(() => pending.forEach(destroy));
86 84
87 this._renderJob = undefined;
85 this._renderJob = undefined;
86 } finally {
87 endRender();
88 }
88 89 }
89 90
90 91 protected _getDomNode() {
@@ -7,15 +7,20 import { isNode, isRendition, isWidget }
7 7 const trace = TraceSource.get(mid);
8 8
9 9 interface Context {
10 scope: IScope;
10 readonly scope: IScope;
11 11
12 hooks?: (() => void)[];
12 readonly hooks?: (() => void)[];
13 13 }
14 14
15 15 let _context: Context = {
16 16 scope: Scope.dummy
17 17 };
18 18
19 let _renderCount = 0;
20 let _renderId = 1;
21 let _renderedHooks: (() => void)[] = [];
22
23
19 24 const guard = (cb: () => unknown) => {
20 25 try {
21 26 const result = cb();
@@ -28,26 +33,104 const guard = (cb: () => unknown) => {
28 33 }
29 34 };
30 35
31 export const beginRender = (scope: IScope = getScope()) => {
36 /**
37 *
38 * @param scope
39 * @returns
40 */
41 export const beginRender = (scope = getScope()) => {
32 42 const prev = _context;
43 _renderCount++;
44 const renderId = _renderId++;
45 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
46 if (_renderCount === 1)
47 onRendering();
48
33 49 _context = {
34 50 scope,
35 51 hooks: []
36 52 };
37 return endRender(prev);
53 return endRender(prev, _context, renderId);
54 };
55
56 /**
57 * Method for a deferred rendering. Returns a promise with `beginRender()` function.
58 * Call to `scheduleRender` will save the current context, and will increment pending
59 * operations counter.
60 *
61 * @example
62 *
63 * const begin = await scheduleRender();
64 * const end = begin();
65 * try {
66 * // do some DOM manipulations
67 * } finally {
68 * end();
69 * }
70 *
71 * @param scope
72 * @returns
73 */
74 export const scheduleRender = async (scope = getScope()) => {
75 const prev = _context;
76 _renderCount++;
77 const renderId = _renderId ++;
78 trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
79 if (_renderCount === 1)
80 onRendering();
81
82 await Promise.resolve();
83
84 return () => {
85 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
86 _context = {
87 scope,
88 hooks: []
89 };
90 return endRender(prev, _context, renderId);
91 };
38 92 };
39 93
40 94 /**
41 95 * Completes render operation
42 96 */
43 const endRender = (prev: Context) => () => {
97 const endRender = (prev: Context, current: Context, renderId: number) => () => {
98 if (_context !== current)
99 trace.error("endRender mismatched beginRender call");
100
44 101 const { hooks } = _context;
45 102 if (hooks)
46 103 hooks.forEach(guard);
47 104
105 _renderCount--;
48 106 _context = prev;
107
108 trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
109 if (_renderCount === 0)
110 onRendered();
49 111 };
50 112
113 // called when the first beginRender is called for this iteration
114 const onRendering = () => {
115 setTimeout(() => {
116 if (_renderCount !== 0)
117 trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
118 });
119 };
120
121 // called when all render operations are complete
122 const onRendered = () => {
123 _renderedHooks.forEach(guard);
124 _renderedHooks = [];
125 };
126
127 export const whenRendered = () => new Promise<void>((resolve) => {
128 if (_renderCount)
129 _renderedHooks.push(resolve);
130 else
131 resolve();
132 });
133
51 134 export const renderHook = (hook: () => void) => {
52 135 const { hooks } = _context;
53 136 if (hooks)
@@ -2,6 +2,15 import MainWidget from "./view/MainWidge
2 2 import "@implab/djx/css!dojo/resources/dojo.css";
3 3 import "@implab/djx/css!dijit/themes/dijit.css";
4 4 import "@implab/djx/css!dijit/themes/tundra/tundra.css";
5 import { TraceSource } from "@implab/core-amd/log/TraceSource";
6 import { ConsoleLogger } from "@implab/core-amd/log/writers/ConsoleLogger";
7
8 const logger = new ConsoleLogger();
9
10 TraceSource.on(source => {
11 source.level = 400;
12 logger.writeEvents(source.events);
13 });
5 14
6 15 const w = new MainWidget();
7 16 w.placeAt(document.body);
@@ -41,6 +41,19 export class MainContext implements IDes
41 41 );
42 42 }
43 43
44 async load() {
45 await Promise.resolve();
46 for (let i = 0; i < 2; i++) {
47 const id = Uuid();
48 this._appointments.add({
49 id,
50 startAt: new Date(),
51 duration: 30,
52 title: `Hello ${i+1}`
53 });
54 }
55 }
56
44 57 private readonly _queryAppointmentsRx = query(this._appointments);
45 58
46 59 private readonly _queryMembersRx = query(this._members);
@@ -8,6 +8,7 import { MainContext } from "./MainConte
8 8 import { LocalDate } from "@js-joda/core";
9 9 import { error } from "../logging";
10 10 import { TraceSource } from "@implab/core-amd/log/TraceSource";
11 import { whenRendered } from "@implab/djx/tsx/render";
11 12
12 13 const trace = TraceSource.get(mid);
13 14
@@ -52,10 +53,20 export default class MainModel implement
52 53 }
53 54
54 55 addAppointment(title: string, startAt: Date, duration: number) {
55 this._context.createAppointment(title,startAt, duration, []).catch(error(trace));
56 this._context.createAppointment(title,startAt, duration, [])
57 .then(() => {
58 trace.debug("addAppointment done");
59 return whenRendered();
60 })
61 .then(() => {
62 trace.debug("Render dome");
63 })
64 .catch(error(trace));
56 65 }
57 66
67
58 68 load() {
69 this._context.load().catch(error(trace));
59 70 }
60 71
61 72 destroy() {
@@ -13,6 +13,6
13 13 ],
14 14 "skipLibCheck": true,
15 15 "target": "ES5",
16 "lib": ["ES2015"]
16 "lib": ["ES2015", "DOM"]
17 17 }
18 18 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now