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