##// 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
@@ -1,14 +1,15
1 1 {
2 2 "java.configuration.updateBuildConfiguration": "automatic",
3 3 "files.exclude": {
4 4 "**/.classpath": true,
5 5 "**/.project": true,
6 6 "**/.settings": true,
7 7 "**/.factorypath": true
8 8 },
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
@@ -1,306 +1,306
1 1 import { Cancellation } from "@implab/core-amd/Cancellation";
2 2 import { ICancellation } from "@implab/core-amd/interfaces";
3 3
4 4 /**
5 5 * The interface for the consumer of an observable sequence
6 6 */
7 7 export interface Observer<T> {
8 8 /**
9 9 * Called for the next element in the sequence
10 10 */
11 11 next: (value: T) => void;
12 12
13 13 /**
14 14 * Called once when the error occurs in the sequence.
15 15 */
16 16 error: (e: unknown) => void;
17 17
18 18 /**
19 19 * Called once at the end of the sequence.
20 20 */
21 21 complete: () => void;
22 22 }
23 23
24 24 /**
25 25 * The group of functions to feed an observable. These methods are provided to
26 26 * the producer to generate a stream of events.
27 27 */
28 28 export type Sink<T> = {
29 29 /**
30 30 * Call to send the next element in the sequence
31 31 */
32 32 next: (value: T) => void;
33 33
34 34 /**
35 35 * Call to notify about the error occurred in the sequence.
36 36 */
37 37 error: (e: unknown) => void;
38 38
39 39 /**
40 40 * Call to signal the end of the sequence.
41 41 */
42 42 complete: () => void;
43 43
44 44 /**
45 45 * Checks whether the sink is accepting new elements. It's safe to
46 46 * send elements to the closed sink.
47 47 */
48 48 isClosed: () => boolean;
49 49 };
50 50
51 51 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
52 52
53 53 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> {
64 64 subscribe(consumer: Partial<Observer<T>>): Unsubscribable;
65 65 }
66 66
67 67 export type AccumulatorFn<T, A> = (acc: A, value: T) => A;
68 68
69 69 /** The observable source of items. */
70 70 export interface Observable<T> extends Subscribable<T> {
71 71 /** Transforms elements of the sequence with the specified mapper
72 72 *
73 73 * @param mapper The mapper used to transform the values
74 74 */
75 75 map<T2>(mapper: (value: T) => T2): Observable<T2>;
76 76
77 77 /** Filters elements of the sequence. The resulting sequence will
78 78 * contain only elements which match the specified predicate.
79 79 *
80 80 * @param predicate The filter predicate.
81 81 */
82 82 filter(predicate: (value: T) => boolean): Observable<T>;
83 83
84 84 /** Applies accumulator to each value in the sequence and
85 85 * emits the accumulated value for each source element
86 86 *
87 87 * @param accumulator
88 88 * @param initial
89 89 */
90 90 scan<A>(accumulator: AccumulatorFn<T, A>, initial: A): Observable<A>;
91 91 scan(accumulator: AccumulatorFn<T, T>): Observable<T>;
92 92
93 93 /** Applies accumulator to each value in the sequence and
94 94 * emits the accumulated value at the end of the sequence
95 95 *
96 96 * @param accumulator
97 97 * @param initial
98 98 */
99 99 reduce<A>(accumulator: AccumulatorFn<T, A>, initial: A): Observable<A>;
100 100 reduce(accumulator: AccumulatorFn<T, T>): Observable<T>;
101 101
102 102 /** Concatenates the specified sequences with this observable
103 103 *
104 104 * @param seq sequences to concatenate with the current observable
105 105 */
106 106 cat(...seq: Subscribable<T>[]): Observable<T>;
107 107
108 108 /** Pipes the specified operator to produce the new observable
109 109 * @param op The operator which consumes this observable and produces a new one
110 110 */
111 111 pipe<U>(op: (source: Observable<T>) => Producer<U>): Observable<U>;
112 112
113 113 /** Waits for the next event to occur and returns a promise for the next value
114 114 * @param ct Cancellation token to
115 115 */
116 116 next(ct?: ICancellation): Promise<T>;
117 117 }
118 118
119 119 const noop = () => { };
120 120
121 121 const sink = <T>(consumer: Partial<Observer<T>>) => {
122 122 const { next, error, complete } = consumer;
123 123 return {
124 124 next: next ? next.bind(consumer) : noop,
125 125 error: error ? error.bind(consumer) : noop,
126 126 complete: complete ? complete.bind(consumer) : noop,
127 127 isClosed: () => false
128 128 };
129 129 };
130 130
131 131 /** Wraps the producer to handle tear down logic and subscription management
132 132 *
133 133 * @param producer The producer to wrap
134 134 * @returns The wrapper producer
135 135 */
136 136 const fuse = <T>(producer: Producer<T>) => ({ next, error, complete }: Sink<T>) => {
137 137 let done = false;
138 138 let cleanup = noop;
139 139
140 140 const _fin = <A extends unknown[]>(fn: (...args: A) => void) =>
141 141 (...args: A) => done ?
142 142 void (0) :
143 143 (done = true, cleanup(), fn(...args));
144 144
145 145 const safeSink = {
146 146 next: (value: T) => { !done && next(value); },
147 147 error: _fin(error),
148 148 complete: _fin(complete),
149 149 isClosed: () => done
150 150 };
151 151 cleanup = producer(safeSink) ?? noop;
152 152 return done ?
153 153 (cleanup(), noop) :
154 154 _fin(noop);
155 155 };
156 156
157 157 const _observe = <T>(producer: Producer<T>): Observable<T> => ({
158 158 subscribe: (consumer: Partial<Observer<T>>) => ({
159 159 unsubscribe: producer(sink(consumer)) ?? noop
160 160 }),
161 161
162 162 map: (mapper) => _observe(({ next, ...rest }) =>
163 163 producer({
164 164 next: next !== noop ? (v: T) => next(mapper(v)) : noop,
165 165 ...rest
166 166 })
167 167 ),
168 168
169 169 filter: (predicate) => _observe(({ next, ...rest }) =>
170 170 producer({
171 171 next: next !== noop ? (v: T) => predicate(v) ? next(v) : void (0) : noop,
172 172 ...rest
173 173 })
174 174 ),
175 175
176 176 scan: <A>(...args: [AccumulatorFn<T, A>, A] | [AccumulatorFn<T, T>]) => _observe<T | A>(({ next, ...rest }) => {
177 177 if (args.length === 1) {
178 178 const [accumulator] = args;
179 179 let _acc: T;
180 180 let index = 0;
181 181 return producer({
182 182 next: next !== noop ? (v: T) => next(index++ === 0 ? _acc = v : _acc = accumulator(_acc, v)) : noop,
183 183 ...rest
184 184 });
185 185 } else {
186 186 const [accumulator, initial] = args;
187 187 let _acc = initial;
188 188 return producer({
189 189 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
190 190 ...rest
191 191 });
192 192 }
193 193 }),
194 194
195 195 reduce: <A>(...args: [AccumulatorFn<T, A>, A] | [AccumulatorFn<T, T>]) => _observe<T | A>(({ next, complete, error, ...rest }) => {
196 196 if (args.length === 1) {
197 197 const [accumulator] = args;
198 198 let _acc: T;
199 199 let index = 0;
200 200 return producer({
201 201 next: next !== noop ? (v: T) => {
202 202 _acc = index++ === 0 ? v : accumulator(_acc, v);
203 203 } : noop,
204 204 complete: () => {
205 205 if (index === 0) {
206 206 error(new Error("The sequence can't be empty"));
207 207 } else {
208 208 next(_acc);
209 209 complete();
210 210 }
211 211 },
212 212 error,
213 213 ...rest
214 214 });
215 215 } else {
216 216 const [accumulator, initial] = args;
217 217 let _acc = initial;
218 218 return producer({
219 219 next: next !== noop ? (v: T) => {
220 220 _acc = accumulator(_acc, v);
221 221 } : noop,
222 222 complete: () => {
223 223 next(_acc);
224 224 complete();
225 225 },
226 226 error,
227 227 ...rest
228 228 });
229 229 }
230 230 }),
231 231
232 232 cat: (...seq) => _observe(({ next, complete: final, ...rest }) => {
233 233 let cleanup: () => void;
234 234 const complete = () => {
235 235 const continuation = seq.shift();
236 236 if (continuation) {
237 237 // if we have a next sequence, subscribe to it
238 238 const subscription = continuation.subscribe({ next, complete, ...rest });
239 239 cleanup = subscription.unsubscribe.bind(subscription);
240 240 } else {
241 241 // otherwise notify the consumer about completion
242 242 final();
243 243 }
244 244 };
245 245
246 246 cleanup = producer({ next, complete, ...rest }) ?? noop;
247 247
248 248 return () => cleanup();
249 249 }),
250 250
251 251 pipe: <U>(op: (source: Observable<T>) => Producer<U>) => observe(op(_observe(producer))),
252 252
253 253 next: (ct?: ICancellation) => {
254 254 const _ct = ct ?? Cancellation.none;
255 255 return new Promise<T>((resolve, reject) => {
256 256 // wrap the producer to handle only single event
257 257 const once = fuse<T>(({ next, complete, error, isClosed }) => {
258 258 const h = _ct.register(error);
259 259
260 260 // is the _ct fires it will call error() and isClosed() will return true
261 261 const cleanup = !isClosed() ?
262 262 producer({
263 263 next: v => (next(v), complete()),
264 264 complete: () => error(new Error("The sequence is empty")),
265 265 error,
266 266 isClosed
267 267 }) ?? noop :
268 268 noop;
269 269
270 270 return () => {
271 271 h.destroy();
272 272 cleanup();
273 273 };
274 274 });
275 275
276 276 once({
277 277 next: resolve,
278 278 error: reject,
279 279 complete: noop,
280 280 isClosed: () => false
281 281 });
282 282 });
283 283 }
284 284 });
285 285
286 286 export const observe = <T>(producer: Producer<T>) => _observe(fuse(producer));
287 287
288 288 export const streamArray = <T>(items: T[]) => _observe<T>(
289 289 ({ next, complete }) => (
290 290 items.forEach(next),
291 291 complete()
292 292 )
293 293 );
294 294
295 295 export const streamPromise = <T>(promise: PromiseLike<T>) => observe<T>(
296 296 ({next, error, complete}) => void promise.then(v => (next(v), complete()), error)
297 297 );
298 298
299 299 export const of = <T>(...items: T[]) => _observe<T>(
300 300 ({ next, complete }) => (
301 301 items.forEach(next),
302 302 complete()
303 303 )
304 304 );
305 305
306 306 export const empty = _observe<never>(({ complete }) => complete()); No newline at end of file
@@ -1,58 +1,58
1 1 import { PromiseOrValue } from "@implab/core-amd/interfaces";
2 2 import { isPromise } from "@implab/core-amd/safe";
3 3 import { observe, Observable } from "./observable";
4 4
5 5 export interface OrderedUpdate<T> {
6 6 /** The item is being updated */
7 7 readonly item: T;
8 8
9 9 /** The previous index of the item, -1 in case it is inserted */
10 10 readonly prevIndex: number;
11 11
12 12 /** The new index of the item, -1 in case it is deleted */
13 13 readonly newIndex: number;
14 14
15 15 }
16 16
17 17 export type QueryResults<T> = Observable<OrderedUpdate<T>>;
18 18
19 19 interface DjObservableResults<T> {
20 20 /**
21 21 * Allows observation of results
22 22 */
23 23 observe(listener: (object: T, previousIndex: number, newIndex: number) => void, includeUpdates?: boolean): {
24 24 remove(): void;
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);
43 43 } else {
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 {
51 51 complete();
52 52 }
53 53 } catch (err) {
54 54 error(err);
55 55 }
56 56 });
57 57
58 58 };
@@ -1,188 +1,188
1 1 import { Constructor } from "@implab/core-amd/interfaces";
2 2 import { HtmlRendition } from "./tsx/HtmlRendition";
3 3 import { WidgetRendition } from "./tsx/WidgetRendition";
4 4 import { isElementNode, isWidget, isWidgetConstructor, Rendition } from "./tsx/traits";
5 5 import { FunctionRendition } from "./tsx/FunctionRendition";
6 6 import Stateful = require("dojo/Stateful");
7 7 import _WidgetBase = require("dijit/_WidgetBase");
8 8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
9 9 import { WatchRendition } from "./tsx/WatchRendition";
10 10 import { Observable, observe, Subscribable } from "./observable";
11 11 import djAttr = require("dojo/dom-attr");
12 12 import djClass = require("dojo/dom-class");
13 13 import { AnimationAttrs, WatchForRendition } from "./tsx/WatchForRendition";
14 14 import { OrderedUpdate } from "./store";
15 15
16 16 export function createElement<T extends Constructor | string | ((props: object) => Element)>(elementType: T, ...args: unknown[]): Rendition {
17 17 if (typeof elementType === "string") {
18 18 const ctx = new HtmlRendition(elementType);
19 19 if (args)
20 20 args.forEach(x => ctx.visitNext(x));
21 21
22 22 return ctx;
23 23 } else if (isWidgetConstructor(elementType)) {
24 24 const ctx = new WidgetRendition(elementType);
25 25 if (args)
26 26 args.forEach(x => ctx.visitNext(x));
27 27
28 28 return ctx;
29 29 } else if (typeof elementType === "function") {
30 30 const ctx = new FunctionRendition(elementType as (props: unknown) => Element);
31 31 if (args)
32 32 args.forEach(x => ctx.visitNext(x));
33 33
34 34 return ctx;
35 35 } else {
36 36 throw new Error(`The element type '${String(elementType)}' is unsupported`);
37 37 }
38 38 }
39 39
40 40 export interface EventDetails<T = unknown> {
41 41 detail: T;
42 42 }
43 43
44 44 export interface EventSelector {
45 45 selectorTarget: HTMLElement;
46 46 target: HTMLElement;
47 47 }
48 48
49 49 export type DojoMouseEvent<T = unknown> = MouseEvent & EventSelector & EventDetails<T>;
50 50
51 51 type StatefulProps<T> = T extends Stateful<infer A> ? A :
52 52 T extends _WidgetBase ? T : never;
53 53
54 54
55 55 /**
56 56 * Observers the property and calls render callback each change.
57 57 *
58 58 * @param target The target object which property will be observed.
59 59 * @param prop The name of the property.
60 60 * @param render The callback which will be called every time the value is changed
61 61 * @returns Rendition which is created instantly
62 62 */
63 63 export function watch<W extends _WidgetBase, K extends keyof W>(
64 64 target: W,
65 65 prop: K,
66 66 render: (model: W[K]) => unknown
67 67 ): Rendition;
68 68 /**
69 69 * Observers the property and calls render callback each change.
70 70 *
71 71 * @param target The target object which property will be observed.
72 72 * @param prop The name of the property.
73 73 * @param render The callback which will be called every time the value is changed
74 74 * @returns Rendition which is created instantly
75 75 */
76 76 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
77 77 target: T,
78 78 prop: K,
79 79 render: (model: StatefulProps<T>[K]) => unknown
80 80 ): Rendition;
81 81 export function watch<V>(subj: Subscribable<V>, render: (model: V) => unknown): Rendition;
82 82 export function watch(
83 83 ...args: [Stateful, string, (model: unknown) => unknown] |
84 84 [Subscribable<unknown>, (model: unknown) => unknown]
85 85 ) {
86 86 if (args.length === 3) {
87 87 const [target, prop, render] = args;
88 88 return new WatchRendition(
89 89 render,
90 90 observe(({next}) => {
91 91 const h = target.watch(
92 92 prop,
93 93 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
94 94 );
95 95 next(target.get(prop));
96 96 return () => h.remove();
97 97 })
98 98 );
99 99 } else {
100 100 const [subj, render] = args;
101 101 return new WatchRendition(render, subj);
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,
109 109 component: render
110 110 });
111 111 };
112 112
113 113
114 114 export const prop: {
115 115 <T extends Stateful, K extends string & keyof StatefulProps<T>>(target: T, name: K): Observable<StatefulProps<T>[K]>;
116 116 <T extends _WidgetBase, K extends keyof T>(target: T, name: K): Observable<T[K]>;
117 117 } = (target: Stateful, name: string) => {
118 118 return observe(({next}) => {
119 119 const h = target.watch(
120 120 name,
121 121 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
122 122 );
123 123 next(target.get(name));
124 124 return () => h.remove();
125 125 });
126 126 };
127 127
128 128 export const attach = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
129 129
130 130 export const bind = <K extends string, T>(attr: K, subj: Subscribable<T>) => {
131 131 let h = { unsubscribe() { } };
132 132
133 133 return (el: Element | { set(name: K, value: T): void; } | undefined) => {
134 134 if (el) {
135 135 if (isElementNode(el)) {
136 136 h = subj.subscribe({
137 137 next: value => djAttr.set(el, attr, value)
138 138 });
139 139 } else {
140 140 h = subj.subscribe({
141 141 next: value => el.set(attr, value)
142 142 });
143 143 }
144 144 } else {
145 145 h.unsubscribe();
146 146 }
147 147 };
148 148 };
149 149
150 150 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
151 151 let h = { unsubscribe() { } };
152 152 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
153 153 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
154 154 if (el) {
155 155 h = subj.subscribe({
156 156 next: v => djClass.toggle(el, className, v)
157 157 });
158 158 } else {
159 159 h.unsubscribe();
160 160 }
161 161 };
162 162 };
163 163
164 164 export const all = <T, A extends JSX.Ref<T>[]>(...cbs: A): JSX.Ref<T> => (arg: T | undefined) => cbs.forEach(cb => cb(arg));
165 165
166 166 /** Decorates the method which will be registered as the handle for the specified event.
167 167 * This decorator can be applied to DjxWidgetBase subclass methods.
168 168 *
169 169 * ```
170 170 * @on("click")
171 171 * _onClick(eventObj: MouseEvent) {
172 172 * // ...
173 173 * }
174 174 * ```
175 175 */
176 176 export const on = <E extends string>(...eventNames: E[]) =>
177 177 <K extends string,
178 178 T extends DjxWidgetBase<object, { [p in E]: EV }>,
179 179 EV extends Event
180 180 >(
181 181 target: T,
182 182 key: K,
183 183 // eslint-disable-next-line @typescript-eslint/no-unused-vars
184 184 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
185 185 ) => {
186 186 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
187 187 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
188 188 };
@@ -1,43 +1,43
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;
7 7 }
8 8
9 9 export class Scope implements IDestroyable, IScope {
10 10 private readonly _cleanup: (() => void)[] = [];
11 11
12 12 static readonly dummy: IScope = { own() { } };
13 13
14 14 own(target: (() => void) | IDestroyable | IRemovable | Unsubscribable) {
15 15 if (target instanceof Function) {
16 16 this._cleanup.push(target);
17 17 } else if (isDestroyable(target)) {
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 }
25 25
26 26 clean() {
27 27 const guard = (cb: () => void) => {
28 28 try {
29 29 cb();
30 30 } catch {
31 31 // guard
32 32 }
33 33 };
34 34
35 35 this._cleanup.forEach(guard);
36 36 this._cleanup.length = 0;
37 37 }
38 38
39 39 destroy() {
40 40 this.clean();
41 41 }
42 42
43 43 } No newline at end of file
@@ -1,204 +1,208
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";
8 8 import { collectNodes, destroy as safeDestroy, isDocumentFragmentNode, isElementNode, isMounted, placeAt, startupWidgets } from "./traits";
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
17 17 interface ItemRendition {
18 18 nodes: Node[];
19 19
20 20 scope: IDestroyable;
21 21
22 22 destroy(): void;
23 23 }
24 24
25 25 interface RenderTask<T> extends OrderedUpdate<T> {
26 26 animate: boolean;
27 27 }
28 28
29 29 export interface AnimationAttrs {
30 30 animate?: boolean;
31 31
32 32 animateIn?: (nodes: Node[]) => Promise<void>;
33 33
34 34 animateOut?: (nodes: Node[]) => Promise<void>;
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 }
42 42
43 43
44 44 const noop = () => { };
45 45
46 46 const fadeIn = (nodes: Node[]) => Promise.all(nodes
47 47 .filter(isElementNode)
48 48 .map(el => play(fx.fadeIn({ node: el as HTMLElement })))
49 49 ).then(noop);
50 50
51 51 const fadeOut = (nodes: Node[]) => Promise.all(nodes
52 52 .filter(isElementNode)
53 53 .map(el => play(fx.fadeOut({ node: el as HTMLElement })))
54 54 ).then(noop);
55 55
56 56
57 57 export class WatchForRendition<T> extends RenditionBase<Node> {
58 58 private readonly _component: (arg: T, index: number) => unknown;
59 59
60 60 private readonly _node: Node;
61 61
62 62 private readonly _itemRenditions: ItemRendition[] = [];
63 63
64 64 private readonly _subject: T[] | Subscribable<OrderedUpdate<T>>;
65 65
66 66 private readonly _renderTasks: RenderTask<T>[] = [];
67 67
68 68 private readonly _animate: boolean;
69 69
70 70 private readonly _animateIn: (nodes: Node[]) => Promise<void>;
71 71
72 72 private readonly _animateOut: (nodes: Node[]) => Promise<void>;
73 73
74 74 private _ct = Cancellation.none;
75 75
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;
87 86 this._animateIn = animateIn ?? fadeIn;
88 87 this._animateOut = animateOut ?? fadeOut;
89 88 }
90 89
91 90 protected _create() {
92 91 const scope = getScope();
93 92 scope.own(() => {
94 93 this._itemRenditions.forEach(safeDestroy);
95 94 safeDestroy(this._node);
96 95 });
97 96
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 })
105 104 });
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++)
113 112 this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate });
114 113 }
115 114 }
116 115 this._ct = new Cancellation(cancel => scope.own(cancel));
117 116 }
118 117
119 118 private readonly _onItemUpdated = (item: RenderTask<T>) => {
120 119 if (!this._renderTasks.length) {
121 120 // schedule a new job
122 121 this._renderTasks.push(item);
123 122 this._render().catch(e => trace.error(e));
124 123 } else {
125 124 // update existing job
126 125 this._renderTasks.push(item);
127 126 }
128 127 };
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>) => {
142 146 const animate = _animate && prevIndex !== newIndex;
143 147
144 148 if (prevIndex > -1) {
145 149 // if we need to delete previous rendition
146 150 const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1);
147 151 if (animate) {
148 152 this._animateOut(nodes)
149 153 .then(destroy)
150 154 .catch(e => trace.error(e));
151 155 } else {
152 156 destroy();
153 157 }
154 158 }
155 159
156 160 if (newIndex > -1) {
157 161 // if we need to create the new rendition
158 162
159 163 // 1. create a new scope for rendering a content
160 164 const scope = new Scope();
161 165
162 166 // 2. render the content
163 167 const itemNode = render(this._component(item, newIndex), scope);
164 168
165 169 // 3. track nodes
166 170 const nodes = isDocumentFragmentNode(itemNode) ?
167 171 collectNodes(itemNode.childNodes) :
168 172 [itemNode];
169 173
170 174 // 5. insert node at the correct position
171 175
172 176 const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] };
173 177
174 178 if (beforeNode)
175 179 placeAt(itemNode, beforeNode, "before");
176 180 else
177 181 placeAt(itemNode, this._node, "before");
178 182
179 183 // 6. store information about rendition
180 184 this._itemRenditions.splice(newIndex, 0, {
181 185 scope,
182 186 nodes,
183 187 destroy: () => {
184 188 scope.destroy();
185 189 nodes.forEach(safeDestroy);
186 190 }
187 191 });
188 192
189 193 // 7. startup widgets if needed
190 194 if (isMounted(this._node))
191 195 nodes.forEach(n => startupWidgets(n));
192 196
193 197 // 8. optionally play the animation
194 198 if (animate)
195 199 this._animateIn(nodes).catch(e => trace.error(e));
196 200 }
197 201 };
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,97 +1,98
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";
8 8 import { Cancellation } from "@implab/core-amd/Cancellation";
9 9 import { collectNodes, destroy, isDocumentFragmentNode, isMounted, placeAt, startupWidgets } from "./traits";
10 10
11 11 const trace = TraceSource.get(mid);
12 12
13 13 export class WatchRendition<T> extends RenditionBase<Node> {
14 14 private readonly _component: (arg: T) => unknown;
15 15
16 16 private readonly _node: Node;
17 17
18 18 private readonly _scope = new Scope();
19 19
20 20 private readonly _subject: Subscribable<T>;
21 21
22 22 private _renderJob?: { value: T };
23 23
24 24 private _ct = Cancellation.none;
25 25
26 26 constructor(component: (arg: T) => unknown, subject: Subscribable<T>) {
27 27 super();
28 28 argumentNotNull(component, "component");
29 29
30 30 this._component = component;
31 31
32 32 this._subject = subject;
33 33
34 34 this._node = document.createComment("[Watch]");
35 35 }
36 36
37 37 protected _create() {
38 38 const scope = getScope();
39 39 scope.own(() => {
40 40 this._scope.destroy();
41 41 destroy(this._node);
42 42 });
43 43 scope.own(this._subject.subscribe({ next: this._onValue }));
44 44 this._ct = new Cancellation(cancel => scope.own(cancel));
45 45 }
46 46
47 47 private readonly _onValue = (value: T) => {
48 48 if (!this._renderJob) {
49 49 // schedule a new job
50 50 this._renderJob = { value };
51 51 this._render().catch(e => trace.error(e));
52 52 } else {
53 53 // update existing job
54 54 this._renderJob = { value };
55 55 }
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() {
91 92 if (!this._node)
92 93 throw new Error("The instance of the widget isn't created");
93 94 return this._node;
94 95 }
95 96
96 97
97 98 }
@@ -1,112 +1,195
1 1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 2 import { isPromise } from "@implab/core-amd/safe";
3 3 import { id as mid } from "module";
4 4 import { IScope, Scope } from "./Scope";
5 5 import { isNode, isRendition, isWidget } from "./traits";
6 6
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();
22 27 if (isPromise(result)) {
23 28 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
24 29 result.then(warn, warn);
25 30 }
26 31 } catch (e) {
27 32 trace.error(e);
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)
54 137 hooks.push(hook);
55 138 else
56 139 guard(hook);
57 140 };
58 141
59 142 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
60 143 const { hooks, scope } = _context;
61 144 if (hooks)
62 145 hooks.push(() => ref(value));
63 146 else
64 147 guard(() => ref(value));
65 148
66 149 scope.own(() => ref(undefined));
67 150 };
68 151
69 152 /** Returns the current scope */
70 153 export const getScope = () => _context.scope;
71 154
72 155 /** Schedules the rendition to be rendered to the DOM Node
73 156 * @param rendition The rendition to be rendered
74 157 * @param scope The scope
75 158 */
76 159 export const render = (rendition: unknown, scope = Scope.dummy) => {
77 160 const complete = beginRender(scope);
78 161 try {
79 162 return getItemDom(rendition);
80 163 } finally {
81 164 complete();
82 165 }
83 166 };
84 167
85 168 /** Renders DOM element for different types of the argument. */
86 169 export const getItemDom = (v: unknown) => {
87 170 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
88 171 // primitive types converted to the text nodes
89 172 return document.createTextNode(v.toString());
90 173 } else if (isNode(v)) {
91 174 // nodes are kept as is
92 175 return v;
93 176 } else if (isRendition(v)) {
94 177 // renditions are instantiated
95 178 return v.getDomNode();
96 179 } else if (isWidget(v)) {
97 180 // widgets are converted to it's markup
98 181 return v.domNode;
99 182 } else if (typeof v === "boolean" || v === null || v === undefined) {
100 183 // null | undefined | boolean are removed
101 184 return document.createDocumentFragment();
102 185 } else if (v instanceof Array) {
103 186 // arrays will be translated to document fragments
104 187 const fragment = document.createDocumentFragment();
105 188 v.map(item => getItemDom(item))
106 189 .forEach(node => fragment.appendChild(node));
107 190 return fragment;
108 191 } else {
109 192 // bug: explicit error otherwise
110 193 throw new Error(`Invalid parameter: ${String(v)}`);
111 194 }
112 195 };
@@ -1,8 +1,17
1 1 import MainWidget from "./view/MainWidget";
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);
8 17 w.load();
@@ -1,75 +1,88
1 1 import Memory = require("dojo/store/Memory");
2 2 import Observable = require("dojo/store/Observable");
3 3 import { Appointment, AppointmentRole, Member } from "./Appointment";
4 4 import { Contact } from "./Contact";
5 5 import { Uuid } from "@implab/core-amd/Uuid";
6 6 import { IDestroyable } from "@implab/core-amd/interfaces";
7 7 import { delay } from "@implab/core-amd/safe";
8 8 import { query } from "@implab/djx/store";
9 9
10 10 type AppointmentRecord = Omit<Appointment, "getMembers"> & { id: string };
11 11
12 12 type ContactRecord = Contact;
13 13
14 14 type MemberRecord = Member & { appointmentId: string; };
15 15
16 16 const item = <T, T2>(map: (x: T) => T2) => <U extends { item: T }>({ item, ...props }: U) => ({ item: map(item), ...props });
17 17
18 18
19 19 export class MainContext implements IDestroyable {
20 20 private readonly _appointments = new Observable(new Memory<AppointmentRecord>());
21 21
22 22 private readonly _contacts = new Observable(new Memory<ContactRecord>());
23 23
24 24 private readonly _members = new Observable(new Memory<MemberRecord>());
25 25
26 26 async createAppointment(title: string, startAt: Date, duration: number, members: Member[]) {
27 27 await delay(1000);
28 28 const id = Uuid();
29 29 this._appointments.add({
30 30 id,
31 31 startAt,
32 32 duration,
33 33 title
34 34 });
35 35
36 36 members.forEach(member =>
37 37 this._members.add({
38 38 appointmentId: id,
39 39 ...member
40 40 }, { id: Uuid() }) as void
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);
47 60
48 61 queryAppointments({ dateFrom, dateTo }: { dateFrom?: Date; dateTo?: Date; } = {}) {
49 62 return this._queryAppointmentsRx(({ startAt }) =>
50 63 (!dateFrom || dateFrom <= startAt) &&
51 64 (!dateTo || startAt <= dateTo)
52 65 ).map(item(this._mapAppointment));
53 66 }
54 67
55 68 async addMember(appointmentId: string, member: Member) {
56 69 await delay(1000);
57 70 this._members.add({
58 71 appointmentId,
59 72 ...member
60 73 });
61 74 }
62 75
63 76 private readonly _mapAppointment = ({ startAt, title, duration, id }: AppointmentRecord) => ({
64 77 id,
65 78 title,
66 79 startAt,
67 80 duration,
68 81 getMembers: (role?: AppointmentRole) => this._queryMembersRx(role ? { appointmentId: id, role } : { appointmentId: id })
69 82 });
70 83
71 84 destroy() {
72 85
73 86 }
74 87
75 88 }
@@ -1,64 +1,75
1 1 import { id as mid } from "module";
2 2 import { BehaviorSubject, Observer, Unsubscribable } from "rxjs";
3 3 import { IDestroyable } from "@implab/core-amd/interfaces";
4 4 import { Observable } from "@implab/djx/observable";
5 5 import { OrderedUpdate } from "@implab/djx/store";
6 6 import { Appointment, Member } from "./Appointment";
7 7 import { MainContext } from "./MainContext";
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
14 15 export interface State {
15 16 appointments: Observable<OrderedUpdate<Appointment>>;
16 17
17 18 dateTo: LocalDate;
18 19
19 20 dateFrom: LocalDate;
20 21
21 22 title: string;
22 23 }
23 24
24 25 export default class MainModel implements IDestroyable {
25 26 private readonly _state: BehaviorSubject<State>;
26 27
27 28 private readonly _context = new MainContext();
28 29
29 30 constructor() {
30 31 this._state = new BehaviorSubject<State>({
31 32 dateTo: LocalDate.now(),
32 33 dateFrom: LocalDate.now().minusMonths(1),
33 34 appointments: this._context.queryAppointments(),
34 35 title: "Appointments"
35 36 });
36 37 }
37 38 getState() {
38 39 return this._state.getValue();
39 40 }
40 41
41 42 subscribe(observer: Partial<Observer<State>>): Unsubscribable {
42 43 return this._state.subscribe(observer);
43 44 }
44 45
45 46 protected dispatch(command: Partial<State>) {
46 47 const state = this.getState();
47 48 this._state.next({ ...state, ...command });
48 49 }
49 50
50 51 addMember(appointmentId: string, member: Member) {
51 52 this._context.addMember(appointmentId, member).catch(error(trace));
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() {
62 73 this._context.destroy();
63 74 }
64 75 } No newline at end of file
@@ -1,18 +1,18
1 1 {
2 2 "compilerOptions": {
3 3 "moduleResolution": "node",
4 4 "experimentalDecorators": true,
5 5 "module": "AMD",
6 6 "jsx": "react",
7 7 "jsxFactory": "createElement",
8 8 "strict": true,
9 9 "types": [
10 10 "requirejs",
11 11 "@implab/djx",
12 12 "@implab/dojo-typings"
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