##// 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 "cSpell.words": [
9 "cSpell.words": [
10 "dijit",
10 "dijit",
11 "djbase",
11 "djbase",
12 "djclass"
12 "djclass",
13 "Unsubscribable"
13 ]
14 ]
14 } No newline at end of file
15 }
@@ -54,10 +54,10 export interface Unsubscribable {
54 unsubscribe(): void;
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 v !== null && v !== undefined && typeof (v as Unsubscribable).unsubscribe === "function";
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 v !== null && v !== undefined && typeof (v as Subscribable<unknown>).subscribe === "function";
61 v !== null && v !== undefined && typeof (v as Subscribable<unknown>).subscribe === "function";
62
62
63 export interface Subscribable<T> {
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[]> {
28 interface Queryable<T, Q, O> {
29 query(...args: A): PromiseOrValue<T[]>;
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 v && (typeof (v as { observe?: unknown; }).observe === "function");
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) =>
35 export const query = <T, Q, O>(store: Queryable<T, Q, O>, includeUpdates = true) =>
36 (...args: A) => {
36 (query?: Q, options?: O & { observe: boolean }) => {
37 return observe<OrderedUpdate<T>>(({ next, complete, error, isClosed }) => {
37 return observe<OrderedUpdate<T>>(({ next, complete, error, isClosed }) => {
38 try {
38 try {
39 const results = store.query(...args);
39 const results = store.query(query, options);
40 if (isPromise(results)) {
40 if (isPromise(results)) {
41 results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })))
41 results.then(items => items.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 })))
42 .then(undefined, error);
42 .then(undefined, error);
@@ -44,7 +44,7 export const query = <T, A extends unkno
44 results.forEach((item, newIndex) => next({ item, newIndex, prevIndex: -1 }));
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 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }), includeUpdates);
48 const h = results.observe((item, prevIndex, newIndex) => next({ item, prevIndex, newIndex }), includeUpdates);
49 return () => h.remove();
49 return () => h.remove();
50 } else {
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 return new WatchForRendition({
106 return new WatchForRendition({
107 ...opts,
107 ...opts,
108 subject: source,
108 subject: source,
@@ -1,6 +1,6
1 import { IDestroyable, IRemovable } from "@implab/core-amd/interfaces";
1 import { IDestroyable, IRemovable } from "@implab/core-amd/interfaces";
2 import { isDestroyable, isRemovable } from "@implab/core-amd/safe";
2 import { isDestroyable, isRemovable } from "@implab/core-amd/safe";
3 import { isUnsubsribable, Unsubscribable } from "../observable";
3 import { isUnsubscribable, Unsubscribable } from "../observable";
4
4
5 export interface IScope {
5 export interface IScope {
6 own(target: (() => void) | IDestroyable | IRemovable | Unsubscribable): void;
6 own(target: (() => void) | IDestroyable | IRemovable | Unsubscribable): void;
@@ -18,7 +18,7 export class Scope implements IDestroyab
18 this._cleanup.push(() => target.destroy());
18 this._cleanup.push(() => target.destroy());
19 } else if (isRemovable(target)) {
19 } else if (isRemovable(target)) {
20 this._cleanup.push(() => target.remove());
20 this._cleanup.push(() => target.remove());
21 } else if (isUnsubsribable(target)) {
21 } else if (isUnsubscribable(target)) {
22 this._cleanup.push(() => target.unsubscribe());
22 this._cleanup.push(() => target.unsubscribe());
23 }
23 }
24 }
24 }
@@ -1,7 +1,7
1 import { id as mid } from "module";
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 import { argumentNotNull } from "@implab/core-amd/safe";
3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render } from "./render";
4 import { getScope, render, scheduleRender } from "./render";
5 import { RenditionBase } from "./RenditionBase";
5 import { RenditionBase } from "./RenditionBase";
6 import { Scope } from "./Scope";
6 import { Scope } from "./Scope";
7 import { Cancellation } from "@implab/core-amd/Cancellation";
7 import { Cancellation } from "@implab/core-amd/Cancellation";
@@ -9,8 +9,8 import { collectNodes, destroy as safeDe
9 import { IDestroyable } from "@implab/core-amd/interfaces";
9 import { IDestroyable } from "@implab/core-amd/interfaces";
10 import { play } from "../play";
10 import { play } from "../play";
11 import * as fx from "dojo/fx";
11 import * as fx from "dojo/fx";
12 import { isSubsribable, Subscribable } from "../observable";
12 import { isSubscribable, Subscribable } from "../observable";
13 import { isObservableResults, OrderedUpdate } from "../store";
13 import { isDjObservableResults, OrderedUpdate } from "../store";
14
14
15 const trace = TraceSource.get(mid);
15 const trace = TraceSource.get(mid);
16
16
@@ -35,7 +35,7 export interface AnimationAttrs {
35 }
35 }
36
36
37 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
37 export interface WatchForRenditionAttrs<T> extends AnimationAttrs {
38 subject: T[] | Subscribable<OrderedUpdate<T>>;
38 subject: T[] | Subscribable<OrderedUpdate<T>> | undefined | null;
39
39
40 component: (arg: T, index: number) => unknown;
40 component: (arg: T, index: number) => unknown;
41 }
41 }
@@ -76,11 +76,10 export class WatchForRendition<T> extend
76 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
76 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
77 super();
77 super();
78 argumentNotNull(component, "component");
78 argumentNotNull(component, "component");
79 argumentNotNull(subject, "component");
80
79
81 this._component = component;
80 this._component = component;
82
81
83 this._subject = subject;
82 this._subject = subject ?? [];
84
83
85 this._node = document.createComment("[WatchFor]");
84 this._node = document.createComment("[WatchFor]");
86 this._animate = !!animate;
85 this._animate = !!animate;
@@ -98,7 +97,7 export class WatchForRendition<T> extend
98 const result = this._subject;
97 const result = this._subject;
99
98
100 if (result) {
99 if (result) {
101 if (isSubsribable<OrderedUpdate<T>>(result)) {
100 if (isSubscribable<OrderedUpdate<T>>(result)) {
102 let animate = false;
101 let animate = false;
103 const subscription = result.subscribe({
102 const subscription = result.subscribe({
104 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
103 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
@@ -106,7 +105,7 export class WatchForRendition<T> extend
106 scope.own(subscription);
105 scope.own(subscription);
107 animate = this._animate;
106 animate = this._animate;
108 } else {
107 } else {
109 if (isObservableResults<T>(result))
108 if (isDjObservableResults<T>(result))
110 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
109 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
111
110
112 for (let i = 0, n = result.length; i < n; i++)
111 for (let i = 0, n = result.length; i < n; i++)
@@ -129,13 +128,18 export class WatchForRendition<T> extend
129
128
130 private async _render() {
129 private async _render() {
131 // fork
130 // fork
132 await Promise.resolve();
131 const beginRender = await scheduleRender();
132 const endRender = beginRender();
133 try {
133 // don't render destroyed rendition
134 // don't render destroyed rendition
134 if (this._ct.isRequested())
135 if (this._ct.isRequested())
135 return;
136 return;
136
137
137 this._renderTasks.forEach(this._onRenderItem);
138 this._renderTasks.forEach(this._onRenderItem);
138 this._renderTasks.length = 0;
139 this._renderTasks.length = 0;
140 } finally {
141 endRender();
142 }
139 }
143 }
140
144
141 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
145 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
@@ -198,7 +202,7 export class WatchForRendition<T> extend
198
202
199 protected _getDomNode() {
203 protected _getDomNode() {
200 if (!this._node)
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 return this._node;
206 return this._node;
203 }
207 }
204 }
208 }
@@ -1,7 +1,7
1 import { id as mid } from "module";
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 import { argumentNotNull } from "@implab/core-amd/safe";
3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render } from "./render";
4 import { getItemDom, getScope, scheduleRender } from "./render";
5 import { RenditionBase } from "./RenditionBase";
5 import { RenditionBase } from "./RenditionBase";
6 import { Scope } from "./Scope";
6 import { Scope } from "./Scope";
7 import { Subscribable } from "../observable";
7 import { Subscribable } from "../observable";
@@ -56,8 +56,9 export class WatchRendition<T> extends R
56 };
56 };
57
57
58 private async _render() {
58 private async _render() {
59 // fork
59 const beginRender = await scheduleRender(this._scope);
60 await Promise.resolve();
60 const endRender = beginRender();
61 try {
61 // don't render destroyed rendition
62 // don't render destroyed rendition
62 if (this._ct.isRequested())
63 if (this._ct.isRequested())
63 return;
64 return;
@@ -66,10 +67,7 export class WatchRendition<T> extends R
66 this._scope.clean();
67 this._scope.clean();
67
68
68 // render the new node
69 // render the new node
69 const node = render(
70 const node = getItemDom(this._renderJob ? this._component(this._renderJob.value) : undefined);
70 this._renderJob ? this._component(this._renderJob.value) : undefined,
71 this._scope
72 );
73
71
74 // get actual content
72 // get actual content
75 const pending = isDocumentFragmentNode(node) ?
73 const pending = isDocumentFragmentNode(node) ?
@@ -85,6 +83,9 export class WatchRendition<T> extends R
85 this._scope.own(() => pending.forEach(destroy));
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 protected _getDomNode() {
91 protected _getDomNode() {
@@ -7,15 +7,20 import { isNode, isRendition, isWidget }
7 const trace = TraceSource.get(mid);
7 const trace = TraceSource.get(mid);
8
8
9 interface Context {
9 interface Context {
10 scope: IScope;
10 readonly scope: IScope;
11
11
12 hooks?: (() => void)[];
12 readonly hooks?: (() => void)[];
13 }
13 }
14
14
15 let _context: Context = {
15 let _context: Context = {
16 scope: Scope.dummy
16 scope: Scope.dummy
17 };
17 };
18
18
19 let _renderCount = 0;
20 let _renderId = 1;
21 let _renderedHooks: (() => void)[] = [];
22
23
19 const guard = (cb: () => unknown) => {
24 const guard = (cb: () => unknown) => {
20 try {
25 try {
21 const result = cb();
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 const prev = _context;
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 _context = {
49 _context = {
34 scope,
50 scope,
35 hooks: []
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 * Completes render operation
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 const { hooks } = _context;
101 const { hooks } = _context;
45 if (hooks)
102 if (hooks)
46 hooks.forEach(guard);
103 hooks.forEach(guard);
47
104
105 _renderCount--;
48 _context = prev;
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 export const renderHook = (hook: () => void) => {
134 export const renderHook = (hook: () => void) => {
52 const { hooks } = _context;
135 const { hooks } = _context;
53 if (hooks)
136 if (hooks)
@@ -2,6 +2,15 import MainWidget from "./view/MainWidge
2 import "@implab/djx/css!dojo/resources/dojo.css";
2 import "@implab/djx/css!dojo/resources/dojo.css";
3 import "@implab/djx/css!dijit/themes/dijit.css";
3 import "@implab/djx/css!dijit/themes/dijit.css";
4 import "@implab/djx/css!dijit/themes/tundra/tundra.css";
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 const w = new MainWidget();
15 const w = new MainWidget();
7 w.placeAt(document.body);
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 private readonly _queryAppointmentsRx = query(this._appointments);
57 private readonly _queryAppointmentsRx = query(this._appointments);
45
58
46 private readonly _queryMembersRx = query(this._members);
59 private readonly _queryMembersRx = query(this._members);
@@ -8,6 +8,7 import { MainContext } from "./MainConte
8 import { LocalDate } from "@js-joda/core";
8 import { LocalDate } from "@js-joda/core";
9 import { error } from "../logging";
9 import { error } from "../logging";
10 import { TraceSource } from "@implab/core-amd/log/TraceSource";
10 import { TraceSource } from "@implab/core-amd/log/TraceSource";
11 import { whenRendered } from "@implab/djx/tsx/render";
11
12
12 const trace = TraceSource.get(mid);
13 const trace = TraceSource.get(mid);
13
14
@@ -52,10 +53,20 export default class MainModel implement
52 }
53 }
53
54
54 addAppointment(title: string, startAt: Date, duration: number) {
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 load() {
68 load() {
69 this._context.load().catch(error(trace));
59 }
70 }
60
71
61 destroy() {
72 destroy() {
@@ -13,6 +13,6
13 ],
13 ],
14 "skipLibCheck": true,
14 "skipLibCheck": true,
15 "target": "ES5",
15 "target": "ES5",
16 "lib": ["ES2015"]
16 "lib": ["ES2015", "DOM"]
17 }
17 }
18 } No newline at end of file
18 }
General Comments 0
You need to be logged in to leave comments. Login now