##// END OF EJS Templates
Added priorities to render tasks, revisited rendering scheduler...
cin -
r146:af4f8424e83d v1.9.0 default
parent child
Show More
@@ -1,60 +1,60
1 import { Observable, Sink, Subscribable, observe } from "../observable";
1 import { Observable, Sink, Subscribable, observe } from "../observable";
2
2
3 const noop = () => { };
3 const noop = () => { };
4
4
5 /** Connects multiple subscribers to the single producer. The producer
5 /** Connects multiple subscribers to the single producer. The producer
6 * will be created when the first client subscribes and will be released
6 * will be created when the first client subscribes and will be released
7 * with the the last client unsubscribed.
7 * with the the last client unsubscribed.
8 *
8 *
9 * Use this wrapper to prevent spawning multiple producers.
9 * Use this wrapper to prevent spawning multiple producers.
10 *
10 *
11 * The emitted values are not cached therefore the new subscriber will not receive
11 * The emitted values are not cached therefore new subscribers will not receive
12 * the values emitted before it has been subscribed.
12 * the values emitted before they had subscribed.
13 *
13 *
14 * @param source The source observable
14 * @param source The source observable
15 * @returns The new observable
15 * @returns The new observable
16 */
16 */
17 export const subject = <T>(source: Subscribable<T>): Observable<T> => {
17 export const subject = <T>(source: Subscribable<T>): Observable<T> => {
18 let subscribers: Sink<T>[] = []; // the list of active subscribers
18 let subscribers: Sink<T>[] = []; // the list of active subscribers
19
19
20 let subscription = { unsubscribe: noop }; // current subscription
20 let subscription = { unsubscribe: noop }; // current subscription
21
21
22 // cleanup method to release resources held by this subscription
22 // cleanup method to release resources held by this subscription
23 const cleanup = (cb: (item: Sink<T>) => void) => {
23 const cleanup = (cb: (item: Sink<T>) => void) => {
24 const _subscribers = subscribers;
24 const _subscribers = subscribers;
25 subscribers = []; // this will prevent a client cleanup login to run
25 subscribers = []; // this will prevent a client cleanup login to run
26 _subscribers.forEach(cb);
26 _subscribers.forEach(cb);
27 // we don't need subscription.unsubscribe(), because cleanup is called
27 // we don't need subscription.unsubscribe(), because cleanup is called
28 // from complete or error methods.
28 // from complete or error methods.
29 };
29 };
30
30
31 const sink: Sink<T> = {
31 const sink: Sink<T> = {
32 isClosed: () => false,
32 isClosed: () => false,
33 complete: () => cleanup(s => s.complete()),
33 complete: () => cleanup(s => s.complete()),
34 error: e => cleanup(s => s.error(e)),
34 error: e => cleanup(s => s.error(e)),
35 next: v => subscribers.forEach(s => s.next(v))
35 next: v => subscribers.forEach(s => s.next(v))
36 };
36 };
37
37
38 return observe(client => {
38 return observe(client => {
39 const _subscribers = subscribers;
39 const _subscribers = subscribers;
40 subscribers.push(client);
40 subscribers.push(client);
41 if (subscribers.length === 1) // this is the first client
41 if (subscribers.length === 1) // this is the first client
42 subscription = source.subscribe(sink); // activate the producer
42 subscription = source.subscribe(sink); // activate the producer
43
43
44 return () => {
44 return () => {
45 // this is a cleanup logic for an individual client
45 // this is a cleanup logic for an individual client
46 if (_subscribers === subscribers) {
46 if (_subscribers === subscribers) {
47 // is the current subscription to the producer is active
47 // is the current subscription to the producer is active
48
48
49 // find this client in the list of subscribers
49 // find this client in the list of subscribers
50 const pos = subscribers.indexOf(client);
50 const pos = subscribers.indexOf(client);
51 if (pos >= 0)
51 if (pos >= 0)
52 subscribers.splice(pos, 1);
52 subscribers.splice(pos, 1);
53
53
54 // is this is the last subscriber we need to release the producer
54 // is this is the last subscriber we need to release the producer
55 if (!subscribers.length)
55 if (!subscribers.length)
56 subscription.unsubscribe();
56 subscription.unsubscribe();
57 }
57 }
58 };
58 };
59 });
59 });
60 };
60 };
@@ -1,135 +1,142
1 import { djbase, djclass } from "../declare";
1 import { djbase, djclass } from "../declare";
2 import _WidgetBase = require("dijit/_WidgetBase");
2 import _WidgetBase = require("dijit/_WidgetBase");
3 import _AttachMixin = require("dijit/_AttachMixin");
3 import _AttachMixin = require("dijit/_AttachMixin");
4 import { isNode, isElementNode } from "./traits";
4 import { isNode, isElementNode } from "./traits";
5 import registry = require("dijit/registry");
5 import registry = require("dijit/registry");
6 import on = require("dojo/on");
6 import on = require("dojo/on");
7 import { Scope } from "./Scope";
7 import { Scope } from "./Scope";
8 import { render } from "./render";
8 import { queueRenderTask, getPriority, render } from "./render";
9 import { isNull } from "@implab/core-amd/safe";
9 import { isNull } from "@implab/core-amd/safe";
10
10
11 // type Handle = dojo.Handle;
11 // type Handle = dojo.Handle;
12
12
13 export interface EventArgs {
13 export interface EventArgs {
14 bubbles?: boolean;
14 bubbles?: boolean;
15
15
16 cancelable?: boolean;
16 cancelable?: boolean;
17
17
18 composed?: boolean;
18 composed?: boolean;
19 }
19 }
20
20
21 // eslint-disable-next-line @typescript-eslint/no-unused-vars
21 // eslint-disable-next-line @typescript-eslint/no-unused-vars
22 export interface DjxWidgetBase<Attrs = object, Events extends { [name in keyof Events]: Event } = object> extends
22 export interface DjxWidgetBase<Attrs = object, Events extends { [name in keyof Events]: Event } = object> extends
23 _WidgetBase<Events> {
23 _WidgetBase<Events> {
24
24
25 /** This property is declared only for type inference to work, it is never assigned
25 /** This property is declared only for type inference to work, it is never assigned
26 * and should not be used.
26 * and should not be used.
27 */
27 */
28 readonly _eventMap: Events & GlobalEventHandlersEventMap;
28 readonly _eventMap: Events & GlobalEventHandlersEventMap;
29
29
30 /** The list of pairs of event and method names. When the widget is created all methods from
30 /** The list of pairs of event and method names. When the widget is created all methods from
31 * this list will be connected to corresponding events.
31 * this list will be connected to corresponding events.
32 *
32 *
33 * This property is maintained in the prototype
33 * This property is maintained in the prototype
34 */
34 */
35 _eventHandlers: Array<{
35 _eventHandlers: Array<{
36 eventName: string,
36 eventName: string,
37 handlerMethod: string;
37 handlerMethod: string;
38 }>;
38 }>;
39 }
39 }
40
40
41 type _super = {
41 type _super = {
42 startup(): void;
42 startup(): void;
43
43
44 destroy(preserveDom?: boolean): void;
44 destroy(preserveDom?: boolean): void;
45 };
45 };
46
46
47 @djclass
47 @djclass
48 // eslint-disable-next-line @typescript-eslint/no-unused-vars
48 // eslint-disable-next-line @typescript-eslint/no-unused-vars
49 export abstract class DjxWidgetBase<Attrs = object, Events = object> extends djbase<_super, _AttachMixin>(_WidgetBase, _AttachMixin) {
49 export abstract class DjxWidgetBase<Attrs = object, Events = object> extends djbase<_super, _AttachMixin>(_WidgetBase, _AttachMixin) {
50 private readonly _scope = new Scope();
50 private readonly _scope = new Scope();
51
51
52 private readonly _priority = getPriority() + 1;
53
52 buildRendering() {
54 buildRendering() {
53 const node = render(this.render(), this._scope);
55 const node = render(this.render(), this._scope);
54 if (!isElementNode(node))
56 if (!isElementNode(node))
55 throw new Error("The render method must return a single DOM element");
57 throw new Error("The render method must return a single DOM element");
56 this.domNode = node as HTMLElement;
58 this.domNode = node as HTMLElement;
57
59
58 super.buildRendering();
60 super.buildRendering();
59
61
60 // now we should get assigned data-dojo-attach-points
62 // now we should get assigned data-dojo-attach-points
61 // place the contents of the original srcNode to the containerNode
63 // place the contents of the original srcNode to the containerNode
62 const src = this.srcNodeRef;
64 const src = this.srcNodeRef;
63 const dest = this.containerNode;
65 const dest = this.containerNode;
64
66
65 // the donNode is constructed now we need to connect event handlers
67 // the donNode is constructed now we need to connect event handlers
66 this._connectEventHandlers();
68 this._connectEventHandlers();
67
69
68 if (src && dest) {
70 if (src && dest) {
69 while (src.firstChild)
71 while (src.firstChild)
70 dest.appendChild(src.firstChild);
72 dest.appendChild(src.firstChild);
71 }
73 }
72 }
74 }
73
75
76 /** Schedules a new deferred rendition within the scope of the widget */
77 scheduleRender(task: () => void) {
78 return queueRenderTask(task, this._scope, this._priority);
79 }
80
74 abstract render(): JSX.Element;
81 abstract render(): JSX.Element;
75
82
76 private _connectEventHandlers() {
83 private _connectEventHandlers() {
77 if (this._eventHandlers)
84 if (this._eventHandlers)
78 this._eventHandlers.forEach(({ eventName, handlerMethod }) => {
85 this._eventHandlers.forEach(({ eventName, handlerMethod }) => {
79 const handler = this[handlerMethod as keyof this];
86 const handler = this[handlerMethod as keyof this];
80 if (typeof handler === "function")
87 if (typeof handler === "function")
81 on(this.domNode, eventName, handler.bind(this) as (...args: unknown[]) => unknown);
88 on(this.domNode, eventName, handler.bind(this) as (...args: unknown[]) => unknown);
82 });
89 });
83 }
90 }
84
91
85 _processTemplateNode<T extends (Element | Node | _WidgetBase)>(
92 _processTemplateNode<T extends (Element | Node | _WidgetBase)>(
86 baseNode: T,
93 baseNode: T,
87 getAttrFunc: (baseNode: T, attr: string) => string | undefined,
94 getAttrFunc: (baseNode: T, attr: string) => string | undefined,
88 // tslint:disable-next-line: ban-types
95 // tslint:disable-next-line: ban-types
89 attachFunc: (node: T, type: string, func?: (...args: unknown[]) => unknown) => dojo.Handle
96 attachFunc: (node: T, type: string, func?: (...args: unknown[]) => unknown) => dojo.Handle
90 ): boolean {
97 ): boolean {
91 if (isNode(baseNode)) {
98 if (isNode(baseNode)) {
92 const w = registry.byNode(baseNode);
99 const w = registry.byNode(baseNode);
93 if (w) {
100 if (w) {
94 // from dijit/_WidgetsInTemplateMixin
101 // from dijit/_WidgetsInTemplateMixin
95 this._processTemplateNode(w,
102 this._processTemplateNode(w,
96 (n, p) => {
103 (n, p) => {
97 const v = n.get(p as keyof typeof n);
104 const v = n.get(p as keyof typeof n);
98 return isNull(v) ? undefined : String(v);
105 return isNull(v) ? undefined : String(v);
99 }, // callback to get a property of a widget
106 }, // callback to get a property of a widget
100 (widget, type, callback) => {
107 (widget, type, callback) => {
101 if (!callback)
108 if (!callback)
102 throw new Error("The callback must be specified");
109 throw new Error("The callback must be specified");
103
110
104 // callback to do data-dojo-attach-event to a widget
111 // callback to do data-dojo-attach-event to a widget
105 if (type in widget) {
112 if (type in widget) {
106 // back-compat, remove for 2.0
113 // back-compat, remove for 2.0
107 return widget.connect(widget, type, callback as EventListener);
114 return widget.connect(widget, type, callback as EventListener);
108 } else {
115 } else {
109 // 1.x may never hit this branch, but it's the default for 2.0
116 // 1.x may never hit this branch, but it's the default for 2.0
110 return widget.on(type as keyof GlobalEventHandlersEventMap, callback);
117 return widget.on(type as keyof GlobalEventHandlersEventMap, callback);
111 }
118 }
112
119
113 });
120 });
114 // don't process widgets internals
121 // don't process widgets internals
115 return false;
122 return false;
116 }
123 }
117 }
124 }
118 // eslint-disable-next-line @typescript-eslint/ban-types
125 // eslint-disable-next-line @typescript-eslint/ban-types
119 return super._processTemplateNode(baseNode, getAttrFunc as (baseNode: T, attr: string) => string, attachFunc as (node: T, type: string, func?: Function) => dojo.Handle);
126 return super._processTemplateNode(baseNode, getAttrFunc as (baseNode: T, attr: string) => string, attachFunc as (node: T, type: string, func?: Function) => dojo.Handle);
120 }
127 }
121
128
122 /** Starts current widget and all its supporting widgets (placed outside
129 /** Starts current widget and all its supporting widgets (placed outside
123 * `containerNode`) and child widgets (placed inside `containerNode`)
130 * `containerNode`) and child widgets (placed inside `containerNode`)
124 */
131 */
125 startup() {
132 startup() {
126 // startup supporting widgets
133 // startup supporting widgets
127 registry.findWidgets(this.domNode, this.containerNode).forEach(w => w.startup());
134 registry.findWidgets(this.domNode, this.containerNode).forEach(w => w.startup());
128 super.startup();
135 super.startup();
129 }
136 }
130
137
131 destroy(preserveDom?: boolean) {
138 destroy(preserveDom?: boolean) {
132 this._scope.destroy();
139 this._scope.destroy();
133 super.destroy(preserveDom);
140 super.destroy(preserveDom);
134 }
141 }
135 }
142 }
@@ -1,208 +1,207
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, scheduleRender } from "./render";
4 import { queueRenderTask, getPriority, getScope, render } 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 { isSubscribable, Subscribable } from "../observable";
12 import { isSubscribable, Subscribable } from "../observable";
13 import { isDjObservableResults, 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>> | undefined | null;
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 private _priority = 0;
77
76 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
78 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
77 super();
79 super();
78 argumentNotNull(component, "component");
80 argumentNotNull(component, "component");
79
81
80 this._component = component;
82 this._component = component;
81
83
82 this._subject = subject ?? [];
84 this._subject = subject ?? [];
83
85
84 this._node = document.createComment("[WatchFor]");
86 this._node = document.createComment("[WatchFor]");
85 this._animate = !!animate;
87 this._animate = !!animate;
86 this._animateIn = animateIn ?? fadeIn;
88 this._animateIn = animateIn ?? fadeIn;
87 this._animateOut = animateOut ?? fadeOut;
89 this._animateOut = animateOut ?? fadeOut;
88 }
90 }
89
91
90 protected _create() {
92 protected _create() {
93 this._priority = getPriority() + 1;
91 const scope = getScope();
94 const scope = getScope();
92 scope.own(() => {
95 scope.own(() => {
93 this._itemRenditions.forEach(safeDestroy);
96 this._itemRenditions.forEach(safeDestroy);
94 safeDestroy(this._node);
97 safeDestroy(this._node);
95 });
98 });
96
99
97 const result = this._subject;
100 const result = this._subject;
98
101
99 if (result) {
102 if (result) {
100 if (isSubscribable<OrderedUpdate<T>>(result)) {
103 if (isSubscribable<OrderedUpdate<T>>(result)) {
101 let animate = false;
104 let animate = false;
102 const subscription = result.subscribe({
105 const subscription = result.subscribe({
103 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
106 next: ({ item, prevIndex, newIndex }) => this._onItemUpdated({ item, prevIndex, newIndex, animate })
104 });
107 });
105 scope.own(subscription);
108 scope.own(subscription);
106 animate = this._animate;
109 animate = this._animate;
107 } else {
110 } else {
108 if (isDjObservableResults<T>(result))
111 if (isDjObservableResults<T>(result))
109 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
112 scope.own(result.observe((item, prevIndex, newIndex) => this._onItemUpdated({ item, prevIndex, newIndex, animate: false }), true));
110
113
111 for (let i = 0, n = result.length; i < n; i++)
114 for (let i = 0, n = result.length; i < n; i++)
112 this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate });
115 this._onItemUpdated({ item: result[i], prevIndex: -1, newIndex: i, animate: this._animate });
113 }
116 }
114 }
117 }
115 this._ct = new Cancellation(cancel => scope.own(cancel));
118 this._ct = new Cancellation(cancel => scope.own(cancel));
116 }
119 }
117
120
118 private readonly _onItemUpdated = (item: RenderTask<T>) => {
121 private readonly _onItemUpdated = (item: RenderTask<T>) => {
119 if (!this._renderTasks.length) {
122 if (!this._renderTasks.length) {
120 // schedule a new job
123 // schedule a new job
121 this._renderTasks.push(item);
124 this._renderTasks.push(item);
122 this._render().catch(e => trace.error(e));
125
126 // fork
127 // use dummy scope, because every item will have it's own scope
128 queueRenderTask(this._render, Scope.dummy, this._priority);
123 } else {
129 } else {
124 // update existing job
130 // update existing job
125 this._renderTasks.push(item);
131 this._renderTasks.push(item);
126 }
132 }
127 };
133 };
128
134
129 private async _render() {
135 private readonly _render = () => {
130 // fork
136 // don't render destroyed rendition
131 const beginRender = await scheduleRender();
137 if (this._ct.isRequested())
132 const endRender = beginRender();
138 return;
133 try {
134 // don't render destroyed rendition
135 if (this._ct.isRequested())
136 return;
137
139
138 this._renderTasks.forEach(this._onRenderItem);
140 this._renderTasks.forEach(this._onRenderItem);
139 this._renderTasks.length = 0;
141 this._renderTasks.length = 0;
140 } finally {
142 };
141 endRender();
142 }
143 }
144
143
145 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
144 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
146 const animate = _animate && prevIndex !== newIndex;
145 const animate = _animate && prevIndex !== newIndex;
147
146
148 if (prevIndex > -1) {
147 if (prevIndex > -1) {
149 // if we need to delete previous rendition
148 // if we need to delete previous rendition
150 const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1);
149 const [{ nodes, destroy }] = this._itemRenditions.splice(prevIndex, 1);
151 if (animate) {
150 if (animate) {
152 this._animateOut(nodes)
151 this._animateOut(nodes)
153 .then(destroy)
152 .then(destroy)
154 .catch(e => trace.error(e));
153 .catch(e => trace.error(e));
155 } else {
154 } else {
156 destroy();
155 destroy();
157 }
156 }
158 }
157 }
159
158
160 if (newIndex > -1) {
159 if (newIndex > -1) {
161 // if we need to create the new rendition
160 // if we need to create the new rendition
162
161
163 // 1. create a new scope for rendering a content
162 // 1. create a new scope for rendering a content
164 const scope = new Scope();
163 const scope = new Scope();
165
164
166 // 2. render the content
165 // 2. render the content
167 const itemNode = render(this._component(item, newIndex), scope);
166 const itemNode = render(this._component(item, newIndex), scope);
168
167
169 // 3. track nodes
168 // 3. track nodes
170 const nodes = isDocumentFragmentNode(itemNode) ?
169 const nodes = isDocumentFragmentNode(itemNode) ?
171 collectNodes(itemNode.childNodes) :
170 collectNodes(itemNode.childNodes) :
172 [itemNode];
171 [itemNode];
173
172
174 // 5. insert node at the correct position
173 // 5. insert node at the correct position
175
174
176 const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] };
175 const { nodes: [beforeNode] } = this._itemRenditions[newIndex] ?? { nodes: [] };
177
176
178 if (beforeNode)
177 if (beforeNode)
179 placeAt(itemNode, beforeNode, "before");
178 placeAt(itemNode, beforeNode, "before");
180 else
179 else
181 placeAt(itemNode, this._node, "before");
180 placeAt(itemNode, this._node, "before");
182
181
183 // 6. store information about rendition
182 // 6. store information about rendition
184 this._itemRenditions.splice(newIndex, 0, {
183 this._itemRenditions.splice(newIndex, 0, {
185 scope,
184 scope,
186 nodes,
185 nodes,
187 destroy: () => {
186 destroy: () => {
188 scope.destroy();
187 scope.destroy();
189 nodes.forEach(safeDestroy);
188 nodes.forEach(safeDestroy);
190 }
189 }
191 });
190 });
192
191
193 // 7. startup widgets if needed
192 // 7. startup widgets if needed
194 if (isMounted(this._node))
193 if (isMounted(this._node))
195 nodes.forEach(n => startupWidgets(n));
194 nodes.forEach(n => startupWidgets(n));
196
195
197 // 8. optionally play the animation
196 // 8. optionally play the animation
198 if (animate)
197 if (animate)
199 this._animateIn(nodes).catch(e => trace.error(e));
198 this._animateIn(nodes).catch(e => trace.error(e));
200 }
199 }
201 };
200 };
202
201
203 protected _getDomNode() {
202 protected _getDomNode() {
204 if (!this._node)
203 if (!this._node)
205 throw new Error("The instance of the rendition isn't created");
204 throw new Error("The instance of the rendition isn't created");
206 return this._node;
205 return this._node;
207 }
206 }
208 }
207 }
@@ -1,98 +1,93
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 import { argumentNotNull } from "@implab/core-amd/safe";
1 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getItemDom, getScope, scheduleRender } from "./render";
2 import { queueRenderTask, getItemDom, getPriority, getScope } from "./render";
5 import { RenditionBase } from "./RenditionBase";
3 import { RenditionBase } from "./RenditionBase";
6 import { Scope } from "./Scope";
4 import { Scope } from "./Scope";
7 import { Subscribable } from "../observable";
5 import { Subscribable } from "../observable";
8 import { Cancellation } from "@implab/core-amd/Cancellation";
6 import { Cancellation } from "@implab/core-amd/Cancellation";
9 import { collectNodes, destroy, isDocumentFragmentNode, isMounted, placeAt, startupWidgets } from "./traits";
7 import { collectNodes, destroy, isDocumentFragmentNode, isMounted, placeAt, startupWidgets } from "./traits";
10
8
11 const trace = TraceSource.get(mid);
12
13 export class WatchRendition<T> extends RenditionBase<Node> {
9 export class WatchRendition<T> extends RenditionBase<Node> {
14 private readonly _component: (arg: T) => unknown;
10 private readonly _component: (arg: T) => unknown;
15
11
16 private readonly _node: Node;
12 private readonly _node: Node;
17
13
18 private readonly _scope = new Scope();
14 private readonly _scope = new Scope();
19
15
20 private readonly _subject: Subscribable<T>;
16 private readonly _subject: Subscribable<T>;
21
17
22 private _renderJob?: { value: T };
18 private _renderJob?: { value: T };
23
19
24 private _ct = Cancellation.none;
20 private _ct = Cancellation.none;
25
21
22 private _priority = 0;
23
26 constructor(component: (arg: T) => unknown, subject: Subscribable<T>) {
24 constructor(component: (arg: T) => unknown, subject: Subscribable<T>) {
27 super();
25 super();
28 argumentNotNull(component, "component");
26 argumentNotNull(component, "component");
29
27
30 this._component = component;
28 this._component = component;
31
29
32 this._subject = subject;
30 this._subject = subject;
33
31
34 this._node = document.createComment("[Watch]");
32 this._node = document.createComment("[Watch]");
35 }
33 }
36
34
37 protected _create() {
35 protected _create() {
38 const scope = getScope();
36 const scope = getScope();
37 this._priority = getPriority() + 1;
38
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 queueRenderTask(this._render, this._scope, this._priority);
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 readonly _render = () => {
59 const beginRender = await scheduleRender(this._scope);
59
60 const endRender = beginRender();
60 // don't render destroyed rendition
61 try {
61 if (this._ct.isRequested())
62 // don't render destroyed rendition
62 return;
63 if (this._ct.isRequested())
64 return;
65
63
66 // remove all previous content
64 // remove all previous content
67 this._scope.clean();
65 this._scope.clean();
68
66
69 // render the new node
67 // render the new node
70 const node = getItemDom(this._renderJob ? this._component(this._renderJob.value) : undefined);
68 const node = getItemDom(this._renderJob ? this._component(this._renderJob.value) : undefined);
71
69
72 // get actual content
70 // get actual content
73 const pending = isDocumentFragmentNode(node) ?
71 const pending = isDocumentFragmentNode(node) ?
74 collectNodes(node.childNodes) :
72 collectNodes(node.childNodes) :
75 [node];
73 [node];
76
74
77 placeAt(node, this._node, "after");
75 placeAt(node, this._node, "after");
78
76
79 if (isMounted(this._node))
77 if (isMounted(this._node))
80 pending.forEach(n => startupWidgets(n));
78 pending.forEach(n => startupWidgets(n));
81
82 if (pending.length)
83 this._scope.own(() => pending.forEach(destroy));
84
79
85 this._renderJob = undefined;
80 if (pending.length)
86 } finally {
81 this._scope.own(() => pending.forEach(destroy));
87 endRender();
82
88 }
83 this._renderJob = undefined;
89 }
84 };
90
85
91 protected _getDomNode() {
86 protected _getDomNode() {
92 if (!this._node)
87 if (!this._node)
93 throw new Error("The instance of the widget isn't created");
88 throw new Error("The instance of the widget isn't created");
94 return this._node;
89 return this._node;
95 }
90 }
96
91
97
92
98 }
93 }
@@ -1,200 +1,334
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";
3 import { id as mid } from "module";
2 import { id as mid } from "module";
4 import { IScope, Scope } from "./Scope";
3 import { IScope, Scope } from "./Scope";
5 import { isNode, isRendition, isWidget } from "./traits";
4 import { isNode, isRendition, isWidget } from "./traits";
6
5
7 const trace = TraceSource.get(mid);
6 const trace = TraceSource.get(mid);
8
7
9 interface Context {
8 interface Context {
10 readonly scope: IScope;
9 readonly scope: IScope;
11
10
11 readonly priority: number;
12
12 readonly hooks?: (() => void)[];
13 readonly hooks?: (() => void)[];
13 }
14 }
14
15
15 let _context: Context = {
16 type RenderTask = {
16 scope: Scope.dummy
17 /**
18 * The priority for this task
19 */
20 readonly priority: number,
21
22 /**
23 * The rendering action performed in this task
24 */
25 readonly render: () => void;
26 };
27
28 type Range = {
29 /** minimum value in this range */
30 readonly min: number;
31 /** maximum value in this range */
32 readonly max: number;
17 };
33 };
18
34
35 // empty range
36 const emptyPriorities: Range = { min: NaN, max: NaN };
37
38 // holds render tasks
39 let _renderQueue: RenderTask[] = [];
40
41 // holds the minimum and the maximum task priorities in the queue. Used to
42 // optimize rendering process is all tasks are with the same priority.
43 let _renderQueuePriorities: Range = emptyPriorities;
44
45 // current context
46 let _context: Context = {
47 scope: Scope.dummy,
48 priority: 0
49 };
50
51 // started render operations
19 let _renderCount = 0;
52 let _renderCount = 0;
53
54 // next id for render operations
20 let _renderId = 1;
55 let _renderId = 1;
56
57 // hooks for render completion, executed when all render operations has
58 // been completed
21 let _renderedHooks: (() => void)[] = [];
59 let _renderedHooks: (() => void)[] = [];
22
60
23
61 const guard = (cb: () => void) => {
24 const guard = (cb: () => unknown) => {
25 try {
62 try {
26 const result = cb();
63 cb();
27 if (isPromise(result)) {
28 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
29 result.then(warn, warn);
30 }
31 } catch (e) {
64 } catch (e) {
32 trace.error(e);
65 trace.error(e);
33 }
66 }
34 };
67 };
35
68
36 /**
69 /**
70 * Creates a new rendition context with the specified parameters and makes it
71 * an active context.
37 *
72 *
38 * @param scope
73 * @see getScope
39 * @returns
74 * @see getPriority
40 */
41 export const beginRender = (scope = getScope()) => {
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
49 _context = {
50 scope,
51 hooks: []
52 };
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 *
75 *
63 * const begin = await scheduleRender();
76 * @param scope The scope for the current context
64 * const end = begin();
77 * @param priority The priority for the current context
65 * try {
78 * @returns The function to restore the previous context and execute pending hooks
66 * // do some DOM manipulations
67 * } finally {
68 * end();
69 * }
70 *
71 * @param scope
72 * @returns
73 */
79 */
74 export const scheduleRender = async (scope = getScope()) => {
80 const enterContext = (scope: IScope, priority: number) => {
75 _renderCount++;
81 const prev = _context;
76 const renderId = _renderId ++;
82 const captured = _context = { scope, priority, hooks: [] };
77 trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
83 return () => {
78 if (_renderCount === 1)
84 if (_context !== captured)
79 onRendering();
85 trace.error("endRender mismatched beginRender call");
80
81 await Promise.resolve();
82
86
83 return () => {
87 const { hooks } = _context;
84 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
88 if (hooks)
85 const prev = _context;
89 hooks.forEach(guard);
86
90
87 _context = {
91 _context = prev;
88 scope,
89 hooks: []
90 };
91 return endRender(prev, _context, renderId);
92 };
92 };
93 };
93 };
94
94
95 /**
95 /**
96 * Completes render operation
96 * Starts the new render operation. When the operation is started the counter
97 * of running operations is increased. If this is the first render operation
98 * then the `onRendering` event is fired.
99 *
100 * @returns The id of the started rendering operation.
97 */
101 */
98 const endRender = (prev: Context, current: Context, renderId: number) => () => {
102 const startRender = () => {
99 if (_context !== current)
103 _renderCount++;
100 trace.error("endRender mismatched beginRender call");
104 if (_renderCount === 1)
105 onRendering();
101
106
102 const { hooks } = _context;
107 return _renderId++;
103 if (hooks)
108 };
104 hooks.forEach(guard);
105
109
110 /**
111 * Completes the rendering operation. When the operation is completed the counter
112 * of running operations is decreased. If there is no more running operations left
113 * then the `onRendered` event is fired.
114 *
115 */
116 const completeRender = () => {
106 _renderCount--;
117 _renderCount--;
107 _context = prev;
108
109 trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
110 if (_renderCount === 0)
118 if (_renderCount === 0)
111 onRendered();
119 onRendered();
112 };
120 };
113
121
122 /**
123 * Invokes the specified within the rendition context. The rendition context
124 * is created for the task invocation and restored after the task is competed.
125 *
126 * @param scope The cope for the rendition context
127 * @param priority The priority for the rendition context
128 * @returns The result returned by the task
129 */
130 export const renderTask = <T>(task: () => T, scope: IScope, priority: number, renderId: number) => {
131 const restoreContext = enterContext(scope, priority);
132 try {
133 trace.debug("beginRender [{0}], priority = {1}", renderId, priority);
134 return task();
135 } finally {
136 trace.debug("endRender [{0}]", renderId);
137 restoreContext();
138 completeRender();
139 }
140 };
141
142 const processRenderQueue = () => {
143 const q = _renderQueue;
144 const { min, max } = _renderQueuePriorities;
145
146 _renderQueue = [];
147 _renderQueuePriorities = emptyPriorities;
148
149 // all tasks scheduled due queue processing will be queued to the next
150 // processRenderQueue iteration
151 if (min !== max) {
152 // if there are tasks with different priorities in the queue
153 trace.debug("Processing render queue, {0} tasks, priorities=[{1}..{2}] ", q.length, min, max);
154 q.sort(({ priority: a }, { priority: b }) => a - b).forEach(({ render }) => guard(render));
155 } else {
156 // all tasks are have same priority
157 trace.debug("Processing render queue, {0} tasks, priority = {1} ", q.length, min);
158 q.forEach(({ render }) => guard(render));
159 }
160
161 if (_renderQueue.length)
162 trace.debug("Render queue is processed, {0} tasks rescheduled", _renderQueue.length);
163 };
164
165 /**
166 * Adds the specified task to the render queue. The task will be added with the
167 * specified priority.
168 *
169 * Render queue contains a list of render tasks. Each task is executed within
170 * its own rendering context with the specified scope. The priority determines
171 * the order in which tasks will be executed where the tasks with lower priority
172 * numbers are executed first.
173 *
174 * When the queue is empty and the task is added then the render queue will be
175 * scheduled for execution. While the current queue is being executed all
176 * enqueued tasks are added to the new queue and processed after the current
177 * execution has been completed.
178 *
179 * @param task The action to execute. This action will be executed with
180 * {@link renderTask} function in its own rendering context.
181 * @param scope The scope used to create a rendering context for the task.
182 * @param priority The priority
183 */
184 export const queueRenderTask = (task: () => void, scope = Scope.dummy, priority = getPriority()) => {
185 const renderId = startRender();
186 trace.debug("scheduleRender [{0}], priority = {1}", renderId, priority);
187
188 const render = () => renderTask(task, scope, priority, renderId);
189
190 if (!_renderQueue.length) {
191 // this is the first task, schedule next render queue processing
192 Promise.resolve().then(processRenderQueue, e => trace.error(e));
193
194 // initialize priorities
195 _renderQueuePriorities = { min: priority, max: priority };
196 } else {
197
198 // update priorities if needed
199 const { min, max } = _renderQueuePriorities;
200 if (priority < min)
201 _renderQueuePriorities = { min: priority, max };
202 else if (priority > max)
203 _renderQueuePriorities = { min, max: priority };
204 }
205
206 _renderQueue.push({ priority, render });
207 };
208
209 /**
210 * Starts the synchronous rendering process with the specified scope and priority.
211 *
212 * @param scope The scope for the current rendition
213 * @param priority The priority for the current scope
214 * @returns The function to complete the current rendering
215 */
216 export const beginRender = (scope = Scope.dummy, priority = 0) => {
217 const renderId = startRender();
218 const restoreContext = enterContext(scope, priority);
219 trace.debug("beginRender [{0}], priority = {1}", renderId, priority);
220
221 return () => {
222 trace.debug("endRender [{0}]", renderId);
223 restoreContext();
224 completeRender();
225 };
226 };
227
114 // called when the first beginRender is called for this iteration
228 // called when the first beginRender is called for this iteration
115 const onRendering = () => {
229 const onRendering = () => {
116 trace.log("Rendering started");
230 trace.log("Rendering started");
117 setTimeout(() => {
231 setTimeout(() => {
118 if (_renderCount !== 0)
232 if (_renderCount !== 0)
119 trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
233 trace.error("Rendering tasks aren't finished, currently running = {0}", _renderCount);
120 });
234 });
121 };
235 };
122
236
123 // called when all render operations are complete
237 // called when all render operations are complete
124 const onRendered = () => {
238 const onRendered = () => {
125 trace.log("Rendering compete");
239 trace.log("Rendering compete");
126 _renderedHooks.forEach(guard);
240 _renderedHooks.forEach(guard);
127 _renderedHooks = [];
241 _renderedHooks = [];
128 };
242 };
129
243
244 /** Returns promise when the rendering has been completed. */
130 export const whenRendered = () => new Promise<void>((resolve) => {
245 export const whenRendered = () => new Promise<void>((resolve) => {
131 if (_renderCount)
246 if (_renderCount)
132 _renderedHooks.push(resolve);
247 _renderedHooks.push(resolve);
133 else
248 else
134 resolve();
249 resolve();
135 });
250 });
136
251
252 /**
253 * Registers hook which is called after the render operation is completed. The
254 * hook will be called once only for the current operation.
255 *
256 * @param hook The hook which should be called when rendering is complete.
257 */
137 export const renderHook = (hook: () => void) => {
258 export const renderHook = (hook: () => void) => {
138 const { hooks } = _context;
259 const { hooks } = _context;
139 if (hooks)
260 if (hooks)
140 hooks.push(hook);
261 hooks.push(hook);
141 else
262 else
142 guard(hook);
263 guard(hook);
143 };
264 };
144
265
266 /**
267 * Registers special hook which will be called with the specified state. The
268 * hook is called once after the rendering is complete. When the rendition is
269 * destroyed the hook is called with the undefined parameter.
270 *
271 * This function is used to register `ref` hooks form a tsx rendition.
272 *
273 * @param value The state which will be supplied as a parameter for the hook
274 * @param ref reference hook
275 */
145 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
276 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
146 const { hooks, scope } = _context;
277 const { hooks, scope } = _context;
147 if (hooks)
278 if (hooks)
148 hooks.push(() => ref(value));
279 hooks.push(() => ref(value));
149 else
280 else
150 guard(() => ref(value));
281 guard(() => ref(value));
151
282
152 scope.own(() => ref(undefined));
283 scope.own(() => ref(undefined));
153 };
284 };
154
285
155 /** Returns the current scope */
286 /** Returns the current scope. Scope is used to track resources bound to the
287 * current rendering. When the rendering is destroyed the scope is cleaned and
288 * all bound resources are released.
289 */
156 export const getScope = () => _context.scope;
290 export const getScope = () => _context.scope;
157
291
292 /**
293 * Returns the current render task priority. This value is used by some renditions
294 * to schedule asynchronous nested updates with lower priority then themselves.
295 */
296 export const getPriority = () => _context.priority;
297
158 /** Schedules the rendition to be rendered to the DOM Node
298 /** Schedules the rendition to be rendered to the DOM Node
159 * @param rendition The rendition to be rendered
299 * @param rendition The rendition to be rendered
160 * @param scope The scope
300 * @param scope The scope
161 */
301 */
162 export const render = (rendition: unknown, scope = Scope.dummy) => {
302 export const render = (rendition: unknown, scope = Scope.dummy) =>
163 const complete = beginRender(scope);
303 renderTask(() => getItemDom(rendition), scope, getPriority(), startRender());
164 try {
165 return getItemDom(rendition);
166 } finally {
167 complete();
168 }
169 };
170
304
171 const emptyFragment = document.createDocumentFragment();
305 const emptyFragment = document.createDocumentFragment();
172
306
173 /** Renders DOM element for different types of the argument. */
307 /** Renders DOM element for different types of the argument. */
174 export const getItemDom = (v: unknown) => {
308 export const getItemDom = (v: unknown) => {
175 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
309 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
176 // primitive types converted to the text nodes
310 // primitive types converted to the text nodes
177 return document.createTextNode(v.toString());
311 return document.createTextNode(v.toString());
178 } else if (isNode(v)) {
312 } else if (isNode(v)) {
179 // nodes are kept as is
313 // nodes are kept as is
180 return v;
314 return v;
181 } else if (isRendition(v)) {
315 } else if (isRendition(v)) {
182 // renditions are instantiated
316 // renditions are instantiated
183 return v.getDomNode();
317 return v.getDomNode();
184 } else if (isWidget(v)) {
318 } else if (isWidget(v)) {
185 // widgets are converted to it's markup
319 // widgets are converted to it's markup
186 return v.domNode;
320 return v.domNode;
187 } else if (typeof v === "boolean" || v === null || v === undefined) {
321 } else if (typeof v === "boolean" || v === null || v === undefined) {
188 // null | undefined | boolean are removed
322 // null | undefined | boolean are removed
189 return emptyFragment;
323 return emptyFragment;
190 } else if (v instanceof Array) {
324 } else if (v instanceof Array) {
191 // arrays will be translated to document fragments
325 // arrays will be translated to document fragments
192 const fragment = document.createDocumentFragment();
326 const fragment = document.createDocumentFragment();
193 v.map(item => getItemDom(item))
327 v.map(item => getItemDom(item))
194 .forEach(node => fragment.appendChild(node));
328 .forEach(node => fragment.appendChild(node));
195 return fragment;
329 return fragment;
196 } else {
330 } else {
197 // bug: explicit error otherwise
331 // bug: explicit error otherwise
198 throw new Error(`Invalid parameter: ${String(v)}`);
332 throw new Error(`Invalid parameter: ${String(v)}`);
199 }
333 }
200 };
334 };
@@ -1,88 +1,106
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 removeAppointment(appointmentId: string) {
45 await delay(10);
46 this._members.query({ appointmentId })
47 .map(m => this._members.getIdentity(m))
48 .forEach(id => this._members.remove(id));
49 this._appointments.remove(appointmentId);
50 }
51
44 async load() {
52 async load() {
45 await Promise.resolve();
53 await delay(10);
46 for (let i = 0; i < 2; i++) {
54 for (let i = 0; i < 5; i++) {
47 const id = Uuid();
55 const id = Uuid();
48 this._appointments.add({
56 this._appointments.add({
49 id,
57 id,
50 startAt: new Date(),
58 startAt: new Date(),
51 duration: 30,
59 duration: 30,
52 title: `Hello ${i+1}`
60 title: `Hello ${i + 1}`
53 });
61 });
62
63 for (let ii = 0; ii < 3; ii++)
64
65 this._members.add({
66 appointmentId: id,
67 email: "some@no.mail",
68 name: `Peter ${ii}`,
69 position: "Manager",
70 role: "participant"
71 }, { id: Uuid() });
54 }
72 }
55 }
73 }
56
74
57 private readonly _queryAppointmentsRx = query(this._appointments);
75 private readonly _queryAppointmentsRx = query(this._appointments);
58
76
59 private readonly _queryMembersRx = query(this._members);
77 private readonly _queryMembersRx = query(this._members);
60
78
61 queryAppointments({ dateFrom, dateTo }: { dateFrom?: Date; dateTo?: Date; } = {}) {
79 queryAppointments({ dateFrom, dateTo }: { dateFrom?: Date; dateTo?: Date; } = {}) {
62 return this._queryAppointmentsRx(({ startAt }) =>
80 return this._queryAppointmentsRx(({ startAt }) =>
63 (!dateFrom || dateFrom <= startAt) &&
81 (!dateFrom || dateFrom <= startAt) &&
64 (!dateTo || startAt <= dateTo)
82 (!dateTo || startAt <= dateTo)
65 ).map(item(this._mapAppointment));
83 ).map(item(this._mapAppointment));
66 }
84 }
67
85
68 async addMember(appointmentId: string, member: Member) {
86 async addMember(appointmentId: string, member: Member) {
69 await delay(1000);
87 await delay(1000);
70 this._members.add({
88 this._members.add({
71 appointmentId,
89 appointmentId,
72 ...member
90 ...member
73 });
91 });
74 }
92 }
75
93
76 private readonly _mapAppointment = ({ startAt, title, duration, id }: AppointmentRecord) => ({
94 private readonly _mapAppointment = ({ startAt, title, duration, id }: AppointmentRecord) => ({
77 id,
95 id,
78 title,
96 title,
79 startAt,
97 startAt,
80 duration,
98 duration,
81 getMembers: (role?: AppointmentRole) => this._queryMembersRx(role ? { appointmentId: id, role } : { appointmentId: id })
99 getMembers: (role?: AppointmentRole) => this._queryMembersRx(role ? { appointmentId: id, role } : { appointmentId: id })
82 });
100 });
83
101
84 destroy() {
102 destroy() {
85
103
86 }
104 }
87
105
88 }
106 }
@@ -1,75 +1,79
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 import { whenRendered } from "@implab/djx/tsx/render";
12
12
13 const trace = TraceSource.get(mid);
13 const trace = TraceSource.get(mid);
14
14
15 export interface State {
15 export interface State {
16 appointments: Observable<OrderedUpdate<Appointment>>;
16 appointments: Observable<OrderedUpdate<Appointment>>;
17
17
18 dateTo: LocalDate;
18 dateTo: LocalDate;
19
19
20 dateFrom: LocalDate;
20 dateFrom: LocalDate;
21
21
22 title: string;
22 title: string;
23 }
23 }
24
24
25 export default class MainModel implements IDestroyable {
25 export default class MainModel implements IDestroyable {
26 private readonly _state: BehaviorSubject<State>;
26 private readonly _state: BehaviorSubject<State>;
27
27
28 private readonly _context = new MainContext();
28 private readonly _context = new MainContext();
29
29
30 constructor() {
30 constructor() {
31 this._state = new BehaviorSubject<State>({
31 this._state = new BehaviorSubject<State>({
32 dateTo: LocalDate.now(),
32 dateTo: LocalDate.now(),
33 dateFrom: LocalDate.now().minusMonths(1),
33 dateFrom: LocalDate.now().minusMonths(1),
34 appointments: this._context.queryAppointments(),
34 appointments: this._context.queryAppointments(),
35 title: "Appointments"
35 title: "Appointments"
36 });
36 });
37 }
37 }
38 getState() {
38 getState() {
39 return this._state.getValue();
39 return this._state.getValue();
40 }
40 }
41
41
42 subscribe(observer: Partial<Observer<State>>): Unsubscribable {
42 subscribe(observer: Partial<Observer<State>>): Unsubscribable {
43 return this._state.subscribe(observer);
43 return this._state.subscribe(observer);
44 }
44 }
45
45
46 protected dispatch(command: Partial<State>) {
46 protected dispatch(command: Partial<State>) {
47 const state = this.getState();
47 const state = this.getState();
48 this._state.next({ ...state, ...command });
48 this._state.next({ ...state, ...command });
49 }
49 }
50
50
51 addMember(appointmentId: string, member: Member) {
51 addMember(appointmentId: string, member: Member) {
52 this._context.addMember(appointmentId, member).catch(error(trace));
52 this._context.addMember(appointmentId, member).catch(error(trace));
53 }
53 }
54
54
55 addAppointment(title: string, startAt: Date, duration: number) {
55 addAppointment(title: string, startAt: Date, duration: number) {
56 this._context.createAppointment(title,startAt, duration, [])
56 this._context.createAppointment(title,startAt, duration, [])
57 .then(() => {
57 .then(() => {
58 trace.debug("addAppointment done");
58 trace.debug("addAppointment done");
59 return whenRendered();
59 return whenRendered();
60 })
60 })
61 .then(() => {
61 .then(() => {
62 trace.debug("Render dome");
62 trace.debug("Render dome");
63 })
63 })
64 .catch(error(trace));
64 .catch(error(trace));
65 }
65 }
66
66
67 removeAppointment(appointmentId: string) {
68 this._context.removeAppointment(appointmentId).catch(error(trace));
69 }
70
67
71
68 load() {
72 load() {
69 this._context.load().catch(error(trace));
73 this._context.load().catch(error(trace));
70 }
74 }
71
75
72 destroy() {
76 destroy() {
73 this._context.destroy();
77 this._context.destroy();
74 }
78 }
75 } No newline at end of file
79 }
@@ -1,77 +1,87
1 import { id as mid } from "module";
1 import { djbase, djclass } from "@implab/djx/declare";
2 import { djbase, djclass } from "@implab/djx/declare";
2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 import { attach, bind, createElement, prop, watch, watchFor } from "@implab/djx/tsx";
4 import { attach, bind, createElement, prop, watch, watchFor } from "@implab/djx/tsx";
4 import MainModel from "../model/MainModel";
5 import MainModel from "../model/MainModel";
5 import { Observable } from "@implab/djx/observable";
6 import { Observable } from "@implab/djx/observable";
6 import { OrderedUpdate } from "@implab/djx/store";
7 import { OrderedUpdate } from "@implab/djx/store";
7 import { Appointment } from "../model/Appointment";
8 import { Appointment } from "../model/Appointment";
8 import { LocalDate } from "@js-joda/core";
9 import { LocalDate } from "@js-joda/core";
9 import Button = require("dijit/form/Button");
10 import Button = require("dijit/form/Button");
10 import NewAppointment from "./NewAppointment";
11 import NewAppointment from "./NewAppointment";
12 import { TraceSource } from "@implab/core-amd/log/TraceSource";
13
14 const trace = TraceSource.get(mid);
11
15
12 @djclass
16 @djclass
13 export default class MainWidget extends djbase(DjxWidgetBase) {
17 export default class MainWidget extends djbase(DjxWidgetBase) {
14
18
15 appointments?: Observable<OrderedUpdate<Appointment>>;
19 appointments?: Observable<OrderedUpdate<Appointment>>;
16
20
17 model: MainModel;
21 model: MainModel;
18
22
19 dateTo?: LocalDate;
23 dateTo?: LocalDate;
20
24
21 dateFrom?: LocalDate;
25 dateFrom?: LocalDate;
22
26
23 toolbarNode?: HTMLDivElement;
27 toolbarNode?: HTMLDivElement;
24
28
25 constructor(opts?: Partial<MainWidget> & ThisType<MainWidget>, srcNode?: string | Node) {
29 constructor(opts?: Partial<MainWidget> & ThisType<MainWidget>, srcNode?: string | Node) {
26 super(opts, srcNode);
30 super(opts, srcNode);
27
31
28 const model = this.model = new MainModel();
32 const model = this.model = new MainModel();
29 this.own(model);
33 this.own(model);
30 model.subscribe({ next: x => this.set(x) });
34 model.subscribe({ next: x => this.set(x) });
31 }
35 }
32
36
33
37
34 render() {
38 render() {
35
39
36 return <div className="tundra">
40 return <div className="tundra">
37 <h2 ref={bind("innerHTML", prop(this, "title"))} />
41 <h2 ref={bind("innerHTML", prop(this, "title"))} />
38 {watch(prop(this, "appointments"), items => items &&
42 {watch(prop(this, "appointments"), items => items &&
39 <ul>
43 <ul>
40 {watchFor(items, ({ id, title, getMembers }) =>
44 {watchFor(items, ({ id, title, getMembers }) =>
41 <li>{title}
45 <li>{title}
42 <ul>
46 <ul>
43 {watchFor(getMembers(), ({ role, name, position }) =>
47 {watchFor(getMembers(), ({ role, name, position }) =>
44 <li className={role}>{name}({position})</li>
48 <li className={role}>{name}({position})</li>
45 )}
49 )}
46 </ul>
50 </ul>
47 <div>
51 <div>
48 <Button onClick={() => this._onAddMemberClick(id)}>Add member</Button>
52 <Button onClick={() => this._onAddMemberClick(id)}>Add member</Button>
53 <Button onClick={() => this._onRemoveAppointmentClick(id)}>Remove appointment</Button>
49 </div>
54 </div>
50 </li>
55 </li>
51 )}
56 )}
52 </ul>
57 </ul>
53 )}
58 )}
54 <NewAppointment/>
59 <NewAppointment/>
55 <div ref={attach(this, "toolbarNode")}>
60 <div ref={attach(this, "toolbarNode")}>
56 <Button onClick={this._onAddAppointmentClick}>Add new appointment</Button>
61 <Button onClick={this._onAddAppointmentClick}>Add new appointment</Button>
57 </div>
62 </div>
58 </div>;
63 </div>;
59 }
64 }
60
65
61 load() {
66 load() {
67 trace.log("Loading data");
62 this.model.load();
68 this.model.load();
63 }
69 }
64
70
65 private readonly _onAddMemberClick = (appointmentId: string) => {
71 private readonly _onAddMemberClick = (appointmentId: string) => {
66 this.model.addMember(appointmentId, {
72 this.model.addMember(appointmentId, {
67 email: "some-mail",
73 email: "some-mail",
68 name: "Member Name",
74 name: "Member Name",
69 position: "Member position",
75 position: "Member position",
70 role: "participant"
76 role: "participant"
71 });
77 });
72 };
78 };
73
79
80 private readonly _onRemoveAppointmentClick = (appointmentId: string) => {
81 this.model.removeAppointment(appointmentId);
82 };
83
74 private readonly _onAddAppointmentClick = () => {
84 private readonly _onAddAppointmentClick = () => {
75 this.model.addAppointment("Appointment", new Date, 30);
85 this.model.addAppointment("Appointment", new Date, 30);
76 };
86 };
77 }
87 }
General Comments 0
You need to be logged in to leave comments. Login now