diff --git a/djx/gradle.properties b/djx/gradle.properties --- a/djx/gradle.properties +++ b/djx/gradle.properties @@ -5,4 +5,3 @@ description=Create HyperText with Typesc license=BSD-2-Clause repository=http://hg.code.sf.net/p/implabjs/djx npmScope=implab -symbols=pack \ No newline at end of file diff --git a/djx/src/main/ts/observable.ts b/djx/src/main/ts/observable.ts --- a/djx/src/main/ts/observable.ts +++ b/djx/src/main/ts/observable.ts @@ -21,9 +21,9 @@ const sink = (consumer: Consumer) let done = false; return { - next: (value: T) => done && next(value), - error: (e: unknown) => done && (done = true, error(e)), - complete: () => done && (done = true, complete()) + next: (value: T) => !done && next(value), + error: (e: unknown) => !done && (done = true, error(e)), + complete: () => !done && (done = true, complete()) }; } diff --git a/djx/src/main/ts/tsx.ts b/djx/src/main/ts/tsx.ts --- a/djx/src/main/ts/tsx.ts +++ b/djx/src/main/ts/tsx.ts @@ -78,6 +78,7 @@ export function watch[K]) => any ) { + return new WatchRendition( render, observe(({next}) => { @@ -85,6 +86,7 @@ export function watch oldValue !== newValue && next(newValue) ); + next(target.get(prop)); return () => h.remove(); }) ) diff --git a/djx/src/main/ts/tsx/FunctionRendition.ts b/djx/src/main/ts/tsx/FunctionRendition.ts --- a/djx/src/main/ts/tsx/FunctionRendition.ts +++ b/djx/src/main/ts/tsx/FunctionRendition.ts @@ -1,7 +1,6 @@ import { argumentNotNull } from "@implab/core-amd/safe"; import { getItemDom } from "./render"; import { RenditionBase } from "./RenditionBase"; -import { IScope } from "./Scope"; export class FunctionRendition extends RenditionBase { private _component: (...args: any[]) => any; @@ -15,13 +14,11 @@ export class FunctionRendition extends R this._component = component; } - protected _create(attrs: object, children: any[], scope: IScope) { + protected _create(attrs: object, children: any[]) { const _attrs: any = attrs || {}; - const _children = children.map(x => getItemDom(x, scope)); + const _children = children.map(x => getItemDom(x)); this._node = getItemDom( - this._component.call(null, { ..._attrs, children: _children }), - scope - ); + this._component.call(null, { ..._attrs, children: _children })); } protected _getDomNode() { diff --git a/djx/src/main/ts/tsx/HtmlRendition.ts b/djx/src/main/ts/tsx/HtmlRendition.ts --- a/djx/src/main/ts/tsx/HtmlRendition.ts +++ b/djx/src/main/ts/tsx/HtmlRendition.ts @@ -1,14 +1,15 @@ -import dom = require("dojo/dom-construct"); +import djDom = require("dojo/dom-construct"); +import djAttr = require("dojo/dom-attr"); import { argumentNotEmptyString } from "@implab/core-amd/safe"; import { RenditionBase } from "./RenditionBase"; import { placeAt } from "./traits"; import { IScope } from "./Scope"; -import { getItemDom } from "./render"; +import { getItemDom, renderHook } from "./render"; -export class HtmlRendition extends RenditionBase { +export class HtmlRendition extends RenditionBase { elementType: string; - _element: HTMLElement | undefined; + _element: Element | undefined; constructor(elementType: string) { argumentNotEmptyString(elementType, "elementType"); @@ -20,13 +21,24 @@ export class HtmlRendition extends Rendi _addChild(child: unknown, scope: IScope): void { if (!this._element) throw new Error("The HTML element isn't created"); - placeAt(getItemDom(child, scope), this._element); + placeAt(getItemDom(child), this._element); } - _create(attrs: object, children: unknown[], scope: IScope) { - this._element = dom.create(this.elementType, attrs); + _create({ xmlns, ref, ...attrs }: { xmlns?: string, ref?: JSX.Ref }, children: unknown[], scope: IScope) { + + if (xmlns) { + this._element = document.createElementNS(xmlns, this.elementType); + djAttr.set(this._element, attrs); + } else { + this._element = djDom.create(this.elementType, attrs); + } children.forEach(v => this._addChild(v, scope)); + + const element = this._element; + + if (ref) + renderHook(() => ref(element)); } _getDomNode() { diff --git a/djx/src/main/ts/tsx/WatchRendition.ts b/djx/src/main/ts/tsx/WatchRendition.ts --- a/djx/src/main/ts/tsx/WatchRendition.ts +++ b/djx/src/main/ts/tsx/WatchRendition.ts @@ -1,10 +1,11 @@ import { id as mid } from "module"; import { TraceSource } from "@implab/core-amd/log/TraceSource"; import { argumentNotNull } from "@implab/core-amd/safe"; -import { getItemDom, render } from "./render"; +import { getScope, render } from "./render"; import { RenditionBase } from "./RenditionBase"; -import { IScope, Scope } from "./Scope"; +import { Scope } from "./Scope"; import { Observable } from "../observable"; +import { destroy } from "./traits"; const trace = TraceSource.get(mid); @@ -28,7 +29,8 @@ export class WatchRendition extends R this._node = document.createComment("WatchRendition placeholder"); } - protected _create(attrs: object, children: any[], scope: IScope) { + protected _create(attrs: object, children: any[]) { + const scope = getScope(); scope.own(this._scope); scope.own(this._subject.on({ next: this._onValue })); } @@ -37,12 +39,10 @@ export class WatchRendition extends R void this._render(value).catch( e => trace.error(e)); private async _render(value: T) { - const prevNode = this._node; this._scope.clean(); - - this._node = await render(this._component(value), this._scope); - - this.placeAt(prevNode, "replace"); + const [refNode, ...rest] = await render(this._component(value), this._node, "replace", this._scope); + this._node = refNode; + this._scope.own(() => rest.forEach(destroy)); } protected _getDomNode() { diff --git a/djx/src/main/ts/tsx/WidgetRendition.ts b/djx/src/main/ts/tsx/WidgetRendition.ts --- a/djx/src/main/ts/tsx/WidgetRendition.ts +++ b/djx/src/main/ts/tsx/WidgetRendition.ts @@ -4,7 +4,7 @@ import { DojoNodePosition, isElementNode import registry = require("dijit/registry"); import ContentPane = require("dijit/layout/ContentPane"); import { IScope } from "./Scope"; -import { getItemDom, getScope } from "./render"; +import { getItemDom, getScope, renderHook } from "./render"; // tslint:disable-next-line: class-name export interface _Widget { @@ -15,10 +15,10 @@ export interface _Widget { placeAt?(refNode: string | Node, position?: DojoNodePosition): void; startup?(): void; - addChild?(widget: any, index?: number): void; + addChild?(widget: unknown, index?: number): void; } -export type _WidgetCtor = new (attrs: any, srcNode?: string | Node) => _Widget; +export type _WidgetCtor = new (attrs: {}, srcNode?: string | Node) => _Widget; export class WidgetRendition extends RenditionBase { readonly widgetClass: _WidgetCtor; @@ -32,7 +32,7 @@ export class WidgetRendition extends Ren this.widgetClass = widgetClass; } - _addChild(child: any, scope: IScope): void { + _addChild(child: unknown, scope: IScope): void { const instance = this._getInstance(); if (instance.addChild) { @@ -42,7 +42,7 @@ export class WidgetRendition extends Ren } else if (isWidget(child)) { instance.addChild(child); } else { - const childDom = getItemDom(child, scope); + const childDom = getItemDom(child); const w = isElementNode(childDom) ? registry.byNode(childDom) : undefined; if (w) { @@ -52,7 +52,7 @@ export class WidgetRendition extends Ren throw new Error("Failed to add DOM content. The widget doesn't have a containerNode"); // the current widget isn't started, it's children shouldn't start too - placeAt(getItemDom(child,scope), instance.containerNode, "last"); + placeAt(getItemDom(child), instance.containerNode, "last"); } } } else { @@ -60,20 +60,20 @@ export class WidgetRendition extends Ren throw new Error("The widget doesn't have neither addChild nor containerNode"); // the current widget isn't started, it's children shouldn't start too - placeAt(getItemDom(child, scope), instance.containerNode, "last"); + placeAt(getItemDom(child), instance.containerNode, "last"); } } - protected _create(attrs: any, children: any[], scope: IScope) { + protected _create({ref, ...attrs}: {ref?: JSX.Ref<_Widget>}, children: unknown[], scope: IScope) { if (this.widgetClass.prototype instanceof ContentPane) { // a special case for the ContentPane this is for - // the compatibility with this heavy widget, all + // compatibility with that heavy widget, all // regular containers could be easily manipulated // through `containerNode` property or `addChild` method. // render children to the DocumentFragment const content = document.createDocumentFragment(); - children.forEach(child => content.appendChild(getItemDom(child, scope))); + children.forEach(child => content.appendChild(getItemDom(child))); // set the content property to the parameters of the widget const _attrs = { ...attrs, content }; @@ -83,6 +83,11 @@ export class WidgetRendition extends Ren children.forEach(x => this._addChild(x, scope)); } + if (ref) { + const instance = this._instance; + renderHook(() => ref(instance)); + } + } private _getInstance() { diff --git a/djx/src/main/ts/tsx/render.ts b/djx/src/main/ts/tsx/render.ts --- a/djx/src/main/ts/tsx/render.ts +++ b/djx/src/main/ts/tsx/render.ts @@ -1,12 +1,53 @@ -import { IScope, Scope } from "./Scope"; -import { destroy, isNode, isRendition, isWidget, Rendition } from "./traits"; +import { TraceSource } from "@implab/core-amd/log/TraceSource"; +import { isPromise } from "@implab/core-amd/safe"; +import { id as mid } from "module"; +import { Scope } from "./Scope"; +import { autostartWidgets, collectNodes, DojoNodePosition, isDocumentFragmentNode, isNode, isRendition, isWidget, placeAt } from "./traits"; + +const trace = TraceSource.get(mid); let _scope = Scope.dummy; -const beginRender = async () => { +let renderCount = 0; + +const hooks: (() => void)[] = []; + +const guard = (cb: () => unknown) => { + try { + const result = cb() + if (isPromise(result)) { + const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret); + result.then(warn, warn); + } + } catch (e) { + trace.error(e); + } } +/** + * Schedules rendering micro task + * @returns Promise + */ +const beginRender = () => { + renderCount++; + return Promise.resolve(); +} + +/** + * Completes render operation + */ const endRender = () => { + if (!--renderCount) { + hooks.forEach(guard); + hooks.length = 0; + } +} + +export const renderHook = (hook: () => void) => { + if (renderCount) + hooks.push(hook); + else + guard(hook); } /** Returns the current scope */ @@ -16,14 +57,17 @@ export const getScope = () => _scope; * @param rendition The rendition to be rendered * @param scope The scope */ -export const render = async (rendition: unknown, scope = Scope.dummy) => { +export const render = async (rendition: unknown, refNode: Node, position: DojoNodePosition = "last", scope = Scope.dummy) => { await beginRender(); const prev = _scope; _scope = scope; try { - const node = getItemDom(rendition, scope); - scope.own(() => destroy(node)); - return node; + const domNode = getItemDom(rendition); + const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.children) : [domNode]; + placeAt(domNode, refNode, position); + startupPending.forEach(autostartWidgets); + + return startupPending; } finally { _scope = prev; endRender(); @@ -31,7 +75,7 @@ export const render = async (rendition: } /** Renders DOM element for different types of the argument. */ -export const getItemDom = (v: unknown, scope: IScope) => { +export const getItemDom = (v: unknown) => { if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) { // primitive types converted to the text nodes return document.createTextNode(v.toString()); @@ -40,7 +84,7 @@ export const getItemDom = (v: unknown, s return v; } else if (isRendition(v)) { // renditions are instantiated - return v.getDomNode(scope); + return v.getDomNode(); } else if (isWidget(v)) { // widgets are converted to it's markup return v.domNode; @@ -50,7 +94,7 @@ export const getItemDom = (v: unknown, s } else if (v instanceof Array) { // arrays will be translated to document fragments const fragment = document.createDocumentFragment(); - v.map(item => getItemDom(item, scope)) + v.map(item => getItemDom(item)) .forEach(node => fragment.appendChild(node)); return fragment; } else { diff --git a/djx/src/main/ts/tsx/traits.ts b/djx/src/main/ts/tsx/traits.ts --- a/djx/src/main/ts/tsx/traits.ts +++ b/djx/src/main/ts/tsx/traits.ts @@ -2,7 +2,6 @@ import { IDestroyable } from "@implab/co import { isDestroyable } from "@implab/core-amd/safe"; import _WidgetBase = require("dijit/_WidgetBase"); import registry = require("dijit/registry"); -import { IScope } from "./Scope"; interface _WidgetBaseConstructor { new (params?: Partial<_WidgetBase & A>, srcNodeRef?: dojo.NodeOrString): _WidgetBase & dojo._base.DeclareCreatedObject; @@ -14,7 +13,7 @@ export type DojoNodePosition = "first" | export type DojoNodeLocation = [Node, DojoNodePosition]; export interface Rendition { - getDomNode(scope?: IScope): TNode; + getDomNode(): TNode; placeAt(refNode: string | Node, position?: DojoNodePosition): void; } @@ -111,7 +110,7 @@ export const destroy = (target: Node | I export const emptyNode = (target: Node) => { registry.findWidgets(target).forEach(destroy); - for(let c; c = target.lastChild;){ // intentional assignment + for (let c; c = target.lastChild;) { // intentional assignment target.removeChild(c); } } @@ -169,7 +168,7 @@ export const placeAt = (node: Node, refN parent && parent.insertBefore(node, ref.nextSibling); break; case "first": - ref.insertBefore(node,ref.firstChild); + ref.insertBefore(node, ref.firstChild); break; case "last": ref.appendChild(node); diff --git a/djx/src/main/typings/index.d.ts b/djx/src/main/typings/index.d.ts --- a/djx/src/main/typings/index.d.ts +++ b/djx/src/main/typings/index.d.ts @@ -75,6 +75,7 @@ declare namespace JSX { } interface IntrinsicClassAttributes { - ref: (value: T) => void; + ref?: (value: T) => void; + children?: unknown; } } diff --git a/gradle.properties b/gradle.properties new file mode 100644 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,1 @@ +symbols=local \ No newline at end of file diff --git a/playground/build.gradle b/playground/build.gradle --- a/playground/build.gradle +++ b/playground/build.gradle @@ -3,6 +3,8 @@ plugins { id "ivy-publish" } +def container = "djx-playground" + configurations { npmLocal } @@ -105,4 +107,17 @@ task copyApp(type: Copy) { task bundle { dependsOn copyModules, processResourcesBundle, copyApp +} + +task up(type: Exec) { + dependsOn bundle + commandLine "podman", "run", "--rm", "-d", + "--name", container, + "-p", "2078:80", + "-v", "$buildDir/bundle:/srv/www/htdocs", + "registry.implab.org/implab/apache2:latest" +} + +task stop(type: Exec) { + commandLine "podman", "stop", container } \ No newline at end of file diff --git a/playground/src/main/ts/MainWidget.tsx b/playground/src/main/ts/MainWidget.tsx --- a/playground/src/main/ts/MainWidget.tsx +++ b/playground/src/main/ts/MainWidget.tsx @@ -1,7 +1,8 @@ import { djbase, djclass } from "@implab/djx/declare"; import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase"; -import { createElement } from "@implab/djx/tsx"; +import { createElement, watch } from "@implab/djx/tsx"; import ProgressBar from "./ProgressBar"; +import Button = require("dijit/form/Button"); const ref = (target: W, name: K) => (v: W[K]) => target.set(name, v); @@ -12,10 +13,37 @@ export default class MainWidget extends progressBar?: ProgressBar; + count = 0; + + showCounter = false; + render() { - return
+ return

Hi!

- + + {watch(this, "showCounter", flag => flag && +
+ +
+ )} +
; } + + postCreate(): void { + super.postCreate(); + + const inc = () => { + this.set("count", this.count + 1); + this.defer(inc, 1000); + } + + inc(); + } + + private _onToggleCounterClick = () => { + this.set("showCounter", !this.showCounter); + } } diff --git a/playground/src/main/ts/main.ts b/playground/src/main/ts/main.ts --- a/playground/src/main/ts/main.ts +++ b/playground/src/main/ts/main.ts @@ -1,4 +1,7 @@ import MainWidget from "./MainWidget"; +import "@implab/djx/css!dojo/resources/dojo.css" +import "@implab/djx/css!dijit/themes/dijit.css" +import "@implab/djx/css!dijit/themes/tundra/tundra.css" const w = new MainWidget(); w.placeAt(document.body); \ No newline at end of file