##// 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
@@ -1,34 +1,36
1 1 {
2 2 "name": "@implab/djx",
3 3 "version": "0.0.1-dev",
4 4 "description": "Supports using dojo version 1 with typescript and .tsx files",
5 5 "keywords": [
6 6 "dojo",
7 7 "tsx",
8 8 "typescript",
9 9 "widgets"
10 10 ],
11 11 "author": "Implab team",
12 12 "license": "BSD-2-Clause",
13 13 "repository": "https://code.implab.org/implab/implabjs-djx",
14 14 "publishConfig": {
15 15 "access": "public"
16 16 },
17 17 "peerDependencies": {
18 18 "@implab/core-amd": "^1.4.0",
19 19 "dojo": "^1.10.0"
20 20 },
21 21 "devDependencies": {
22 22 "@implab/core-amd": "^1.4.0",
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;
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";
40
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>;
52
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>;
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>;
15 67 }
16 68
17 69 const noop = () => {};
18 70
19 const sink = <T>(consumer: Consumer<T>) => {
20 const { next = noop, error = noop, complete = noop } = consumer;
21 let done = false;
22
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())
74 next: next ? next.bind(consumer) : noop,
75 error: error ? error.bind(consumer) : noop,
76 complete: complete ? complete.bind(consumer) : noop
77 }
27 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,116 +1,176
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") {
14 16 const ctx = new HtmlRendition(elementType);
15 17 if (args)
16 18 args.forEach(x => ctx.visitNext(x));
17 19
18 20 return ctx;
19 21 } else if (isWidgetConstructor(elementType)) {
20 22 const ctx = new WidgetRendition(elementType);
21 23 if (args)
22 24 args.forEach(x => ctx.visitNext(x));
23 25
24 26 return ctx;
25 27 } else if (typeof elementType === "function") {
26 28 const ctx = new FunctionRendition(elementType as (props: any) => Element);
27 29 if (args)
28 30 args.forEach(x => ctx.visitNext(x));
29 31
30 32 return ctx;
31 33 } else {
32 34 throw new Error(`The element type '${elementType}' is unsupported`);
33 35 }
34 36 }
35 37
36 38 export interface EventDetails<T = any> {
37 39 detail: T;
38 40 }
39 41
40 42 export interface EventSelector {
41 43 selectorTarget: HTMLElement;
42 44 target: HTMLElement;
43 45 }
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 /**
51 54 * Observers the property and calls render callback each change.
52 55 *
53 56 * @param target The target object which property will be observed.
54 57 * @param prop The name of the property.
55 58 * @param render The callback which will be called every time the value is changed
56 59 * @returns Rendition which is created instantly
57 60 */
58 61 export function watch<W extends _WidgetBase, K extends keyof W>(
59 62 target: W,
60 63 prop: K,
61 64 render: (model: W[K]) => any
62 65 ): Rendition;
63 66 /**
64 67 * Observers the property and calls render callback each change.
65 68 *
66 69 * @param target The target object which property will be observed.
67 70 * @param prop The name of the property.
68 71 * @param render The callback which will be called every time the value is changed
69 72 * @returns Rendition which is created instantly
70 73 */
71 74 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
72 75 target: T,
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 ) {
81
84 if (args.length === 3) {
85 const [target, prop, render] = args;
82 86 return new WatchRendition(
83 87 render,
84 88 observe(({next}) => {
85 const h = target.watch(
89 const h = target.watch<any>(
86 90 prop,
87 91 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
88 92 );
89 93 next(target.get(prop));
90 94 return () => h.remove();
91 95 })
92 )
96 );
97 } else {
98 const [subj, render] = args;
99 return new WatchRendition(render, subj);
100 }
93 101 }
94 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 };
116
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 }
151 }
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 *
98 158 * ```
99 159 * @on("click")
100 160 * _onClick(eventObj: MouseEvent) {
101 161 * // ...
102 162 * }
103 163 * ```
104 164 */
105 165 export const on = <E extends string>(...eventNames: E[]) =>
106 166 <K extends string,
107 167 T extends DjxWidgetBase<any, { [p in E]: EV }>,
108 168 EV extends Event
109 169 >(
110 170 target: T,
111 171 key: K,
112 172 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
113 173 ): any => {
114 174 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
115 175 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
116 176 };
@@ -1,114 +1,128
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
10 12 export interface EventArgs {
11 13 bubbles?: boolean;
12 14
13 15 cancelable?: boolean;
14 16
15 17 composed?: boolean;
16 18 }
17 19
18 20 export interface DjxWidgetBase<Attrs = {}, Events extends { [name in keyof Events]: Event } = {}> extends
19 21 _WidgetBase<Events> {
20 22
21 23 /** This property is declared only for type inference to work, it is never assigned
22 24 * and should not be used.
23 25 */
24 26 readonly _eventMap: Events & GlobalEventHandlersEventMap;
25 27
26 28 /** The list of pairs of event and method names. When the widget is created all methods from
27 29 * this list will be connected to corresponding events.
28 30 *
29 31 * This property is maintained in the prototype
30 32 */
31 33 _eventHandlers: Array<{
32 34 eventName: string,
33 35 handlerMethod: keyof any;
34 36 }>;
35 37 }
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
49 58 // place the contents of the original srcNode to the containerNode
50 59 const src = this.srcNodeRef;
51 60 const dest = this.containerNode;
52 61
53 62 // the donNode is constructed now we need to connect event handlers
54 63 this._connectEventHandlers();
55 64
56 65 if (src && dest) {
57 66 while (src.firstChild)
58 67 dest.appendChild(src.firstChild);
59 68 }
60 69 }
61 70
62 71 abstract render(): Rendition<HTMLElement>;
63 72
64 73 private _connectEventHandlers() {
65 74 if (this._eventHandlers)
66 75 this._eventHandlers.forEach(({ eventName, handlerMethod }) => {
67 76 const handler = this[handlerMethod as keyof this];
68 77 if (typeof handler === "function")
69 78 on(this.domNode, eventName, handler.bind(this));
70 79 });
71 80 }
72 81
73 82 _processTemplateNode<T extends (Element | Node | _WidgetBase)>(
74 83 baseNode: T,
75 84 getAttrFunc: (baseNode: T, attr: string) => any,
76 85 // tslint:disable-next-line: ban-types
77 86 attachFunc: (node: T, type: string, func?: Function) => dojo.Handle
78 87 ): boolean {
79 88 if (isNode(baseNode)) {
80 89 const w = registry.byNode(baseNode);
81 90 if (w) {
82 91 // from dijit/_WidgetsInTemplateMixin
83 92 this._processTemplateNode(w,
84 93 (n, p) => n.get(p as any), // callback to get a property of a widget
85 94 (widget, type, callback) => {
86 95 if (!callback)
87 96 throw new Error("The callback must be specified");
88 97
89 98 // callback to do data-dojo-attach-event to a widget
90 99 if (type in widget) {
91 100 // back-compat, remove for 2.0
92 101 return widget.connect(widget, type, callback as EventListener);
93 102 } else {
94 103 // 1.x may never hit this branch, but it's the default for 2.0
95 104 return widget.on(type, callback);
96 105 }
97 106
98 107 });
99 108 // don't process widgets internals
100 109 return false;
101 110 }
102 111 }
103 112 return super._processTemplateNode(baseNode, getAttrFunc, attachFunc);
104 113 }
105 114
106 115 /** Starts current widget and all its supporting widgets (placed outside
107 116 * `containerNode`) and child widgets (placed inside `containerNode`)
108 117 */
109 118 startup() {
110 119 // startup supporting widgets
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);
114 127 }
128 }
@@ -1,51 +1,50
1 1 import djDom = require("dojo/dom-construct");
2 2 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;
11 10
12 11 _element: Element | undefined;
13 12
14 13 constructor(elementType: string) {
15 14 argumentNotEmptyString(elementType, "elementType");
16 15 super();
17 16
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);
31 30 djAttr.set(this._element, attrs);
32 31 } else {
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() {
45 44 if (!this._element)
46 45 throw new Error("The HTML element isn't created");
47 46
48 47 return this._element;
49 48 }
50 49
51 50 }
@@ -1,73 +1,70
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 = {};
9 5
10 6 private _children = new Array();
11 7
12 8 private _created: boolean = false;
13 9
14 10 visitNext(v: any) {
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 {
27 23 this._children.push(v);
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;
37 33 }
38 34 }
39 35
40 36 /** Is rendition was instantiated to the DOM node */
41 37 isCreated() {
42 38 return this._created;
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
51 47 /** Creates DOM node if not created, places it to the specified position
52 48 * and calls startup() method for all widgets contained by this node.
53 49 *
54 50 * @param {string | Node} refNode The reference node where the created
55 51 * DOM should be placed.
56 52 * @param {DojoNodePosition} position Optional parameter, specifies the
57 53 * position relative to refNode. Default is "last" (i.e. last child).
58 54 */
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,40 +1,43
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 {
9 10 private readonly _cleanup: (() => void)[] = [];
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
23 26 clean() {
24 27 const guard = (cb: () => void) => {
25 28 try {
26 29 cb();
27 30 } catch {
28 31 // guard
29 32 }
30 33 }
31 34
32 35 this._cleanup.forEach(guard);
33 36 this._cleanup.length = 0;
34 37 }
35 38
36 39 destroy() {
37 40 this.clean();
38 41 }
39 42
40 43 } No newline at end of file
@@ -1,55 +1,97
1 1 import { id as mid } from "module";
2 2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 3 import { argumentNotNull } from "@implab/core-amd/safe";
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
25 30 this._component = component;
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() {
49 91 if (!this._node)
50 92 throw new Error("The instance of the widget isn't created");
51 93 return this._node;
52 94 }
53 95
54 96
55 97 }
@@ -1,134 +1,130
1 1 import { argumentNotNull } from "@implab/core-amd/safe";
2 2 import { RenditionBase } from "./RenditionBase";
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 {
11 10 domNode: Node;
12 11
13 12 containerNode?: Node;
14 13
15 14 placeAt?(refNode: string | Node, position?: DojoNodePosition): void;
16 15 startup?(): void;
17 16
18 17 addChild?(widget: unknown, index?: number): void;
19 18 }
20 19
21 20 export type _WidgetCtor = new (attrs: {}, srcNode?: string | Node) => _Widget;
22 21
23 22 export class WidgetRendition extends RenditionBase<Node> {
24 23 readonly widgetClass: _WidgetCtor;
25 24
26 25 _instance: _Widget | undefined;
27 26
28 27 constructor(widgetClass: _WidgetCtor) {
29 28 super();
30 29 argumentNotNull(widgetClass, "widgetClass");
31 30
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 {
45 44 const childDom = getItemDom(child);
46 45 const w = isElementNode(childDom) ? registry.byNode(childDom) : undefined;
47 46
48 47 if (w) {
49 48 instance.addChild(w);
50 49 } else {
51 50 if (!instance.containerNode)
52 51 throw new Error("Failed to add DOM content. The widget doesn't have a containerNode");
53 52
54 53 // the current widget isn't started, it's children shouldn't start too
55 54 placeAt(getItemDom(child), instance.containerNode, "last");
56 55 }
57 56 }
58 57 } else {
59 58 if (!instance.containerNode)
60 59 throw new Error("The widget doesn't have neither addChild nor containerNode");
61 60
62 61 // the current widget isn't started, it's children shouldn't start too
63 62 placeAt(getItemDom(child), instance.containerNode, "last");
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
71 70 // regular containers could be easily manipulated
72 71 // through `containerNode` property or `addChild` method.
73 72
74 73 // render children to the DocumentFragment
75 74 const content = document.createDocumentFragment();
76 75 children.forEach(child => content.appendChild(getItemDom(child)));
77 76
78 77 // set the content property to the parameters of the widget
79 78 const _attrs = { ...attrs, content };
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() {
94 90 if (!this._instance)
95 91 throw new Error("The instance of the widget isn't created");
96 92 return this._instance;
97 93 }
98 94
99 95 protected _getDomNode() {
100 96 if (!this._instance)
101 97 throw new Error("The instance of the widget isn't created");
102 98 return this._instance.domNode;
103 99 }
104 100
105 101 /** Overrides default placeAt implementation. Calls placeAt of the
106 102 * widget and then starts it.
107 103 *
108 104 * @param refNode A node or id of the node where the widget should be placed.
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);
116 112
117 113 // fix the dojo startup behavior when the widget is placed
118 114 // directly to the document and doesn't have any enclosing widgets
119 115 const parentWidget = instance.domNode.parentNode ?
120 116 registry.getEnclosingWidget(instance.domNode.parentNode) : null;
121 117 if (!parentWidget && isInPage(instance.domNode) && typeof instance.startup === "function")
122 118 instance.startup();
123 119 } else {
124 120 // the widget doesn't have a placeAt method, strange but whatever
125 121 super.placeAt(refNode, position);
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
134 130 }
@@ -1,104 +1,112
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 {
17 21 const result = cb()
18 22 if (isPromise(result)) {
19 23 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
20 24 result.then(warn, warn);
21 25 }
22 26 } catch (e) {
23 27 trace.error(e);
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
77 85 /** Renders DOM element for different types of the argument. */
78 86 export const getItemDom = (v: unknown) => {
79 87 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
80 88 // primitive types converted to the text nodes
81 89 return document.createTextNode(v.toString());
82 90 } else if (isNode(v)) {
83 91 // nodes are kept as is
84 92 return v;
85 93 } else if (isRendition(v)) {
86 94 // renditions are instantiated
87 95 return v.getDomNode();
88 96 } else if (isWidget(v)) {
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();
97 105 v.map(item => getItemDom(item))
98 106 .forEach(node => fragment.appendChild(node));
99 107 return fragment;
100 108 } else {
101 109 // bug: explicit error otherwise
102 110 throw new Error("Invalid parameter: " + v);
103 111 }
104 112 }
@@ -1,214 +1,214
1 1 import { IDestroyable } from "@implab/core-amd/interfaces";
2 2 import { isDestroyable } from "@implab/core-amd/safe";
3 3 import _WidgetBase = require("dijit/_WidgetBase");
4 4 import registry = require("dijit/registry");
5 5
6 6 interface _WidgetBaseConstructor {
7 7 new <A = {}, E extends { [k in keyof E]: Event } = {}>(params?: Partial<_WidgetBase<E> & A>, srcNodeRef?: dojo.NodeOrString): _WidgetBase<E> & dojo._base.DeclareCreatedObject;
8 8 prototype: _WidgetBase<any>;
9 9 }
10 10
11 11 export type DojoNodePosition = "first" | "after" | "before" | "last" | "replace" | "only" | number;
12 12
13 13 export type DojoNodeLocation = [Node, DojoNodePosition];
14 14
15 15 export interface Rendition<TNode extends Node = Node> {
16 16 getDomNode(): TNode;
17 17
18 18 placeAt(refNode: string | Node, position?: DojoNodePosition): void;
19 19 }
20 20
21 21 /**
22 22 * @deprecated use Rendition
23 23 */
24 24 export type BuildContext<TNode extends Node = Node> = Rendition<TNode>;
25 25
26 26 export interface IRecursivelyDestroyable {
27 27 destroyRecursive(): void;
28 28 }
29 29
30 30 export const isNode = (el: unknown): el is Node => !!(el && (el as Node).nodeName && (el as Node).nodeType);
31 31
32 32 export const isElementNode = (el: unknown): el is Element => isNode(el) && el.nodeType === 1;
33 33
34 34 export const isTextNode = (el: unknown): el is Text => isNode(el) && el.nodeType === 3;
35 35
36 36 export const isProcessingInstructionNode = (el: unknown): el is ProcessingInstruction => isNode(el) && el.nodeType === 7;
37 37
38 38 export const isCommentNode = (el: unknown): el is Comment => isNode(el) && el.nodeType === 8;
39 39
40 40 export const isDocumentNode = (el: unknown): el is Document => isNode(el) && el.nodeType === 9;
41 41
42 42 export const isDocumentTypeNode = (el: unknown): el is DocumentType => isNode(el) && el.nodeType === 10;
43 43
44 44 export const isDocumentFragmentNode = (el: any): el is DocumentFragment => isNode(el) && el.nodeType === 11;
45 45
46 46 export const isWidget = (v: unknown): v is _WidgetBase => !!(v && "domNode" in (v as _WidgetBase));
47 47
48 48 export const isRendition = (v: unknown): v is Rendition => !!(v && typeof (v as Rendition).getDomNode === "function");
49 49
50 50 /**
51 51 * @deprecated use isRendition
52 52 */
53 53 export const isBuildContext = isRendition;
54 54
55 55 export const isPlainObject = (v: object) => {
56 56 if (typeof v !== "object")
57 57 return false;
58 58
59 59 const vp = Object.getPrototypeOf(v);
60 60 return !vp || vp === Object.prototype;
61 61 }
62 62
63 63 export const isWidgetConstructor = (v: unknown): v is _WidgetBaseConstructor =>
64 64 typeof v === "function" && v.prototype && (
65 65 "domNode" in v.prototype ||
66 66 "buildRendering" in v.prototype
67 67 );
68 68
69 69
70 70 /** Tests whether the specified node is placed in visible dom.
71 71 * @param {Node} node The node to test
72 72 */
73 73 export const isInPage = (node: Node) => node === document.body ? false : document.body.contains(node);
74 74
75 75 export const isRecursivelyDestroyable = (target: unknown): target is IRecursivelyDestroyable =>
76 76 !!(target && typeof (target as IRecursivelyDestroyable).destroyRecursive === "function");
77 77
78 78
79 79
80 80 /** Destroys DOM Node with all contained widgets.
81 81 * If the specified node is the root node of a widget, then the
82 82 * widget will be destroyed.
83 83 *
84 84 * @param target DOM Node or widget to destroy
85 85 */
86 86 export const destroy = (target: Node | IDestroyable | IRecursivelyDestroyable) => {
87 87 if (isRecursivelyDestroyable(target)) {
88 88 target.destroyRecursive();
89 89 } else if (isDestroyable(target)) {
90 90 target.destroy();
91 91 } else if (isNode(target)) {
92 92 if (isElementNode(target)) {
93 93 const w = registry.byNode(target);
94 94 if (w) {
95 95 w.destroyRecursive();
96 return;
96 97 } else {
97 98 emptyNode(target);
99 }
100 }
98 101 const parent = target.parentNode;
99 102 if (parent)
100 103 parent.removeChild(target);
101 }
102 }
104
103 105 }
104 106 }
105 107
106 108 /** Empties a content of the specified node and destroys all contained widgets.
107 109 *
108 110 * @param target DOM node to empty.
109 111 */
110 112 export const emptyNode = (target: Node) => {
111 113 registry.findWidgets(target).forEach(destroy);
112 114
113 115 for (let c; c = target.lastChild;) { // intentional assignment
114 116 target.removeChild(c);
115 117 }
116 118 }
117 119
118 120 /** This function starts all widgets inside the DOM node if the target is a node
119 121 * or starts widget itself if the target is the widget. If the specified node
120 122 * associated with the widget that widget will be started.
121 123 *
122 124 * @param target DOM node to find and start widgets or the widget itself.
123 125 */
124 126 export const startupWidgets = (target: Node | _WidgetBase, skipNode?: Node) => {
125 127 if (isNode(target)) {
126 128 if (isElementNode(target)) {
127 129 const w = registry.byNode(target);
128 130 if (w) {
129 131 if (w.startup)
130 132 w.startup();
131 133 } else {
132 134 registry.findWidgets(target, skipNode).forEach(x => x.startup());
133 135 }
134 136 }
135 137 } else {
136 138 if (target.startup)
137 139 target.startup();
138 140 }
139 141 }
140 142
141 143 /** Places the specified DOM node at the specified location.
142 144 *
143 145 * @param node The node which should be placed
144 146 * @param refNodeOrId The reference node where the created
145 147 * DOM should be placed.
146 148 * @param position Optional parameter, specifies the
147 149 * position relative to refNode. Default is "last" (i.e. last child).
148 150 */
149 151 export const placeAt = (node: Node, refNodeOrId: string | Node, position: DojoNodePosition = "last") => {
150 152 const ref = typeof refNodeOrId == "string" ? document.getElementById(refNodeOrId) : refNodeOrId;
151 153 if (!ref)
152 154 return;
153 155
154 156 const parent = ref.parentNode;
155 157
156 158 if (typeof position == "number") {
157 159 if (ref.childNodes.length <= position) {
158 160 ref.appendChild(node);
159 161 } else {
160 162 ref.insertBefore(node, ref.childNodes[position]);
161 163 }
162 164 } else {
163 165 switch (position) {
164 166 case "before":
165 167 parent && parent.insertBefore(node, ref);
166 168 break;
167 169 case "after":
168 170 parent && parent.insertBefore(node, ref.nextSibling);
169 171 break;
170 172 case "first":
171 173 ref.insertBefore(node, ref.firstChild);
172 174 break;
173 175 case "last":
174 176 ref.appendChild(node);
175 177 break;
176 178 case "only":
177 179 emptyNode(ref);
178 180 ref.appendChild(node);
179 181 break;
180 182 case "replace":
181 183 if (parent)
182 184 parent.replaceChild(node, ref);
183 185 destroy(ref);
184 186 break;
185 187 }
186 188 }
187 189 }
188 190
189 191 /** Collects nodes from collection to an array.
190 192 *
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]);
198 200 }
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
@@ -1,81 +1,81
1 1 /// <reference path="./css-plugin.d.ts"/>
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 */
9 9 class: string;
10 10
11 11 /** specifies the name of the property in the widget where the the
12 12 * reference to the current object will be stored
13 13 */
14 14 "data-dojo-attach-point": string;
15 15
16 16 /** specifies handlers map for the events */
17 17 "data-dojo-attach-event": string;
18 18
19 19 ref: Ref<E>;
20 20
21 21 /** @deprecated */
22 22 [attr: string]: any;
23 23 }
24 24
25 25 interface DjxIntrinsicElements {
26 26 }
27 27
28 28 type RecursivePartial<T> = T extends string | number | boolean | null | undefined | Function ?
29 29 T :
30 30 { [k in keyof T]?: RecursivePartial<T[k]> };
31 31
32 32 type MatchingMemberKeys<T, U> = {
33 33 [K in keyof T]: T[K] extends U ? K : never;
34 34 }[keyof T];
35 35 type NotMatchingMemberKeys<T, U> = {
36 36 [K in keyof T]: T[K] extends U ? never : K;
37 37 }[keyof T];
38 38
39 39 type ExtractMembers<T, U> = Pick<T, MatchingMemberKeys<T, U>>;
40 40
41 41 type ExcludeMembers<T, U> = Pick<T, NotMatchingMemberKeys<T, U>>;
42 42
43 43 type ElementAttrNames<E> = NotMatchingMemberKeys<E, (...args: any[]) => any>;
44 44
45 45 type ElementAttrType<E, K extends keyof any> = K extends keyof E ? RecursivePartial<E[K]> : string;
46 46
47 47
48 48 type ElementAttrNamesBlacklist = "children" | "getRootNode" | keyof EventTarget;
49 49
50 50 /** This type extracts keys of the specified parameter E by the following rule:
51 51 * 1. skips all ElementAttrNamesBlacklist
52 52 * 2. skips all methods except with the signature of event handlers
53 53 */
54 54 type AssignableElementAttrNames<E> = {
55 55 [K in keyof E]: K extends ElementAttrNamesBlacklist ? never :
56 56 ((evt: Event) => any) extends E[K] ? K :
57 57 E[K] extends ((...args: any[]) => any) ? never :
58 58 K;
59 59 }[keyof E];
60 60
61 61 type LaxElement<E extends object> =
62 62 Pick<E, AssignableElementAttrNames<E>> &
63 63 DjxIntrinsicAttributes<E>;
64 64
65 65 type LaxIntrinsicElementsMap = {
66 66 [tag in keyof HTMLElementTagNameMap]: LaxElement<HTMLElementTagNameMap[tag]>
67 67 } & DjxIntrinsicElements;
68 68
69 69 type IntrinsicElements = {
70 70 [tag in keyof LaxIntrinsicElementsMap]: RecursivePartial<LaxIntrinsicElementsMap[tag]>;
71 71 }
72 72
73 73 interface ElementChildrenAttribute {
74 74 children: {};
75 75 }
76 76
77 77 interface IntrinsicClassAttributes<T> {
78 78 ref?: (value: T) => void;
79 79 children?: unknown;
80 80 }
81 81 }
@@ -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
@@ -1,123 +1,126
1 1 plugins {
2 2 id "org.implab.gradle-typescript" version "1.3.4"
3 3 id "ivy-publish"
4 4 }
5 5
6 6 def container = "djx-playground"
7 7
8 8 configurations {
9 9 npmLocal
10 10 }
11 11
12 12 dependencies {
13 13 npmLocal project(":djx")
14 14 }
15 15
16 16 def bundleDir = fileTree(layout.buildDirectory.dir("bundle")) {
17 17 builtBy "bundle"
18 18 }
19 19
20 20 typescript {
21 21 compilerOptions {
22 22 lib = ["es5", "dom", "scripthost", "es2015.promise", "es2015.symbol", "es2015.iterable"]
23 23 // listFiles = true
24 24 strict = true
25 25 types = ["requirejs", "@implab/dojo-typings", "@implab/djx"]
26 26 module = "amd"
27 27 it.target = "es5"
28 28 experimentalDecorators = true
29 29 noUnusedLocals = false
30 30 jsx = "react"
31 31 jsxFactory = "createElement"
32 32 moduleResolution = "node"
33 33 // dojo-typings are sick
34 34 skipLibCheck = true
35 35 // traceResolution = true
36 36 // baseUrl = "./"
37 37 // paths = [ "*": [ "$projectDir/src/typings/*" ] ]
38 38 // baseUrl = "$projectDir/src/typings"
39 39 // typeRoots = ["$projectDir/src/typings"]
40 40 }
41 41 tscCmd = "$projectDir/node_modules/.bin/tsc"
42 42 tsLintCmd = "$projectDir/node_modules/.bin/tslint"
43 43 esLintCmd = "$projectDir/node_modules/.bin/eslint"
44 44 }
45 45
46 46 tasks.matching{ it.name =~ /^configureTs/ }.configureEach {
47 47 compilerOptions {
48 48 if (symbols != 'none') {
49 49 sourceMap = true
50 50 switch(symbols) {
51 51 case "local":
52 52 sourceRoot = ( isWindows ? "file:///" : "file://" ) + it.rootDir
53 53 break;
54 54 }
55 55 }
56 56 }
57 57 }
58 58
59 59 npmInstall {
60 60 //npmInstall.dependsOn it
61 61 dependsOn configurations.npmLocal
62 62
63 63 doFirst {
64 64 configurations.npmLocal.each { f ->
65 65 exec {
66 66 commandLine "npm", "install", f, "--save-dev"
67 67 }
68 68 }
69 69 }
70 70 }
71 71
72 72 clean {
73 73 doFirst {
74 74 delete "$buildDir/bundle"
75 75 }
76 76 }
77 77
78 78
79 79 task processResourcesBundle(type: Copy) {
80 80 from "src/bundle"
81 81 into layout.buildDirectory.dir("bundle")
82 82 }
83 83
84 84 task copyModules(type: Copy) {
85 85 dependsOn npmInstall
86 86 into layout.buildDirectory.dir("bundle/js");
87 87
88 88 def pack = { String jsmod ->
89 89 into(jsmod) {
90 90 from npm.module(jsmod)
91 91 }
92 92 }
93 93
94 94
95 95 pack("@implab/djx")
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
102 105 task copyApp(type: Copy) {
103 106 dependsOn assemble
104 107 from typescript.assemblyDir
105 108 into layout.buildDirectory.dir("bundle/js/app")
106 109 }
107 110
108 111 task bundle {
109 112 dependsOn copyModules, processResourcesBundle, copyApp
110 113 }
111 114
112 115 task up(type: Exec) {
113 116 dependsOn bundle
114 117 commandLine "podman", "run", "--rm", "-d",
115 118 "--name", container,
116 119 "-p", "2078:80",
117 120 "-v", "$buildDir/bundle:/srv/www/htdocs",
118 121 "registry.implab.org/implab/apache2:latest"
119 122 }
120 123
121 124 task stop(type: Exec) {
122 125 commandLine "podman", "stop", container
123 126 } No newline at end of file
@@ -1,143 +1,170
1 1 {
2 2 "name": "@implab/djx-playground",
3 3 "lockfileVersion": 2,
4 4 "requires": true,
5 5 "packages": {
6 6 "": {
7 7 "name": "@implab/djx-playground",
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",
15 16 "@implab/djx": "file:../djx/build/npm/package",
16 17 "@implab/dojo-typings": "1.0.2",
17 18 "@types/requirejs": "2.1.34",
18 19 "typescript": "4.8.2"
19 20 }
20 21 },
21 22 "../djx/build/npm/package": {
22 23 "name": "@implab/djx",
23 24 "dev": true,
24 25 "license": "BSD-2-Clause",
25 26 "peerDependencies": {
26 27 "@implab/core-amd": "^1.4.0",
27 28 "dojo": "^1.10.0"
28 29 }
29 30 },
30 31 "node_modules/@implab/core-amd": {
31 32 "version": "1.4.6",
32 33 "resolved": "https://registry.npmjs.org/@implab/core-amd/-/core-amd-1.4.6.tgz",
33 34 "integrity": "sha512-I1RwUAxeiodePpiBzveoHaehMSAyk7NFPPPEvDqfphHBC8yXoXWAaUrp7EcOKEzjXAs7lJQVhNpmjCjIqoj6BQ==",
34 35 "dev": true,
35 36 "peerDependencies": {
36 37 "dojo": "^1.10.0"
37 38 }
38 39 },
39 40 "node_modules/@implab/djx": {
40 41 "resolved": "../djx/build/npm/package",
41 42 "link": true
42 43 },
43 44 "node_modules/@implab/dojo-typings": {
44 45 "version": "1.0.2",
45 46 "resolved": "https://registry.npmjs.org/@implab/dojo-typings/-/dojo-typings-1.0.2.tgz",
46 47 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
47 48 "dev": true
48 49 },
49 50 "node_modules/@types/requirejs": {
50 51 "version": "2.1.34",
51 52 "resolved": "https://registry.npmjs.org/@types/requirejs/-/requirejs-2.1.34.tgz",
52 53 "integrity": "sha512-iQLGNE1DyIRYih60B47l/hI5X7J0wAnnRBL6Yn85GUYQg8Fm3wl8kvT6NRwncKroUOSx7/lbAagIFNV7y02DiQ==",
53 54 "dev": true
54 55 },
55 56 "node_modules/dijit": {
56 57 "version": "1.17.3",
57 58 "resolved": "https://registry.npmjs.org/dijit/-/dijit-1.17.3.tgz",
58 59 "integrity": "sha512-QS+1bNhPT+BF9E+iomQSi5qI+o3oUNSx1r5TF8WlGH4LybGZP+IIGJBOO5/41YduBPljVXhY7vaPsgrycxC6UQ==",
59 60 "dependencies": {
60 61 "dojo": "1.17.3"
61 62 }
62 63 },
63 64 "node_modules/dojo": {
64 65 "version": "1.17.3",
65 66 "resolved": "https://registry.npmjs.org/dojo/-/dojo-1.17.3.tgz",
66 67 "integrity": "sha512-iWDx1oSfCEDnIrs8cMW7Zh9Fbjgxu8iRagFz+Qi2eya3MXIAxFXKhv2A7dpi+bfpMpFozLwcsLV8URLw6BsHsA=="
67 68 },
68 69 "node_modules/requirejs": {
69 70 "version": "2.3.6",
70 71 "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",
71 72 "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==",
72 73 "bin": {
73 74 "r_js": "bin/r.js",
74 75 "r.js": "bin/r.js"
75 76 },
76 77 "engines": {
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",
83 97 "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
84 98 "dev": true,
85 99 "bin": {
86 100 "tsc": "bin/tsc",
87 101 "tsserver": "bin/tsserver"
88 102 },
89 103 "engines": {
90 104 "node": ">=4.2.0"
91 105 }
92 106 }
93 107 },
94 108 "dependencies": {
95 109 "@implab/core-amd": {
96 110 "version": "1.4.6",
97 111 "resolved": "https://registry.npmjs.org/@implab/core-amd/-/core-amd-1.4.6.tgz",
98 112 "integrity": "sha512-I1RwUAxeiodePpiBzveoHaehMSAyk7NFPPPEvDqfphHBC8yXoXWAaUrp7EcOKEzjXAs7lJQVhNpmjCjIqoj6BQ==",
99 113 "dev": true,
100 114 "requires": {}
101 115 },
102 116 "@implab/djx": {
103 117 "version": "file:../djx/build/npm/package",
104 118 "requires": {}
105 119 },
106 120 "@implab/dojo-typings": {
107 121 "version": "1.0.2",
108 122 "resolved": "https://registry.npmjs.org/@implab/dojo-typings/-/dojo-typings-1.0.2.tgz",
109 123 "integrity": "sha512-/lbcMCHdRoHJLKFcT8xdk1KbGazSlb1pGSDJ406io7iMenPm/XbJYcUti+VzXnn71zOJ8aYpGT12T5L0rfOZNA==",
110 124 "dev": true
111 125 },
112 126 "@types/requirejs": {
113 127 "version": "2.1.34",
114 128 "resolved": "https://registry.npmjs.org/@types/requirejs/-/requirejs-2.1.34.tgz",
115 129 "integrity": "sha512-iQLGNE1DyIRYih60B47l/hI5X7J0wAnnRBL6Yn85GUYQg8Fm3wl8kvT6NRwncKroUOSx7/lbAagIFNV7y02DiQ==",
116 130 "dev": true
117 131 },
118 132 "dijit": {
119 133 "version": "1.17.3",
120 134 "resolved": "https://registry.npmjs.org/dijit/-/dijit-1.17.3.tgz",
121 135 "integrity": "sha512-QS+1bNhPT+BF9E+iomQSi5qI+o3oUNSx1r5TF8WlGH4LybGZP+IIGJBOO5/41YduBPljVXhY7vaPsgrycxC6UQ==",
122 136 "requires": {
123 137 "dojo": "1.17.3"
124 138 }
125 139 },
126 140 "dojo": {
127 141 "version": "1.17.3",
128 142 "resolved": "https://registry.npmjs.org/dojo/-/dojo-1.17.3.tgz",
129 143 "integrity": "sha512-iWDx1oSfCEDnIrs8cMW7Zh9Fbjgxu8iRagFz+Qi2eya3MXIAxFXKhv2A7dpi+bfpMpFozLwcsLV8URLw6BsHsA=="
130 144 },
131 145 "requirejs": {
132 146 "version": "2.3.6",
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",
139 166 "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
140 167 "dev": true
141 168 }
142 169 }
143 170 }
@@ -1,16 +1,17
1 1 {
2 2 "name": "@implab/djx-playground",
3 3 "private": true,
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",
11 12 "@implab/djx": "file:../djx/build/npm/package",
12 13 "@implab/dojo-typings": "1.0.2",
13 14 "@types/requirejs": "2.1.34",
14 15 "typescript": "4.8.2"
15 16 }
16 17 }
@@ -1,11 +1,16
1 1 requirejs.config({
2 2 baseUrl: "js",
3 3 packages: [
4 4 "app",
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,49 +1,82
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) {
11 10
12 11 titleNode?: HTMLHeadingElement;
13 12
14 13 progressBar?: ProgressBar;
15 14
16 15 count = 0;
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 &&
27 <h2 ref={attach(this, "titleNode")}>Hi!</h2>
28 <ProgressBar ref={attach(this, "progressBar")} />
25 29 <section style={{padding: "10px"}}>
26 <label>
27 Counter: {watch(this, "count", v => [<span>{v}</span>, " ", <span>sec</span>])}
28 </label>
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 )}
29 54 </section>
30 )}
31 55 <Button onClick={this._onToggleCounterClick}>Toggle counter</Button>
32 56 </div>;
33 57 }
34 58
35 59 postCreate(): void {
36 60 super.postCreate();
37 61
38 const inc = () => {
62 const h = setInterval(
63 () => {
39 64 this.set("count", this.count + 1);
40 this.defer(inc, 1000);
65 },
66 10
67 );
68 this.own({
69 destroy: () => {
70 clearInterval(h);
71 }
72 });
41 73 }
42 74
43 inc();
75 private _onPauseClick = () => {
76 this.set("paused", !this.paused);
44 77 }
45 78
46 79 private _onToggleCounterClick = () => {
47 80 this.set("showCounter", !this.showCounter);
48 81 }
49 82 }
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