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