##// END OF EJS Templates
`Subscribable` is made compatible with rxjs, added map, filter and scan...
cin -
r102:c65ea2350b1a v1.3
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,13
1 import * as t from "tap";
2 import { Baz } from "./mock/Baz";
3
4 t.comment("Declare tests");
5
6 const baz = new Baz();
7
8 const data: string[] = [];
9 baz.writeHello(data);
10 t.pass("Test complete");
11
12 // tslint:disable-next-line: no-console
13 t.comment(data.join("\n"));
@@ -0,0 +1,52
1 import { observe } from "./observable";
2 import * as t from "tap";
3
4 const subj1 = observe<number>(({ next, complete }) => {
5 next(1);
6 complete();
7 next(2);
8 });
9
10 const consumer1 = {
11 sum: 0,
12 next(v: number) {
13 this.sum += v;
14 }
15 }
16
17 subj1.subscribe(consumer1);
18 t.equal(consumer1.sum, 1, "Should get only one value");
19
20 subj1.subscribe(consumer1);
21 t.equal(consumer1.sum, 2, "Should get the value again");
22
23 const consumer2 = {
24 value: 0,
25 completed: false,
26 next(v: number) { this.value = v; },
27 complete() { this.completed = true; }
28 };
29
30 let maps = 0;
31
32 subj1
33 .map(v => {
34 t.comment("map1: " + v * 2);
35 maps++;
36 return v * 2;
37 })
38 .map (v => {
39 t.comment("map2: " + v * 2);
40 maps++;
41 return v * 2;
42 })
43 .map(v => {
44 t.comment("map3: " + v * 2);
45 maps++;
46 return v * 2
47 })
48 .subscribe(consumer2);
49
50 t.equal(consumer2.value, 8, "Should map");
51 t.equal(maps, 3, "The map chain should not be executed after completion");
52 t.ok(consumer2.completed, "The completion signal should pass through"); No newline at end of file
1 NO CONTENT: new file 100644
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -23,12 +23,14
23 23 "@types/chai": "4.1.3",
24 24 "@types/requirejs": "2.1.31",
25 25 "@types/yaml": "1.2.0",
26 "@types/tap": "15.0.7",
26 27 "dojo": "1.16.0",
27 28 "@implab/dojo-typings": "1.0.0",
28 29 "eslint": "6.8.0",
29 30 "requirejs": "2.3.6",
30 31 "tslint": "^6.1.3",
31 "typescript": "4.7.4",
32 "yaml": "~1.7.2"
32 "typescript": "4.8.2",
33 "yaml": "~1.7.2",
34 "tap": "16.3.0"
33 35 }
34 36 }
@@ -1,34 +1,147
1 import { IDestroyable } from "@implab/core-amd/interfaces";
1 /**
2 * The interface for the consumer of an observable sequence
3 */
4 export interface Observer<T> {
5 /**
6 * Called for the next element in the sequence
7 */
8 next: (value: T) => void;
2 9
3 export interface Sink<T> {
4 next: (value: T) => void;
10 /**
11 * Called once when the error occurs in the sequence.
12 */
5 13 error: (e: unknown) => void;
14
15 /**
16 * Called once at the end of the sequence.
17 */
6 18 complete: () => void;
7 19 }
8 20
9 export type Consumer<T> = Partial<Sink<T>>;
21 /**
22 * The group of functions to feed an observable. This methods are provided to
23 * the producer to generate a stream of events.
24 */
25 export type Sink<T> = {
26 [k in keyof Observer<T>]: (this: void, ...args: Parameters<Observer<T>[k]>) => void;
27 };
10 28
11 29 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
12 30
13 export interface Observable<T> {
14 on(consumer: Partial<Sink<T>>): IDestroyable;
15 }
31 export interface Unsubscribable {
32 unsubscribe(): void;
33 }
34
35 export const isUnsubsribable = (v: unknown): v is Unsubscribable =>
36 v !== null && v !== undefined && typeof (v as Unsubscribable).unsubscribe === "function";
37
38 export const isSubsribable = (v: unknown): v is Subscribable<unknown> =>
39 v !== null && v !== undefined && typeof (v as Subscribable<unknown>).subscribe === "function";
16 40
17 const noop = () => {};
41 export interface Subscribable<T> {
42 subscribe(consumer: Partial<Observer<T>>): Unsubscribable;
43 }
44
45 /** The observable source of items. */
46 export interface Observable<T> extends Subscribable<T> {
47 /** Transforms elements of the sequence with the specified mapper
48 *
49 * @param mapper The mapper used to transform the values
50 */
51 map<T2>(mapper: (value: T) => T2): Observable<T2>;
18 52
19 const sink = <T>(consumer: Consumer<T>) => {
20 const { next = noop, error = noop, complete = noop } = consumer;
21 let done = false;
53 /** Filters elements of the sequence. The resulting sequence will
54 * contain only elements which match the specified predicate.
55 *
56 * @param predicate The filter predicate.
57 */
58 filter(predicate: (value: T) => boolean): Observable<T>;
22 59
60 /** Applies accumulator to each value in the sequence and
61 * emits the accumulated value for each source element
62 *
63 * @param accumulator
64 * @param initial
65 */
66 scan<A>(accumulator: (acc: A, value: T) => A, initial: A): Observable<A>;
67 }
68
69 const noop = () => { };
70
71 const sink = <T>(consumer: Partial<Observer<T>>) => {
72 const { next, error, complete } = consumer;
23 73 return {
24 next: (value: T) => !done && next(value),
25 error: (e: unknown) => !done && (done = true, error(e)),
26 complete: () => !done && (done = true, complete())
27 };
74 next: next ? next.bind(consumer) : noop,
75 error: error ? error.bind(consumer) : noop,
76 complete: complete ? complete.bind(consumer) : noop
77 }
78 };
79
80 const fuse = <T>({ next, error, complete }: Sink<T>) => {
81 let done = false;
82 return {
83 next: (value: T) => { !done && next(value) },
84 error: (e: unknown) => { !done && (done = true, error(e)) },
85 complete: () => { !done && (done = true, complete()) }
86 }
28 87 }
29 88
30 export const observe = <T>(producer: Producer<T>) : Observable<T> => ({
31 on: (consumer: Consumer<T>) => ({
32 destroy: producer(sink(consumer)) ?? noop
89 const _observe = <T>(producer: Producer<T>): Observable<T> => ({
90 subscribe: (consumer: Partial<Observer<T>>) => ({
91 unsubscribe: producer(sink(consumer)) ?? noop
92 }),
93 map: (mapper) => _observe(({ next, error, complete }) =>
94 producer({
95 next: next !== noop ? (v: T) => next(mapper(v)) : noop,
96 error,
97 complete
98 })
99 ),
100 filter: (predicate) => _observe(({ next, error, complete }) =>
101 producer({
102 next: next !== noop ?
103 (v: T) => predicate(v) ? next(v) : void(0) : noop,
104 error,
105 complete
106 })
107 ),
108 scan: (accumulator, initial) => _observe(({ next, error, complete }) => {
109 let _acc = initial;
110 return producer({
111 next: next !== noop ?
112 (v: T) => next(_acc = accumulator(_acc, v)) : noop,
113 error,
114 complete
115 });
33 116 })
34 117 });
118
119 export const observe = <T>(producer: Producer<T>): Observable<T> => ({
120 subscribe: (consumer: Partial<Observer<T>>) => ({
121 unsubscribe: producer(fuse(sink(consumer))) ?? noop
122 }),
123 map: (mapper) => _observe(({ next, error, complete }) =>
124 producer(fuse({
125 next: next !== noop ?
126 (v: T) => next(mapper(v)) : noop,
127 error,
128 complete
129 }))
130 ),
131 filter: (predicate) => _observe(({ next, error, complete }) =>
132 producer(fuse({
133 next: next !== noop ?
134 (v: T) => predicate(v) ? next(v) : void (0) : noop,
135 error,
136 complete
137 }))
138 ),
139 scan: (accumulator, initial?) => observe(({ next, error, complete }) => {
140 let _acc = initial;
141 return producer(fuse({
142 next: next !== noop ? (v: T) => next(_acc = accumulator(_acc, v)) : noop,
143 error,
144 complete
145 }));
146 })
147 });
@@ -1,13 +1,15
1 1 import { Constructor } from "@implab/core-amd/interfaces";
2 2 import { HtmlRendition } from "./tsx/HtmlRendition";
3 3 import { WidgetRendition } from "./tsx/WidgetRendition";
4 import { isWidgetConstructor, Rendition } from "./tsx/traits";
4 import { isElementNode, isWidget, isWidgetConstructor, Rendition } from "./tsx/traits";
5 5 import { FunctionRendition } from "./tsx/FunctionRendition";
6 6 import Stateful = require("dojo/Stateful");
7 7 import _WidgetBase = require("dijit/_WidgetBase");
8 8 import { DjxWidgetBase } from "./tsx/DjxWidgetBase";
9 9 import { WatchRendition } from "./tsx/WatchRendition";
10 import { observe } from "./observable";
10 import { Observable, observe, Subscribable } from "./observable";
11 import djAttr = require("dojo/dom-attr");
12 import djClass = require("dojo/dom-class");
11 13
12 14 export function createElement<T extends Constructor | string | ((props: any) => Element)>(elementType: T, ...args: any[]): Rendition {
13 15 if (typeof elementType === "string") {
@@ -44,7 +46,8 export interface EventSelector {
44 46
45 47 export type DojoMouseEvent<T = any> = MouseEvent & EventSelector & EventDetails<T>;
46 48
47 type StatefulProps<T> = T extends Stateful<infer A> ? A : never;
49 type StatefulProps<T> = T extends Stateful<infer A> ? A :
50 T extends _WidgetBase ? T : never;
48 51
49 52
50 53 /**
@@ -73,25 +76,82 export function watch<T extends Stateful
73 76 prop: K,
74 77 render: (model: StatefulProps<T>[K]) => any
75 78 ): Rendition;
76 export function watch<T extends Stateful, K extends keyof StatefulProps<T> & string>(
77 target: T,
78 prop: K,
79 render: (model: StatefulProps<T>[K]) => any
79 export function watch<V>(subj: Subscribable<V>, render: (model: V) => unknown): Rendition;
80 export function watch(
81 ...args: [Stateful, string, (model: unknown) => unknown] |
82 [Subscribable<unknown>, (model: unknown) => unknown]
80 83 ) {
84 if (args.length === 3) {
85 const [target, prop, render] = args;
86 return new WatchRendition(
87 render,
88 observe(({next}) => {
89 const h = target.watch<any>(
90 prop,
91 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
92 );
93 next(target.get(prop));
94 return () => h.remove();
95 })
96 );
97 } else {
98 const [subj, render] = args;
99 return new WatchRendition(render, subj);
100 }
101 }
102
103 export const prop: {
104 <T extends Stateful, K extends string & keyof StatefulProps<T>>(target: T, name: K): Observable<StatefulProps<T>[K]>;
105 <T extends _WidgetBase, K extends keyof T>(target: T, name: K): Observable<T[K]>;
106 } = (target: Stateful, name: string) => {
107 return observe(({next}) => {
108 const h = target.watch(
109 name,
110 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
111 );
112 next(target.get(name));
113 return () => h.remove();
114 })
115 };
81 116
82 return new WatchRendition(
83 render,
84 observe(({next}) => {
85 const h = target.watch(
86 prop,
87 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
88 );
89 next(target.get(prop));
90 return () => h.remove();
91 })
92 )
117 export const attach = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
118
119 export const bind = <K extends string, T>(attr: K, subj: Subscribable<T>) => {
120 let h = { unsubscribe() { } };
121
122 return <E extends (HTMLElement & { [p in K]: T }) | { set(name: K, value: T): void; }>(el: E | undefined) => {
123 if (el) {
124 if (isElementNode(el)) {
125 h = subj.subscribe({
126 next: value => djAttr.set(el, attr, value)
127 });
128 } else {
129 h = subj.subscribe({
130 next: value => el.set(attr, value)
131 });
132 }
133 } else {
134 h.unsubscribe();
135 }
136 }
137 };
138
139 export const toggleClass = (className: string, subj: Subscribable<boolean>) => {
140 let h = { unsubscribe() { } };
141 return (elOrWidget: HTMLElement | _WidgetBase | undefined) => {
142 const el = isWidget(elOrWidget) ? elOrWidget.domNode : elOrWidget;
143 if (el) {
144 h = subj.subscribe({
145 next: v => djClass.toggle(el, className, v)
146 });
147 } else {
148 h.unsubscribe();
149 }
150 }
93 151 }
94 152
153 export const all = <T, A extends JSX.Ref<T>[]>(...cbs: A): JSX.Ref<T> => (arg: T | undefined) => cbs.forEach(cb => cb(arg));
154
95 155 /** Decorates the method which will be registered as the handle for the specified event.
96 156 * This decorator can be applied to DjxWidgetBase subclass methods.
97 157 *
@@ -1,9 +1,11
1 1 import { djbase, djclass } from "../declare";
2 2 import _WidgetBase = require("dijit/_WidgetBase");
3 3 import _AttachMixin = require("dijit/_AttachMixin");
4 import { Rendition, isNode } from "./traits";
4 import { Rendition, isNode, isElementNode } from "./traits";
5 5 import registry = require("dijit/registry");
6 6 import on = require("dojo/on");
7 import { Scope } from "./Scope";
8 import { render } from "./render";
7 9
8 10 // type Handle = dojo.Handle;
9 11
@@ -36,13 +38,20 export interface DjxWidgetBase<Attrs = {
36 38
37 39 type _super = {
38 40 startup(): void;
41
42 destroy(preserveDom?: boolean): void;
39 43 };
40 44
41 45 @djclass
42 46 export abstract class DjxWidgetBase<Attrs = {}, Events = {}> extends djbase<_super, _AttachMixin>(_WidgetBase, _AttachMixin) {
47 private readonly _scope = new Scope();
43 48
44 49 buildRendering() {
45 this.domNode = this.render().getDomNode();
50 const node = render(this.render(), this._scope);
51 if (!isElementNode(node))
52 throw new Error("The render method must return a single DOM element");
53 this.domNode = node as HTMLElement;
54
46 55 super.buildRendering();
47 56
48 57 // now we should get assigned data-dojo-attach-points
@@ -111,4 +120,9 export abstract class DjxWidgetBase<Attr
111 120 registry.findWidgets(this.domNode, this.containerNode).forEach(w => w.startup());
112 121 super.startup();
113 122 }
123
124 destroy(preserveDom?: boolean) {
125 this._scope.destroy();
126 super.destroy(preserveDom);
127 }
114 128 }
@@ -3,8 +3,7 import djAttr = require("dojo/dom-attr")
3 3 import { argumentNotEmptyString } from "@implab/core-amd/safe";
4 4 import { RenditionBase } from "./RenditionBase";
5 5 import { placeAt } from "./traits";
6 import { IScope } from "./Scope";
7 import { getItemDom, renderHook } from "./render";
6 import { getItemDom, refHook } from "./render";
8 7
9 8 export class HtmlRendition extends RenditionBase<Element> {
10 9 elementType: string;
@@ -18,13 +17,13 export class HtmlRendition extends Rendi
18 17 this.elementType = elementType;
19 18 }
20 19
21 _addChild(child: unknown, scope: IScope): void {
20 _addChild(child: unknown): void {
22 21 if (!this._element)
23 22 throw new Error("The HTML element isn't created");
24 23 placeAt(getItemDom(child), this._element);
25 24 }
26 25
27 _create({ xmlns, ref, ...attrs }: { xmlns?: string, ref?: JSX.Ref<Element> }, children: unknown[], scope: IScope) {
26 _create({ xmlns, ref, ...attrs }: { xmlns?: string, ref?: JSX.Ref<Element> }, children: unknown[]) {
28 27
29 28 if (xmlns) {
30 29 this._element = document.createElementNS(xmlns, this.elementType);
@@ -33,12 +32,12 export class HtmlRendition extends Rendi
33 32 this._element = djDom.create(this.elementType, attrs);
34 33 }
35 34
36 children.forEach(v => this._addChild(v, scope));
35 children.forEach(v => this._addChild(v));
37 36
38 37 const element = this._element;
39 38
40 39 if (ref)
41 renderHook(() => ref(element));
40 refHook(element, ref);
42 41 }
43 42
44 43 _getDomNode() {
@@ -1,8 +1,4
1 import { isNull, mixin } from "@implab/core-amd/safe";
2 import { isPlainObject, DojoNodePosition, Rendition, isDocumentFragmentNode, placeAt, collectNodes, autostartWidgets } from "./traits";
3
4 import { IScope } from "./Scope";
5 import { getScope } from "./render";
1 import { isPlainObject, DojoNodePosition, Rendition, isDocumentFragmentNode, placeAt, collectNodes, isMounted, startupWidgets } from "./traits";
6 2
7 3 export abstract class RenditionBase<TNode extends Node> implements Rendition<TNode> {
8 4 private _attrs = {};
@@ -15,12 +11,12 export abstract class RenditionBase<TNod
15 11 if (this._created)
16 12 throw new Error("The Element is already created");
17 13
18 if (isNull(v) || typeof v === "boolean")
14 if (v === null || v === undefined || typeof v === "boolean")
19 15 // skip null, undefined, booleans ( this will work: {value && <span>{value}</span>} )
20 16 return;
21 17
22 18 if (isPlainObject(v)) {
23 mixin(this._attrs, v);
19 this._attrs = {... this._attrs, ...v};
24 20 } else if (v instanceof Array) {
25 21 v.forEach(x => this.visitNext(x));
26 22 } else {
@@ -28,9 +24,9 export abstract class RenditionBase<TNod
28 24 }
29 25 }
30 26
31 ensureCreated(scope: IScope) {
27 ensureCreated() {
32 28 if (!this._created) {
33 this._create(this._attrs, this._children, scope);
29 this._create(this._attrs, this._children);
34 30 this._children = [];
35 31 this._attrs = {};
36 32 this._created = true;
@@ -43,8 +39,8 export abstract class RenditionBase<TNod
43 39 }
44 40
45 41 /** Creates DOM node if not created. No additional actions are taken. */
46 getDomNode(scope?: IScope) {
47 this.ensureCreated(scope || getScope());
42 getDomNode() {
43 this.ensureCreated();
48 44 return this._getDomNode();
49 45 }
50 46
@@ -59,15 +55,16 export abstract class RenditionBase<TNod
59 55 placeAt(refNode: string | Node, position: DojoNodePosition = "last") {
60 56 const domNode = this.getDomNode();
61 57
62 const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.children) : [domNode];
58 const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.childNodes) : [domNode];
63 59
64 60 placeAt(domNode, refNode, position);
65 61
66 startupPending.forEach(autostartWidgets);
62 if (isMounted(startupPending[0]))
63 startupPending.forEach(n => startupWidgets(n));
67 64
68 65 }
69 66
70 protected abstract _create(attrs: object, children: unknown[], scope: IScope): void;
67 protected abstract _create(attrs: object, children: unknown[]): void;
71 68
72 69 protected abstract _getDomNode(): TNode;
73 70 }
@@ -1,8 +1,9
1 1 import { IDestroyable, IRemovable } from "@implab/core-amd/interfaces";
2 2 import { isDestroyable, isRemovable } from "@implab/core-amd/safe";
3 import { isUnsubsribable, Unsubscribable } from "../observable";
3 4
4 5 export interface IScope {
5 own(target: (() => void) | IDestroyable | IRemovable): void;
6 own(target: (() => void) | IDestroyable | IRemovable | Unsubscribable): void;
6 7 }
7 8
8 9 export class Scope implements IDestroyable, IScope {
@@ -10,13 +11,15 export class Scope implements IDestroyab
10 11
11 12 static readonly dummy: IScope = { own() { } };
12 13
13 own(target: (() => void) | IDestroyable | IRemovable) {
14 own(target: (() => void) | IDestroyable | IRemovable | Unsubscribable) {
14 15 if (target instanceof Function) {
15 16 this._cleanup.push(target);
16 17 } else if (isDestroyable(target)) {
17 18 this._cleanup.push(() => target.destroy());
18 19 } else if (isRemovable(target)) {
19 20 this._cleanup.push(() => target.remove());
21 } else if (isUnsubsribable(target)) {
22 this._cleanup.push(() => target.unsubscribe());
20 23 }
21 24 }
22 25
@@ -4,21 +4,26 import { argumentNotNull } from "@implab
4 4 import { getScope, render } from "./render";
5 5 import { RenditionBase } from "./RenditionBase";
6 6 import { Scope } from "./Scope";
7 import { Observable } from "../observable";
8 import { destroy } from "./traits";
7 import { Subscribable } from "../observable";
8 import { Cancellation } from "@implab/core-amd/Cancellation";
9 import { collectNodes, destroy, isDocumentFragmentNode, isMounted, placeAt, startupWidgets } from "./traits";
9 10
10 11 const trace = TraceSource.get(mid);
11 12
12 13 export class WatchRendition<T> extends RenditionBase<Node> {
13 14 private readonly _component: (arg: T) => unknown;
14 15
15 private _node: Node;
16 private readonly _node: Node;
16 17
17 18 private readonly _scope = new Scope();
18 19
19 private readonly _subject: Observable<T>;
20 private readonly _subject: Subscribable<T>;
21
22 private _renderJob?: { value: T };
20 23
21 constructor(component: (arg: T) => unknown, subject: Observable<T>) {
24 private _ct = Cancellation.none;
25
26 constructor(component: (arg: T) => unknown, subject: Subscribable<T>) {
22 27 super();
23 28 argumentNotNull(component, "component");
24 29
@@ -26,23 +31,60 export class WatchRendition<T> extends R
26 31
27 32 this._subject = subject;
28 33
29 this._node = document.createComment("WatchRendition placeholder");
34 this._node = document.createComment("[Watch]");
30 35 }
31 36
32 protected _create(attrs: object, children: any[]) {
37 protected _create() {
33 38 const scope = getScope();
34 scope.own(this._scope);
35 scope.own(this._subject.on({ next: this._onValue }));
39 scope.own(() => {
40 this._scope.destroy();
41 destroy(this._node);
42 });
43 scope.own(this._subject.subscribe({ next: this._onValue }));
44 this._ct = new Cancellation(cancel => scope.own(cancel));
45 }
46
47 private _onValue = (value: T) => {
48 if (!this._renderJob) {
49 // schedule a new job
50 this._renderJob = { value };
51 this._render().catch(e => trace.error(e));
52 } else {
53 // update existing job
54 this._renderJob = { value };
55 }
36 56 }
37 57
38 private _onValue = (value: T) =>
39 void this._render(value).catch( e => trace.error(e));
58 private async _render() {
59 // fork
60 await Promise.resolve();
61 // don't render destroyed rendition
62 if (this._ct.isRequested())
63 return;
64
65 // remove all previous content
66 this._scope.clean();
40 67
41 private async _render(value: T) {
42 this._scope.clean();
43 const [refNode, ...rest] = await render(this._component(value), this._node, "replace", this._scope);
44 this._node = refNode;
45 this._scope.own(() => rest.forEach(destroy));
68 // render the new node
69 const node = render(
70 this._renderJob ? this._component(this._renderJob.value) : undefined,
71 this._scope
72 );
73
74 // get actual content
75 const pending = isDocumentFragmentNode(node) ?
76 collectNodes(node.childNodes) :
77 [node];
78
79 placeAt(node, this._node, "after");
80
81 if (isMounted(this._node))
82 pending.forEach(n => startupWidgets(n));
83
84 if (pending.length)
85 this._scope.own(() => pending.forEach(destroy));
86
87 this._renderJob = undefined;
46 88 }
47 89
48 90 protected _getDomNode() {
@@ -3,8 +3,7 import { RenditionBase } from "./Renditi
3 3 import { DojoNodePosition, isElementNode, isInPage, isWidget, placeAt } from "./traits";
4 4 import registry = require("dijit/registry");
5 5 import ContentPane = require("dijit/layout/ContentPane");
6 import { IScope } from "./Scope";
7 import { getItemDom, getScope, renderHook } from "./render";
6 import { getItemDom, refHook } from "./render";
8 7
9 8 // tslint:disable-next-line: class-name
10 9 export interface _Widget {
@@ -32,13 +31,13 export class WidgetRendition extends Ren
32 31 this.widgetClass = widgetClass;
33 32 }
34 33
35 _addChild(child: unknown, scope: IScope): void {
34 _addChild(child: unknown): void {
36 35 const instance = this._getInstance();
37 36
38 37 if (instance.addChild) {
39 38 if (child instanceof WidgetRendition) {
40 39 // layout containers add custom logic to addChild methods
41 instance.addChild(child.getWidgetInstance(scope));
40 instance.addChild(child.getWidgetInstance());
42 41 } else if (isWidget(child)) {
43 42 instance.addChild(child);
44 43 } else {
@@ -64,7 +63,7 export class WidgetRendition extends Ren
64 63 }
65 64 }
66 65
67 protected _create({ref, ...attrs}: {ref?: JSX.Ref<_Widget>}, children: unknown[], scope: IScope) {
66 protected _create({ref, ...attrs}: {ref?: JSX.Ref<_Widget>}, children: unknown[]) {
68 67 if (this.widgetClass.prototype instanceof ContentPane) {
69 68 // a special case for the ContentPane this is for
70 69 // compatibility with that heavy widget, all
@@ -80,14 +79,11 export class WidgetRendition extends Ren
80 79 this._instance = new this.widgetClass(_attrs);
81 80 } else {
82 81 this._instance = new this.widgetClass(attrs);
83 children.forEach(x => this._addChild(x, scope));
82 children.forEach(x => this._addChild(x));
84 83 }
85 84
86 if (ref) {
87 const instance = this._instance;
88 renderHook(() => ref(instance));
89 }
90
85 if (ref)
86 refHook(this._instance, ref);
91 87 }
92 88
93 89 private _getInstance() {
@@ -109,7 +105,7 export class WidgetRendition extends Ren
109 105 * @param position A position relative to refNode.
110 106 */
111 107 placeAt(refNode: string | Node, position?: DojoNodePosition) {
112 this.ensureCreated(getScope());
108 this.ensureCreated();
113 109 const instance = this._getInstance();
114 110 if (typeof instance.placeAt === "function") {
115 111 instance.placeAt(refNode, position);
@@ -126,8 +122,8 export class WidgetRendition extends Ren
126 122 }
127 123 }
128 124
129 getWidgetInstance(scope?: IScope) {
130 this.ensureCreated(scope || getScope());
125 getWidgetInstance() {
126 this.ensureCreated();
131 127 return this._getInstance();
132 128 }
133 129
@@ -1,16 +1,20
1 1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 2 import { isPromise } from "@implab/core-amd/safe";
3 3 import { id as mid } from "module";
4 import { Scope } from "./Scope";
5 import { autostartWidgets, collectNodes, DojoNodePosition, isDocumentFragmentNode, isNode, isRendition, isWidget, placeAt } from "./traits";
4 import { IScope, Scope } from "./Scope";
5 import { isNode, isRendition, isWidget } from "./traits";
6 6
7 7 const trace = TraceSource.get(mid);
8 8
9 let _scope = Scope.dummy;
9 interface Context {
10 scope: IScope;
10 11
11 let renderCount = 0;
12 hooks?: (() => void)[];
13 }
12 14
13 const hooks: (() => void)[] = [];
15 let _context: Context = {
16 scope: Scope.dummy
17 }
14 18
15 19 const guard = (cb: () => unknown) => {
16 20 try {
@@ -24,53 +28,57 const guard = (cb: () => unknown) => {
24 28 }
25 29 }
26 30
27 /**
28 * Schedules rendering micro task
29 * @returns Promise
30 */
31 const beginRender = () => {
32 renderCount++;
33 return Promise.resolve();
31 export const beginRender = (scope: IScope = getScope()) => {
32 const prev = _context;
33 _context = {
34 scope,
35 hooks: []
36 };
37 return endRender(prev);
34 38 }
35 39
36 40 /**
37 41 * Completes render operation
38 42 */
39 const endRender = () => {
40 if (!--renderCount) {
43 const endRender = (prev: Context) => () => {
44 const { hooks } = _context;
45 if (hooks)
41 46 hooks.forEach(guard);
42 hooks.length = 0;
43 }
47
48 _context = prev;
44 49 }
45 50
46 51 export const renderHook = (hook: () => void) => {
47 if (renderCount)
52 const { hooks } = _context;
53 if (hooks)
48 54 hooks.push(hook);
49 55 else
50 56 guard(hook);
51 57 }
52 58
59 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
60 const { hooks, scope } = _context;
61 if (hooks)
62 hooks.push(() => ref(value));
63 else
64 guard(() => ref(value));
65
66 scope.own(() => ref(undefined));
67 }
68
53 69 /** Returns the current scope */
54 export const getScope = () => _scope;
70 export const getScope = () => _context.scope;
55 71
56 72 /** Schedules the rendition to be rendered to the DOM Node
57 73 * @param rendition The rendition to be rendered
58 74 * @param scope The scope
59 75 */
60 export const render = async (rendition: unknown, refNode: Node, position: DojoNodePosition = "last", scope = Scope.dummy) => {
61 await beginRender();
62 const prev = _scope;
63 _scope = scope;
76 export const render = (rendition: unknown, scope = Scope.dummy) => {
77 const complete = beginRender(scope);
64 78 try {
65 const domNode = getItemDom(rendition);
66 const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.children) : [domNode];
67 placeAt(domNode, refNode, position);
68 startupPending.forEach(autostartWidgets);
69
70 return startupPending;
79 return getItemDom(rendition);
71 80 } finally {
72 _scope = prev;
73 endRender();
81 complete();
74 82 }
75 83 }
76 84
@@ -89,8 +97,8 export const getItemDom = (v: unknown) =
89 97 // widgets are converted to it's markup
90 98 return v.domNode;
91 99 } else if (typeof v === "boolean" || v === null || v === undefined) {
92 // null | undefined | boolean are removed, converted to comments
93 return document.createComment(`[${typeof v} ${String(v)}]`);
100 // null | undefined | boolean are removed
101 return document.createDocumentFragment();
94 102 } else if (v instanceof Array) {
95 103 // arrays will be translated to document fragments
96 104 const fragment = document.createDocumentFragment();
@@ -93,13 +93,15 export const destroy = (target: Node | I
93 93 const w = registry.byNode(target);
94 94 if (w) {
95 95 w.destroyRecursive();
96 return;
96 97 } else {
97 98 emptyNode(target);
98 const parent = target.parentNode;
99 if (parent)
100 parent.removeChild(target);
101 99 }
102 100 }
101 const parent = target.parentNode;
102 if (parent)
103 parent.removeChild(target);
104
103 105 }
104 106 }
105 107
@@ -191,7 +193,7 export const placeAt = (node: Node, refN
191 193 * @param collection The collection of nodes.
192 194 * @returns The array of nodes.
193 195 */
194 export const collectNodes = (collection: HTMLCollection) => {
196 export const collectNodes = (collection: NodeListOf<ChildNode>) => {
195 197 const items = [];
196 198 for (let i = 0, n = collection.length; i < n; i++) {
197 199 items.push(collection[i]);
@@ -199,16 +201,14 export const collectNodes = (collection:
199 201 return items;
200 202 };
201 203
202 /** Starts widgets if the node contained in the document or in the started widget.
203 *
204 * @param node The node to start.
205 */
206 export const autostartWidgets = (node: Node) => {
204
205 export const isMounted = (node: Node) => {
207 206 if (node.parentNode) {
208 207 const parentWidget = registry.getEnclosingWidget(node.parentNode);
209 208 if (parentWidget && parentWidget._started)
210 return startupWidgets(node);
209 return true;
211 210 }
212 211 if (isInPage(node))
213 startupWidgets(node);
212 return true;
213 return false;
214 214 }; No newline at end of file
@@ -2,7 +2,7
2 2
3 3 declare namespace JSX {
4 4
5 type Ref<T> = (value: T) => void;
5 type Ref<T> = ((value: T | undefined) => void);
6 6
7 7 interface DjxIntrinsicAttributes<E> {
8 8 /** alias for className */
@@ -1,1 +1,2
1 import "./DeclareTests"; No newline at end of file
1 import "./declare-tests";
2 import "./observable-tests"; No newline at end of file
@@ -96,6 +96,9 task copyModules(type: Copy) {
96 96 pack("@implab/core-amd")
97 97 pack("dojo")
98 98 pack("dijit")
99 into("rxjs") {
100 from(npm.module("rxjs/dist/bundles"))
101 }
99 102 from npm.module("requirejs/require.js")
100 103 }
101 104
@@ -8,7 +8,8
8 8 "dependencies": {
9 9 "dijit": "1.17.3",
10 10 "dojo": "1.17.3",
11 "requirejs": "2.3.6"
11 "requirejs": "2.3.6",
12 "rxjs": "7.5.6"
12 13 },
13 14 "devDependencies": {
14 15 "@implab/core-amd": "1.4.6",
@@ -77,6 +78,19
77 78 "node": ">=0.4.0"
78 79 }
79 80 },
81 "node_modules/rxjs": {
82 "version": "7.5.6",
83 "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
84 "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
85 "dependencies": {
86 "tslib": "^2.1.0"
87 }
88 },
89 "node_modules/tslib": {
90 "version": "2.4.0",
91 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
92 "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
93 },
80 94 "node_modules/typescript": {
81 95 "version": "4.8.2",
82 96 "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
@@ -133,6 +147,19
133 147 "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",
134 148 "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg=="
135 149 },
150 "rxjs": {
151 "version": "7.5.6",
152 "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
153 "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
154 "requires": {
155 "tslib": "^2.1.0"
156 }
157 },
158 "tslib": {
159 "version": "2.4.0",
160 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
161 "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
162 },
136 163 "typescript": {
137 164 "version": "4.8.2",
138 165 "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
@@ -4,7 +4,8
4 4 "dependencies": {
5 5 "dijit": "1.17.3",
6 6 "dojo": "1.17.3",
7 "requirejs": "2.3.6"
7 "requirejs": "2.3.6",
8 "rxjs": "7.5.6"
8 9 },
9 10 "devDependencies": {
10 11 "@implab/core-amd": "1.4.6",
@@ -5,7 +5,12 requirejs.config({
5 5 "@implab/djx",
6 6 "@implab/core-amd",
7 7 "dojo",
8 "dijit"
8 "dijit",
9 {
10 name: "rxjs",
11 location: "rxjs",
12 main: "rxjs.umd.min"
13 }
9 14 ],
10 15 deps: ["app"]
11 16 });
@@ -1,10 +1,9
1 1 import { djbase, djclass } from "@implab/djx/declare";
2 2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 import { createElement, watch } from "@implab/djx/tsx";
3 import { createElement, watch, prop, attach, all, bind, toggleClass } from "@implab/djx/tsx";
4 4 import ProgressBar from "./ProgressBar";
5 5 import Button = require("dijit/form/Button");
6
7 const ref = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
6 import { interval } from "rxjs";
8 7
9 8 @djclass
10 9 export default class MainWidget extends djbase(DjxWidgetBase) {
@@ -17,17 +16,42 export default class MainWidget extends
17 16
18 17 showCounter = false;
19 18
19 counterNode?: HTMLInputElement;
20
21 paused = false;
22
20 23 render() {
24 const Counter = ({ children }: { children: unknown[] }) => <span>Counter: {children}</span>;
25
21 26 return <div className="tundra">
22 <h2 ref={ref(this, "titleNode")}>Hi!</h2>
23 <ProgressBar ref={ref(this, "progressBar")} />
24 {watch(this, "showCounter", flag => flag &&
25 <section style={{padding: "10px"}}>
26 <label>
27 Counter: {watch(this, "count", v => [<span>{v}</span>, " ", <span>sec</span>])}
28 </label>
29 </section>
30 )}
27 <h2 ref={attach(this, "titleNode")}>Hi!</h2>
28 <ProgressBar ref={attach(this, "progressBar")} />
29 <section style={{ padding: "10px" }}>
30 {watch(prop(this, "showCounter"), flag => flag &&
31 [
32 <Counter><input ref={all(
33 bind("value", prop(this, "count")
34 .map(x => x*10)
35 .map(String)
36 ),
37 attach(this, "counterNode")
38 )} /> <span>ms</span></Counter>,
39 " | ",
40 <span ref={bind("innerHTML", interval(1000))}></span>,
41 " | ",
42 <Button
43 ref={all(
44 bind("label", prop(this, "paused")
45 .map(x => x ? "Unpause" : "Pause")
46 ),
47 toggleClass("paused", prop(this,"paused"))
48 )}
49 onClick={this._onPauseClick}
50 />
51 ]
52
53 )}
54 </section>
31 55 <Button onClick={this._onToggleCounterClick}>Toggle counter</Button>
32 56 </div>;
33 57 }
@@ -35,12 +59,21 export default class MainWidget extends
35 59 postCreate(): void {
36 60 super.postCreate();
37 61
38 const inc = () => {
39 this.set("count", this.count + 1);
40 this.defer(inc, 1000);
41 }
62 const h = setInterval(
63 () => {
64 this.set("count", this.count + 1);
65 },
66 10
67 );
68 this.own({
69 destroy: () => {
70 clearInterval(h);
71 }
72 });
73 }
42 74
43 inc();
75 private _onPauseClick = () => {
76 this.set("paused", !this.paused);
44 77 }
45 78
46 79 private _onToggleCounterClick = () => {
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now