##// END OF EJS Templates
Testing nested watch, release candidate
cin -
r101:bb6b1db1b430 v1.3
parent child
Show More
@@ -0,0 +1,1
1 symbols=local No newline at end of file
@@ -1,8 +1,7
1 1 group=org.implab.implabjs
2 2 version=
3 3 author=Implab team
4 4 description=Create HyperText with Typescript, integrate Dojo1 widgets in your .tsx scripts.
5 5 license=BSD-2-Clause
6 6 repository=http://hg.code.sf.net/p/implabjs/djx
7 7 npmScope=implab
8 symbols=pack No newline at end of file
@@ -1,34 +1,34
1 1 import { IDestroyable } from "@implab/core-amd/interfaces";
2 2
3 3 export interface Sink<T> {
4 4 next: (value: T) => void;
5 5 error: (e: unknown) => void;
6 6 complete: () => void;
7 7 }
8 8
9 9 export type Consumer<T> = Partial<Sink<T>>;
10 10
11 11 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
12 12
13 13 export interface Observable<T> {
14 14 on(consumer: Partial<Sink<T>>): IDestroyable;
15 15 }
16 16
17 17 const noop = () => {};
18 18
19 19 const sink = <T>(consumer: Consumer<T>) => {
20 20 const { next = noop, error = noop, complete = noop } = consumer;
21 21 let done = false;
22 22
23 23 return {
24 next: (value: T) => done && next(value),
25 error: (e: unknown) => done && (done = true, error(e)),
26 complete: () => done && (done = true, complete())
24 next: (value: T) => !done && next(value),
25 error: (e: unknown) => !done && (done = true, error(e)),
26 complete: () => !done && (done = true, complete())
27 27 };
28 28 }
29 29
30 30 export const observe = <T>(producer: Producer<T>) : Observable<T> => ({
31 31 on: (consumer: Consumer<T>) => ({
32 32 destroy: producer(sink(consumer)) ?? noop
33 33 })
34 34 });
@@ -1,114 +1,116
1 1 import { Constructor } from "@implab/core-amd/interfaces";
2 2 import { HtmlRendition } from "./tsx/HtmlRendition";
3 3 import { WidgetRendition } from "./tsx/WidgetRendition";
4 4 import { 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 10 import { observe } from "./observable";
11 11
12 12 export function createElement<T extends Constructor | string | ((props: any) => Element)>(elementType: T, ...args: any[]): Rendition {
13 13 if (typeof elementType === "string") {
14 14 const ctx = new HtmlRendition(elementType);
15 15 if (args)
16 16 args.forEach(x => ctx.visitNext(x));
17 17
18 18 return ctx;
19 19 } else if (isWidgetConstructor(elementType)) {
20 20 const ctx = new WidgetRendition(elementType);
21 21 if (args)
22 22 args.forEach(x => ctx.visitNext(x));
23 23
24 24 return ctx;
25 25 } else if (typeof elementType === "function") {
26 26 const ctx = new FunctionRendition(elementType as (props: any) => Element);
27 27 if (args)
28 28 args.forEach(x => ctx.visitNext(x));
29 29
30 30 return ctx;
31 31 } else {
32 32 throw new Error(`The element type '${elementType}' is unsupported`);
33 33 }
34 34 }
35 35
36 36 export interface EventDetails<T = any> {
37 37 detail: T;
38 38 }
39 39
40 40 export interface EventSelector {
41 41 selectorTarget: HTMLElement;
42 42 target: HTMLElement;
43 43 }
44 44
45 45 export type DojoMouseEvent<T = any> = MouseEvent & EventSelector & EventDetails<T>;
46 46
47 47 type StatefulProps<T> = T extends Stateful<infer A> ? A : never;
48 48
49 49
50 50 /**
51 51 * Observers the property and calls render callback each change.
52 52 *
53 53 * @param target The target object which property will be observed.
54 54 * @param prop The name of the property.
55 55 * @param render The callback which will be called every time the value is changed
56 56 * @returns Rendition which is created instantly
57 57 */
58 58 export function watch<W extends _WidgetBase, K extends keyof W>(
59 59 target: W,
60 60 prop: K,
61 61 render: (model: W[K]) => any
62 62 ): Rendition;
63 63 /**
64 64 * Observers the property and calls render callback each change.
65 65 *
66 66 * @param target The target object which property will be observed.
67 67 * @param prop The name of the property.
68 68 * @param render The callback which will be called every time the value is changed
69 69 * @returns Rendition which is created instantly
70 70 */
71 71 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
72 72 target: T,
73 73 prop: K,
74 74 render: (model: StatefulProps<T>[K]) => any
75 75 ): Rendition;
76 76 export function watch<T extends Stateful, K extends keyof StatefulProps<T> & string>(
77 77 target: T,
78 78 prop: K,
79 79 render: (model: StatefulProps<T>[K]) => any
80 80 ) {
81
81 82 return new WatchRendition(
82 83 render,
83 84 observe(({next}) => {
84 85 const h = target.watch(
85 86 prop,
86 87 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
87 88 );
89 next(target.get(prop));
88 90 return () => h.remove();
89 91 })
90 92 )
91 93 }
92 94
93 95 /** Decorates the method which will be registered as the handle for the specified event.
94 96 * This decorator can be applied to DjxWidgetBase subclass methods.
95 97 *
96 98 * ```
97 99 * @on("click")
98 100 * _onClick(eventObj: MouseEvent) {
99 101 * // ...
100 102 * }
101 103 * ```
102 104 */
103 105 export const on = <E extends string>(...eventNames: E[]) =>
104 106 <K extends string,
105 107 T extends DjxWidgetBase<any, { [p in E]: EV }>,
106 108 EV extends Event
107 109 >(
108 110 target: T,
109 111 key: K,
110 112 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
111 113 ): any => {
112 114 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
113 115 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
114 116 };
@@ -1,33 +1,30
1 1 import { argumentNotNull } from "@implab/core-amd/safe";
2 2 import { getItemDom } from "./render";
3 3 import { RenditionBase } from "./RenditionBase";
4 import { IScope } from "./Scope";
5 4
6 5 export class FunctionRendition extends RenditionBase<Node> {
7 6 private _component: (...args: any[]) => any;
8 7
9 8 private _node: Node | undefined;
10 9
11 10 constructor(component: (...args: any[]) => any) {
12 11 super();
13 12 argumentNotNull(component, "component");
14 13
15 14 this._component = component;
16 15 }
17 16
18 protected _create(attrs: object, children: any[], scope: IScope) {
17 protected _create(attrs: object, children: any[]) {
19 18 const _attrs: any = attrs || {};
20 const _children = children.map(x => getItemDom(x, scope));
19 const _children = children.map(x => getItemDom(x));
21 20 this._node = getItemDom(
22 this._component.call(null, { ..._attrs, children: _children }),
23 scope
24 );
21 this._component.call(null, { ..._attrs, children: _children }));
25 22 }
26 23
27 24 protected _getDomNode() {
28 25 if (!this._node)
29 26 throw new Error("The instance of the widget isn't created");
30 27 return this._node;
31 28 }
32 29
33 30 }
@@ -1,39 +1,51
1 import dom = require("dojo/dom-construct");
1 import djDom = require("dojo/dom-construct");
2 import djAttr = require("dojo/dom-attr");
2 3 import { argumentNotEmptyString } from "@implab/core-amd/safe";
3 4 import { RenditionBase } from "./RenditionBase";
4 5 import { placeAt } from "./traits";
5 6 import { IScope } from "./Scope";
6 import { getItemDom } from "./render";
7 import { getItemDom, renderHook } from "./render";
7 8
8 export class HtmlRendition extends RenditionBase<HTMLElement> {
9 export class HtmlRendition extends RenditionBase<Element> {
9 10 elementType: string;
10 11
11 _element: HTMLElement | undefined;
12 _element: Element | undefined;
12 13
13 14 constructor(elementType: string) {
14 15 argumentNotEmptyString(elementType, "elementType");
15 16 super();
16 17
17 18 this.elementType = elementType;
18 19 }
19 20
20 21 _addChild(child: unknown, scope: IScope): void {
21 22 if (!this._element)
22 23 throw new Error("The HTML element isn't created");
23 placeAt(getItemDom(child, scope), this._element);
24 placeAt(getItemDom(child), this._element);
24 25 }
25 26
26 _create(attrs: object, children: unknown[], scope: IScope) {
27 this._element = dom.create(this.elementType, attrs);
27 _create({ xmlns, ref, ...attrs }: { xmlns?: string, ref?: JSX.Ref<Element> }, children: unknown[], scope: IScope) {
28
29 if (xmlns) {
30 this._element = document.createElementNS(xmlns, this.elementType);
31 djAttr.set(this._element, attrs);
32 } else {
33 this._element = djDom.create(this.elementType, attrs);
34 }
28 35
29 36 children.forEach(v => this._addChild(v, scope));
37
38 const element = this._element;
39
40 if (ref)
41 renderHook(() => ref(element));
30 42 }
31 43
32 44 _getDomNode() {
33 45 if (!this._element)
34 46 throw new Error("The HTML element isn't created");
35 47
36 48 return this._element;
37 49 }
38 50
39 51 }
@@ -1,55 +1,55
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 import { getItemDom, render } from "./render";
4 import { getScope, render } from "./render";
5 5 import { RenditionBase } from "./RenditionBase";
6 import { IScope, Scope } from "./Scope";
6 import { Scope } from "./Scope";
7 7 import { Observable } from "../observable";
8 import { destroy } from "./traits";
8 9
9 10 const trace = TraceSource.get(mid);
10 11
11 12 export class WatchRendition<T> extends RenditionBase<Node> {
12 13 private readonly _component: (arg: T) => unknown;
13 14
14 15 private _node: Node;
15 16
16 17 private readonly _scope = new Scope();
17 18
18 19 private readonly _subject: Observable<T>;
19 20
20 21 constructor(component: (arg: T) => unknown, subject: Observable<T>) {
21 22 super();
22 23 argumentNotNull(component, "component");
23 24
24 25 this._component = component;
25 26
26 27 this._subject = subject;
27 28
28 29 this._node = document.createComment("WatchRendition placeholder");
29 30 }
30 31
31 protected _create(attrs: object, children: any[], scope: IScope) {
32 protected _create(attrs: object, children: any[]) {
33 const scope = getScope();
32 34 scope.own(this._scope);
33 35 scope.own(this._subject.on({ next: this._onValue }));
34 36 }
35 37
36 38 private _onValue = (value: T) =>
37 39 void this._render(value).catch( e => trace.error(e));
38 40
39 41 private async _render(value: T) {
40 const prevNode = this._node;
41 42 this._scope.clean();
42
43 this._node = await render(this._component(value), this._scope);
44
45 this.placeAt(prevNode, "replace");
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));
46 46 }
47 47
48 48 protected _getDomNode() {
49 49 if (!this._node)
50 50 throw new Error("The instance of the widget isn't created");
51 51 return this._node;
52 52 }
53 53
54 54
55 55 }
@@ -1,129 +1,134
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 6 import { IScope } from "./Scope";
7 import { getItemDom, getScope } from "./render";
7 import { getItemDom, getScope, renderHook } from "./render";
8 8
9 9 // tslint:disable-next-line: class-name
10 10 export interface _Widget {
11 11 domNode: Node;
12 12
13 13 containerNode?: Node;
14 14
15 15 placeAt?(refNode: string | Node, position?: DojoNodePosition): void;
16 16 startup?(): void;
17 17
18 addChild?(widget: any, index?: number): void;
18 addChild?(widget: unknown, index?: number): void;
19 19 }
20 20
21 export type _WidgetCtor = new (attrs: any, srcNode?: string | Node) => _Widget;
21 export type _WidgetCtor = new (attrs: {}, srcNode?: string | Node) => _Widget;
22 22
23 23 export class WidgetRendition extends RenditionBase<Node> {
24 24 readonly widgetClass: _WidgetCtor;
25 25
26 26 _instance: _Widget | undefined;
27 27
28 28 constructor(widgetClass: _WidgetCtor) {
29 29 super();
30 30 argumentNotNull(widgetClass, "widgetClass");
31 31
32 32 this.widgetClass = widgetClass;
33 33 }
34 34
35 _addChild(child: any, scope: IScope): void {
35 _addChild(child: unknown, scope: IScope): void {
36 36 const instance = this._getInstance();
37 37
38 38 if (instance.addChild) {
39 39 if (child instanceof WidgetRendition) {
40 40 // layout containers add custom logic to addChild methods
41 41 instance.addChild(child.getWidgetInstance(scope));
42 42 } else if (isWidget(child)) {
43 43 instance.addChild(child);
44 44 } else {
45 const childDom = getItemDom(child, scope);
45 const childDom = getItemDom(child);
46 46 const w = isElementNode(childDom) ? registry.byNode(childDom) : undefined;
47 47
48 48 if (w) {
49 49 instance.addChild(w);
50 50 } else {
51 51 if (!instance.containerNode)
52 52 throw new Error("Failed to add DOM content. The widget doesn't have a containerNode");
53 53
54 54 // the current widget isn't started, it's children shouldn't start too
55 placeAt(getItemDom(child,scope), instance.containerNode, "last");
55 placeAt(getItemDom(child), instance.containerNode, "last");
56 56 }
57 57 }
58 58 } else {
59 59 if (!instance.containerNode)
60 60 throw new Error("The widget doesn't have neither addChild nor containerNode");
61 61
62 62 // the current widget isn't started, it's children shouldn't start too
63 placeAt(getItemDom(child, scope), instance.containerNode, "last");
63 placeAt(getItemDom(child), instance.containerNode, "last");
64 64 }
65 65 }
66 66
67 protected _create(attrs: any, children: any[], scope: IScope) {
67 protected _create({ref, ...attrs}: {ref?: JSX.Ref<_Widget>}, children: unknown[], scope: IScope) {
68 68 if (this.widgetClass.prototype instanceof ContentPane) {
69 69 // a special case for the ContentPane this is for
70 // the compatibility with this heavy widget, all
70 // compatibility with that heavy widget, all
71 71 // regular containers could be easily manipulated
72 72 // through `containerNode` property or `addChild` method.
73 73
74 74 // render children to the DocumentFragment
75 75 const content = document.createDocumentFragment();
76 children.forEach(child => content.appendChild(getItemDom(child, scope)));
76 children.forEach(child => content.appendChild(getItemDom(child)));
77 77
78 78 // set the content property to the parameters of the widget
79 79 const _attrs = { ...attrs, content };
80 80 this._instance = new this.widgetClass(_attrs);
81 81 } else {
82 82 this._instance = new this.widgetClass(attrs);
83 83 children.forEach(x => this._addChild(x, scope));
84 84 }
85 85
86 if (ref) {
87 const instance = this._instance;
88 renderHook(() => ref(instance));
89 }
90
86 91 }
87 92
88 93 private _getInstance() {
89 94 if (!this._instance)
90 95 throw new Error("The instance of the widget isn't created");
91 96 return this._instance;
92 97 }
93 98
94 99 protected _getDomNode() {
95 100 if (!this._instance)
96 101 throw new Error("The instance of the widget isn't created");
97 102 return this._instance.domNode;
98 103 }
99 104
100 105 /** Overrides default placeAt implementation. Calls placeAt of the
101 106 * widget and then starts it.
102 107 *
103 108 * @param refNode A node or id of the node where the widget should be placed.
104 109 * @param position A position relative to refNode.
105 110 */
106 111 placeAt(refNode: string | Node, position?: DojoNodePosition) {
107 112 this.ensureCreated(getScope());
108 113 const instance = this._getInstance();
109 114 if (typeof instance.placeAt === "function") {
110 115 instance.placeAt(refNode, position);
111 116
112 117 // fix the dojo startup behavior when the widget is placed
113 118 // directly to the document and doesn't have any enclosing widgets
114 119 const parentWidget = instance.domNode.parentNode ?
115 120 registry.getEnclosingWidget(instance.domNode.parentNode) : null;
116 121 if (!parentWidget && isInPage(instance.domNode) && typeof instance.startup === "function")
117 122 instance.startup();
118 123 } else {
119 124 // the widget doesn't have a placeAt method, strange but whatever
120 125 super.placeAt(refNode, position);
121 126 }
122 127 }
123 128
124 129 getWidgetInstance(scope?: IScope) {
125 130 this.ensureCreated(scope || getScope());
126 131 return this._getInstance();
127 132 }
128 133
129 134 }
@@ -1,60 +1,104
1 import { IScope, Scope } from "./Scope";
2 import { destroy, isNode, isRendition, isWidget, Rendition } from "./traits";
1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { isPromise } from "@implab/core-amd/safe";
3 import { id as mid } from "module";
4 import { Scope } from "./Scope";
5 import { autostartWidgets, collectNodes, DojoNodePosition, isDocumentFragmentNode, isNode, isRendition, isWidget, placeAt } from "./traits";
6
7 const trace = TraceSource.get(mid);
3 8
4 9 let _scope = Scope.dummy;
5 10
6 const beginRender = async () => {
11 let renderCount = 0;
12
13 const hooks: (() => void)[] = [];
14
15 const guard = (cb: () => unknown) => {
16 try {
17 const result = cb()
18 if (isPromise(result)) {
19 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
20 result.then(warn, warn);
21 }
22 } catch (e) {
23 trace.error(e);
24 }
7 25 }
8 26
27 /**
28 * Schedules rendering micro task
29 * @returns Promise
30 */
31 const beginRender = () => {
32 renderCount++;
33 return Promise.resolve();
34 }
35
36 /**
37 * Completes render operation
38 */
9 39 const endRender = () => {
40 if (!--renderCount) {
41 hooks.forEach(guard);
42 hooks.length = 0;
43 }
44 }
45
46 export const renderHook = (hook: () => void) => {
47 if (renderCount)
48 hooks.push(hook);
49 else
50 guard(hook);
10 51 }
11 52
12 53 /** Returns the current scope */
13 54 export const getScope = () => _scope;
14 55
15 56 /** Schedules the rendition to be rendered to the DOM Node
16 57 * @param rendition The rendition to be rendered
17 58 * @param scope The scope
18 59 */
19 export const render = async (rendition: unknown, scope = Scope.dummy) => {
60 export const render = async (rendition: unknown, refNode: Node, position: DojoNodePosition = "last", scope = Scope.dummy) => {
20 61 await beginRender();
21 62 const prev = _scope;
22 63 _scope = scope;
23 64 try {
24 const node = getItemDom(rendition, scope);
25 scope.own(() => destroy(node));
26 return node;
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;
27 71 } finally {
28 72 _scope = prev;
29 73 endRender();
30 74 }
31 75 }
32 76
33 77 /** Renders DOM element for different types of the argument. */
34 export const getItemDom = (v: unknown, scope: IScope) => {
78 export const getItemDom = (v: unknown) => {
35 79 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
36 80 // primitive types converted to the text nodes
37 81 return document.createTextNode(v.toString());
38 82 } else if (isNode(v)) {
39 83 // nodes are kept as is
40 84 return v;
41 85 } else if (isRendition(v)) {
42 86 // renditions are instantiated
43 return v.getDomNode(scope);
87 return v.getDomNode();
44 88 } else if (isWidget(v)) {
45 89 // widgets are converted to it's markup
46 90 return v.domNode;
47 91 } else if (typeof v === "boolean" || v === null || v === undefined) {
48 92 // null | undefined | boolean are removed, converted to comments
49 93 return document.createComment(`[${typeof v} ${String(v)}]`);
50 94 } else if (v instanceof Array) {
51 95 // arrays will be translated to document fragments
52 96 const fragment = document.createDocumentFragment();
53 v.map(item => getItemDom(item, scope))
97 v.map(item => getItemDom(item))
54 98 .forEach(node => fragment.appendChild(node));
55 99 return fragment;
56 100 } else {
57 101 // bug: explicit error otherwise
58 102 throw new Error("Invalid parameter: " + v);
59 103 }
60 104 }
@@ -1,215 +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 import { IScope } from "./Scope";
6 5
7 6 interface _WidgetBaseConstructor {
8 7 new <A = {}, E extends { [k in keyof E]: Event } = {}>(params?: Partial<_WidgetBase<E> & A>, srcNodeRef?: dojo.NodeOrString): _WidgetBase<E> & dojo._base.DeclareCreatedObject;
9 8 prototype: _WidgetBase<any>;
10 9 }
11 10
12 11 export type DojoNodePosition = "first" | "after" | "before" | "last" | "replace" | "only" | number;
13 12
14 13 export type DojoNodeLocation = [Node, DojoNodePosition];
15 14
16 15 export interface Rendition<TNode extends Node = Node> {
17 getDomNode(scope?: IScope): TNode;
16 getDomNode(): TNode;
18 17
19 18 placeAt(refNode: string | Node, position?: DojoNodePosition): void;
20 19 }
21 20
22 21 /**
23 22 * @deprecated use Rendition
24 23 */
25 24 export type BuildContext<TNode extends Node = Node> = Rendition<TNode>;
26 25
27 26 export interface IRecursivelyDestroyable {
28 27 destroyRecursive(): void;
29 28 }
30 29
31 30 export const isNode = (el: unknown): el is Node => !!(el && (el as Node).nodeName && (el as Node).nodeType);
32 31
33 32 export const isElementNode = (el: unknown): el is Element => isNode(el) && el.nodeType === 1;
34 33
35 34 export const isTextNode = (el: unknown): el is Text => isNode(el) && el.nodeType === 3;
36 35
37 36 export const isProcessingInstructionNode = (el: unknown): el is ProcessingInstruction => isNode(el) && el.nodeType === 7;
38 37
39 38 export const isCommentNode = (el: unknown): el is Comment => isNode(el) && el.nodeType === 8;
40 39
41 40 export const isDocumentNode = (el: unknown): el is Document => isNode(el) && el.nodeType === 9;
42 41
43 42 export const isDocumentTypeNode = (el: unknown): el is DocumentType => isNode(el) && el.nodeType === 10;
44 43
45 44 export const isDocumentFragmentNode = (el: any): el is DocumentFragment => isNode(el) && el.nodeType === 11;
46 45
47 46 export const isWidget = (v: unknown): v is _WidgetBase => !!(v && "domNode" in (v as _WidgetBase));
48 47
49 48 export const isRendition = (v: unknown): v is Rendition => !!(v && typeof (v as Rendition).getDomNode === "function");
50 49
51 50 /**
52 51 * @deprecated use isRendition
53 52 */
54 53 export const isBuildContext = isRendition;
55 54
56 55 export const isPlainObject = (v: object) => {
57 56 if (typeof v !== "object")
58 57 return false;
59 58
60 59 const vp = Object.getPrototypeOf(v);
61 60 return !vp || vp === Object.prototype;
62 61 }
63 62
64 63 export const isWidgetConstructor = (v: unknown): v is _WidgetBaseConstructor =>
65 64 typeof v === "function" && v.prototype && (
66 65 "domNode" in v.prototype ||
67 66 "buildRendering" in v.prototype
68 67 );
69 68
70 69
71 70 /** Tests whether the specified node is placed in visible dom.
72 71 * @param {Node} node The node to test
73 72 */
74 73 export const isInPage = (node: Node) => node === document.body ? false : document.body.contains(node);
75 74
76 75 export const isRecursivelyDestroyable = (target: unknown): target is IRecursivelyDestroyable =>
77 76 !!(target && typeof (target as IRecursivelyDestroyable).destroyRecursive === "function");
78 77
79 78
80 79
81 80 /** Destroys DOM Node with all contained widgets.
82 81 * If the specified node is the root node of a widget, then the
83 82 * widget will be destroyed.
84 83 *
85 84 * @param target DOM Node or widget to destroy
86 85 */
87 86 export const destroy = (target: Node | IDestroyable | IRecursivelyDestroyable) => {
88 87 if (isRecursivelyDestroyable(target)) {
89 88 target.destroyRecursive();
90 89 } else if (isDestroyable(target)) {
91 90 target.destroy();
92 91 } else if (isNode(target)) {
93 92 if (isElementNode(target)) {
94 93 const w = registry.byNode(target);
95 94 if (w) {
96 95 w.destroyRecursive();
97 96 } else {
98 97 emptyNode(target);
99 98 const parent = target.parentNode;
100 99 if (parent)
101 100 parent.removeChild(target);
102 101 }
103 102 }
104 103 }
105 104 }
106 105
107 106 /** Empties a content of the specified node and destroys all contained widgets.
108 107 *
109 108 * @param target DOM node to empty.
110 109 */
111 110 export const emptyNode = (target: Node) => {
112 111 registry.findWidgets(target).forEach(destroy);
113 112
114 for(let c; c = target.lastChild;){ // intentional assignment
113 for (let c; c = target.lastChild;) { // intentional assignment
115 114 target.removeChild(c);
116 115 }
117 116 }
118 117
119 118 /** This function starts all widgets inside the DOM node if the target is a node
120 119 * or starts widget itself if the target is the widget. If the specified node
121 120 * associated with the widget that widget will be started.
122 121 *
123 122 * @param target DOM node to find and start widgets or the widget itself.
124 123 */
125 124 export const startupWidgets = (target: Node | _WidgetBase, skipNode?: Node) => {
126 125 if (isNode(target)) {
127 126 if (isElementNode(target)) {
128 127 const w = registry.byNode(target);
129 128 if (w) {
130 129 if (w.startup)
131 130 w.startup();
132 131 } else {
133 132 registry.findWidgets(target, skipNode).forEach(x => x.startup());
134 133 }
135 134 }
136 135 } else {
137 136 if (target.startup)
138 137 target.startup();
139 138 }
140 139 }
141 140
142 141 /** Places the specified DOM node at the specified location.
143 142 *
144 143 * @param node The node which should be placed
145 144 * @param refNodeOrId The reference node where the created
146 145 * DOM should be placed.
147 146 * @param position Optional parameter, specifies the
148 147 * position relative to refNode. Default is "last" (i.e. last child).
149 148 */
150 149 export const placeAt = (node: Node, refNodeOrId: string | Node, position: DojoNodePosition = "last") => {
151 150 const ref = typeof refNodeOrId == "string" ? document.getElementById(refNodeOrId) : refNodeOrId;
152 151 if (!ref)
153 152 return;
154 153
155 154 const parent = ref.parentNode;
156 155
157 156 if (typeof position == "number") {
158 157 if (ref.childNodes.length <= position) {
159 158 ref.appendChild(node);
160 159 } else {
161 160 ref.insertBefore(node, ref.childNodes[position]);
162 161 }
163 162 } else {
164 163 switch (position) {
165 164 case "before":
166 165 parent && parent.insertBefore(node, ref);
167 166 break;
168 167 case "after":
169 168 parent && parent.insertBefore(node, ref.nextSibling);
170 169 break;
171 170 case "first":
172 ref.insertBefore(node,ref.firstChild);
171 ref.insertBefore(node, ref.firstChild);
173 172 break;
174 173 case "last":
175 174 ref.appendChild(node);
176 175 break;
177 176 case "only":
178 177 emptyNode(ref);
179 178 ref.appendChild(node);
180 179 break;
181 180 case "replace":
182 181 if (parent)
183 182 parent.replaceChild(node, ref);
184 183 destroy(ref);
185 184 break;
186 185 }
187 186 }
188 187 }
189 188
190 189 /** Collects nodes from collection to an array.
191 190 *
192 191 * @param collection The collection of nodes.
193 192 * @returns The array of nodes.
194 193 */
195 194 export const collectNodes = (collection: HTMLCollection) => {
196 195 const items = [];
197 196 for (let i = 0, n = collection.length; i < n; i++) {
198 197 items.push(collection[i]);
199 198 }
200 199 return items;
201 200 };
202 201
203 202 /** Starts widgets if the node contained in the document or in the started widget.
204 203 *
205 204 * @param node The node to start.
206 205 */
207 206 export const autostartWidgets = (node: Node) => {
208 207 if (node.parentNode) {
209 208 const parentWidget = registry.getEnclosingWidget(node.parentNode);
210 209 if (parentWidget && parentWidget._started)
211 210 return startupWidgets(node);
212 211 }
213 212 if (isInPage(node))
214 213 startupWidgets(node);
215 214 }; No newline at end of file
@@ -1,80 +1,81
1 1 /// <reference path="./css-plugin.d.ts"/>
2 2
3 3 declare namespace JSX {
4 4
5 5 type Ref<T> = (value: T) => 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 ref: (value: T) => void;
78 ref?: (value: T) => void;
79 children?: unknown;
79 80 }
80 81 }
@@ -1,108 +1,123
1 1 plugins {
2 2 id "org.implab.gradle-typescript" version "1.3.4"
3 3 id "ivy-publish"
4 4 }
5 5
6 def container = "djx-playground"
7
6 8 configurations {
7 9 npmLocal
8 10 }
9 11
10 12 dependencies {
11 13 npmLocal project(":djx")
12 14 }
13 15
14 16 def bundleDir = fileTree(layout.buildDirectory.dir("bundle")) {
15 17 builtBy "bundle"
16 18 }
17 19
18 20 typescript {
19 21 compilerOptions {
20 22 lib = ["es5", "dom", "scripthost", "es2015.promise", "es2015.symbol", "es2015.iterable"]
21 23 // listFiles = true
22 24 strict = true
23 25 types = ["requirejs", "@implab/dojo-typings", "@implab/djx"]
24 26 module = "amd"
25 27 it.target = "es5"
26 28 experimentalDecorators = true
27 29 noUnusedLocals = false
28 30 jsx = "react"
29 31 jsxFactory = "createElement"
30 32 moduleResolution = "node"
31 33 // dojo-typings are sick
32 34 skipLibCheck = true
33 35 // traceResolution = true
34 36 // baseUrl = "./"
35 37 // paths = [ "*": [ "$projectDir/src/typings/*" ] ]
36 38 // baseUrl = "$projectDir/src/typings"
37 39 // typeRoots = ["$projectDir/src/typings"]
38 40 }
39 41 tscCmd = "$projectDir/node_modules/.bin/tsc"
40 42 tsLintCmd = "$projectDir/node_modules/.bin/tslint"
41 43 esLintCmd = "$projectDir/node_modules/.bin/eslint"
42 44 }
43 45
44 46 tasks.matching{ it.name =~ /^configureTs/ }.configureEach {
45 47 compilerOptions {
46 48 if (symbols != 'none') {
47 49 sourceMap = true
48 50 switch(symbols) {
49 51 case "local":
50 52 sourceRoot = ( isWindows ? "file:///" : "file://" ) + it.rootDir
51 53 break;
52 54 }
53 55 }
54 56 }
55 57 }
56 58
57 59 npmInstall {
58 60 //npmInstall.dependsOn it
59 61 dependsOn configurations.npmLocal
60 62
61 63 doFirst {
62 64 configurations.npmLocal.each { f ->
63 65 exec {
64 66 commandLine "npm", "install", f, "--save-dev"
65 67 }
66 68 }
67 69 }
68 70 }
69 71
70 72 clean {
71 73 doFirst {
72 74 delete "$buildDir/bundle"
73 75 }
74 76 }
75 77
76 78
77 79 task processResourcesBundle(type: Copy) {
78 80 from "src/bundle"
79 81 into layout.buildDirectory.dir("bundle")
80 82 }
81 83
82 84 task copyModules(type: Copy) {
83 85 dependsOn npmInstall
84 86 into layout.buildDirectory.dir("bundle/js");
85 87
86 88 def pack = { String jsmod ->
87 89 into(jsmod) {
88 90 from npm.module(jsmod)
89 91 }
90 92 }
91 93
92 94
93 95 pack("@implab/djx")
94 96 pack("@implab/core-amd")
95 97 pack("dojo")
96 98 pack("dijit")
97 99 from npm.module("requirejs/require.js")
98 100 }
99 101
100 102 task copyApp(type: Copy) {
101 103 dependsOn assemble
102 104 from typescript.assemblyDir
103 105 into layout.buildDirectory.dir("bundle/js/app")
104 106 }
105 107
106 108 task bundle {
107 109 dependsOn copyModules, processResourcesBundle, copyApp
110 }
111
112 task up(type: Exec) {
113 dependsOn bundle
114 commandLine "podman", "run", "--rm", "-d",
115 "--name", container,
116 "-p", "2078:80",
117 "-v", "$buildDir/bundle:/srv/www/htdocs",
118 "registry.implab.org/implab/apache2:latest"
119 }
120
121 task stop(type: Exec) {
122 commandLine "podman", "stop", container
108 123 } No newline at end of file
@@ -1,21 +1,49
1 1 import { djbase, djclass } from "@implab/djx/declare";
2 2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 import { createElement } from "@implab/djx/tsx";
3 import { createElement, watch } from "@implab/djx/tsx";
4 4 import ProgressBar from "./ProgressBar";
5 import Button = require("dijit/form/Button");
5 6
6 7 const ref = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
7 8
8 9 @djclass
9 10 export default class MainWidget extends djbase(DjxWidgetBase) {
10 11
11 12 titleNode?: HTMLHeadingElement;
12 13
13 14 progressBar?: ProgressBar;
14 15
16 count = 0;
17
18 showCounter = false;
19
15 20 render() {
16 return <div>
21 return <div className="tundra">
17 22 <h2 ref={ref(this, "titleNode")}>Hi!</h2>
18 <ProgressBar ref={ref(this, "progressBar")}/>
23 <ProgressBar ref={ref(this, "progressBar")} />
24 {watch(this, "showCounter", flag => flag &&
25 <section style={{padding: "10px"}}>
26 <label>
27 Counter: {watch(this, "count", v => [<span>{v}</span>, " ", <span>sec</span>])}
28 </label>
29 </section>
30 )}
31 <Button onClick={this._onToggleCounterClick}>Toggle counter</Button>
19 32 </div>;
20 33 }
34
35 postCreate(): void {
36 super.postCreate();
37
38 const inc = () => {
39 this.set("count", this.count + 1);
40 this.defer(inc, 1000);
41 }
42
43 inc();
44 }
45
46 private _onToggleCounterClick = () => {
47 this.set("showCounter", !this.showCounter);
48 }
21 49 }
@@ -1,4 +1,7
1 1 import MainWidget from "./MainWidget";
2 import "@implab/djx/css!dojo/resources/dojo.css"
3 import "@implab/djx/css!dijit/themes/dijit.css"
4 import "@implab/djx/css!dijit/themes/tundra/tundra.css"
2 5
3 6 const w = new MainWidget();
4 7 w.placeAt(document.body); No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now