##// END OF EJS Templates
Added priorities to render tasks, revisited rendering scheduler...
cin -
r146:af4f8424e83d v1.9.0 default
parent child
Show More
@@ -8,8 +8,8 const noop = () => { };
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
@@ -5,7 +5,7 import { isNode, isElementNode } from ".
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;
@@ -49,6 +49,8 type _super = {
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))
@@ -71,6 +73,11 export abstract class DjxWidgetBase<Attr
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() {
@@ -1,7 +1,7
1 import { id as mid } from "module";
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 import { argumentNotNull } from "@implab/core-amd/safe";
3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render, 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";
@@ -73,6 +73,8 export class WatchForRendition<T> extend
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");
@@ -88,6 +90,7 export class WatchForRendition<T> extend
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);
@@ -119,28 +122,24 export class WatchForRendition<T> extend
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
131 const beginRender = await scheduleRender();
132 const endRender = beginRender();
133 try {
134 // don't render destroyed rendition
136 // don't render destroyed rendition
135 if (this._ct.isRequested())
137 if (this._ct.isRequested())
136 return;
138 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;
@@ -1,15 +1,11
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
@@ -23,6 +19,8 export class WatchRendition<T> extends R
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");
@@ -36,6 +34,8 export class WatchRendition<T> extends R
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);
@@ -48,17 +48,15 export class WatchRendition<T> extends R
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();
61 try {
62 // don't render destroyed rendition
60 // don't render destroyed rendition
63 if (this._ct.isRequested())
61 if (this._ct.isRequested())
64 return;
62 return;
@@ -83,10 +81,7 export class WatchRendition<T> extends R
83 this._scope.own(() => pending.forEach(destroy));
81 this._scope.own(() => pending.forEach(destroy));
84
82
85 this._renderJob = undefined;
83 this._renderJob = undefined;
86 } finally {
84 };
87 endRender();
88 }
89 }
90
85
91 protected _getDomNode() {
86 protected _getDomNode() {
92 if (!this._node)
87 if (!this._node)
@@ -1,5 +1,4
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";
@@ -9,108 +8,223 const trace = TraceSource.get(mid);
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++;
76 const renderId = _renderId ++;
77 trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
78 if (_renderCount === 1)
79 onRendering();
80
81 await Promise.resolve();
82
83 return () => {
84 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
85 const prev = _context;
81 const prev = _context;
86
82 const captured = _context = { scope, priority, hooks: [] };
87 _context = {
83 return () => {
88 scope,
84 if (_context !== captured)
89 hooks: []
90 };
91 return endRender(prev, _context, renderId);
92 };
93 };
94
95 /**
96 * Completes render operation
97 */
98 const endRender = (prev: Context, current: Context, renderId: number) => () => {
99 if (_context !== current)
100 trace.error("endRender mismatched beginRender call");
85 trace.error("endRender mismatched beginRender call");
101
86
102 const { hooks } = _context;
87 const { hooks } = _context;
103 if (hooks)
88 if (hooks)
104 hooks.forEach(guard);
89 hooks.forEach(guard);
105
90
91 _context = prev;
92 };
93 };
94
95 /**
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.
101 */
102 const startRender = () => {
103 _renderCount++;
104 if (_renderCount === 1)
105 onRendering();
106
107 return _renderId++;
108 };
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");
@@ -127,6 +241,7 const onRendered = () => {
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);
@@ -134,6 +249,12 export const whenRendered = () => new Pr
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)
@@ -142,6 +263,16 export const renderHook = (hook: () => v
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)
@@ -152,21 +283,24 export const refHook = <T>(value: T, ref
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
@@ -41,9 +41,17 export class MainContext implements IDes
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,
@@ -51,6 +59,16 export class MainContext implements IDes
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
@@ -64,6 +64,10 export default class MainModel implement
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));
@@ -1,3 +1,4
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";
@@ -8,6 +9,9 import { Appointment } from "../model/Ap
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) {
@@ -46,6 +50,7 export default class MainWidget extends
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 )}
@@ -59,6 +64,7 export default class MainWidget extends
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
@@ -71,6 +77,10 export default class MainWidget extends
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 };
General Comments 0
You need to be logged in to leave comments. Login now