diff --git a/package-lock.json b/package-lock.json --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ }, "peerDependencies": { "@implab/core-amd": "^1.4.0", - "dojo": "1.16.0" + "dojo": "^1.10.0" } }, "node_modules/@babel/code-frame": { diff --git a/src/main/ts/tsx.ts b/src/main/ts/tsx.ts --- a/src/main/ts/tsx.ts +++ b/src/main/ts/tsx.ts @@ -1,24 +1,24 @@ import { Constructor } from "@implab/core-amd/interfaces"; -import { HtmlElementContext } from "./tsx/HtmlElementContext"; -import { WidgetContext } from "./tsx/WidgetContext"; -import { isWidgetConstructor, BuildContext } from "./tsx/traits"; -import { FunctionComponentContext } from "./tsx/FunctionComponentContext"; +import { HtmlRendition } from "./tsx/HtmlRendition"; +import { WidgetRendition } from "./tsx/WidgetRendition"; +import { isWidgetConstructor, Rendition } from "./tsx/traits"; +import { FunctionRendition } from "./tsx/FunctionRendition"; -export function createElement Element)>(elementType: T, ...args: any[]): BuildContext { +export function createElement Element)>(elementType: T, ...args: any[]): Rendition { if (typeof elementType === "string") { - const ctx = new HtmlElementContext(elementType); + const ctx = new HtmlRendition(elementType); if (args) args.forEach(x => ctx.visitNext(x)); return ctx; } else if (isWidgetConstructor(elementType)) { - const ctx = new WidgetContext(elementType); + const ctx = new WidgetRendition(elementType); if (args) args.forEach(x => ctx.visitNext(x)); return ctx; } else if (typeof elementType === "function") { - const ctx = new FunctionComponentContext(elementType as (props: any) => Element); + const ctx = new FunctionRendition(elementType as (props: any) => Element); if (args) args.forEach(x => ctx.visitNext(x)); diff --git a/src/main/ts/tsx/BuildContextBase.ts b/src/main/ts/tsx/BuildContextBase.ts --- a/src/main/ts/tsx/BuildContextBase.ts +++ b/src/main/ts/tsx/BuildContextBase.ts @@ -1,93 +1,11 @@ -import { isNull, mixin } from "@implab/core-amd/safe"; -import { isPlainObject, isNode, isBuildContext, DojoNodePosition, BuildContext, isInPage, isWidget } from "./traits"; - -import dom = require("dojo/dom-construct"); -import registry = require("dijit/registry"); - - -export abstract class BuildContextBase implements BuildContext { - private _attrs = {}; - - private _children = new Array(); - - private _created: boolean = false; - - visitNext(v: any) { - if (this._created) - throw new Error("The Element is already created"); - - if (isNull(v) || typeof v === "boolean") - // skip null, undefined, booleans ( this will work: {value && {value}} ) - return; - - if (isPlainObject(v)) { - mixin(this._attrs, v); - } else if (v instanceof Array) { - v.forEach(x => this.visitNext(x)); - } else { - this._children.push(v); - } - } +import { RenditionBase } from "./RenditionBase"; - protected getItemDom(v: any) { - const tv = typeof v; - if (tv === "string" || tv === "number" || v instanceof RegExp || v instanceof Date) { - return document.createTextNode(v.toString()); - } else if (isNode(v)) { - return v; - } else if (isBuildContext(v)) { - return v.getDomNode(); - } else if(isWidget(v)) { - return v.domNode; - } else if(tv === "boolean" || v === null || v === undefined) { - return document.createComment(`[${tv} ${String(v)}]`); - } else { - throw new Error("Invalid parameter"); - } - } - - ensureCreated() { - if (!this._created) { - this._create(this._attrs, this._children); - this._children = []; - this._attrs = {}; - this._created = true; - } - } - - /** @deprecated will be removed in 1.0.0, use getDomNode() */ - getDomElement() { - return this.getDomNode(); - } +/** + * @deprecated use RenditionBase instead + */ + export type BuildContextBase = RenditionBase; - /** Creates DOM node if not created. No additional actions are taken. */ - getDomNode() { - this.ensureCreated(); - return this._getDomNode(); - } - - /** Creates DOM node if not created, places it to the specified position - * and calls startup() method for all widgets contained by this node. - * - * @param {string | Node} refNode The reference node where the created - * DOM should be placed. - * @param {DojoNodePosition} position Optional parameter, specifies the - * position relative to refNode. Default is "last" (i.e. last child). - */ - placeAt(refNode: string | Node, position?: DojoNodePosition) { - const domNode = this.getDomNode(); - dom.place(domNode, refNode, position); - const parentWidget = domNode.parentNode ? registry.getEnclosingWidget(domNode.parentNode) : null; - - if ((parentWidget && parentWidget._started) || isInPage(domNode)) - this._startup(); - } - - _startup () { - registry.findWidgets(this._getDomNode()).forEach(w => w.startup()); - } - - abstract _create(attrs: object, children: any[]): void; - - abstract _getDomNode(): TNode; -} + /** + * @deprecated use RenditionBase instead + */ + export const BuildContextBase = RenditionBase; diff --git a/src/main/ts/tsx/DjxFragment.ts b/src/main/ts/tsx/DjxFragment.ts --- a/src/main/ts/tsx/DjxFragment.ts +++ b/src/main/ts/tsx/DjxFragment.ts @@ -1,6 +1,7 @@ /** Special functional component used to create a document fragment */ -export function DjxFragment({children}: {children: Node[]}){ +export function DjxFragment({children}: {children?: Node | Node[]}){ const fragment = document.createDocumentFragment(); if (children) - children.forEach(child => fragment.appendChild(child)); + (children instanceof Array ? children : [children]).forEach(child => fragment.appendChild(child)); + return fragment; } \ No newline at end of file diff --git a/src/main/ts/tsx/DjxWidgetBase.ts b/src/main/ts/tsx/DjxWidgetBase.ts --- a/src/main/ts/tsx/DjxWidgetBase.ts +++ b/src/main/ts/tsx/DjxWidgetBase.ts @@ -1,7 +1,7 @@ import { djbase, djclass } from "../declare"; import _WidgetBase = require("dijit/_WidgetBase"); import _AttachMixin = require("dijit/_AttachMixin"); -import { BuildContext, isNode, startupWidgets } from "./traits"; +import { Rendition, isNode, startupWidgets } from "./traits"; import registry = require("dijit/registry"); // type Handle = dojo.Handle; @@ -42,7 +42,7 @@ export abstract class DjxWidgetBase; + abstract render(): Rendition; _processTemplateNode( baseNode: T, @@ -82,7 +82,7 @@ export abstract class DjxWidgetBase w.startup()); super.startup(); } } diff --git a/src/main/ts/tsx/FunctionComponentContext.ts b/src/main/ts/tsx/FunctionComponentContext.ts --- a/src/main/ts/tsx/FunctionComponentContext.ts +++ b/src/main/ts/tsx/FunctionComponentContext.ts @@ -1,33 +1,11 @@ -import dom = require("dojo/dom-construct"); -import attr = require("dojo/dom-attr"); -import { argumentNotNull } from "@implab/core-amd/safe"; -import { BuildContextBase } from "./BuildContextBase"; -import registry = require("dijit/registry"); - - -export class FunctionComponentContext extends BuildContextBase { - private _component: (props: any) => any; - - private _node: Node | undefined; - - constructor(component: (props: any) => any) { - super(); - argumentNotNull(component, "component"); +import { FunctionRendition } from "./FunctionRendition"; - this._component = component; - } - - _create(attrs: object, children: any[]) { - const _attrs: any = attrs || {}; - _attrs.children = children.map(x => this.getItemDom(x)); +/** + * @deprecated use FunctionRendition + */ +export type FunctionComponentContext = FunctionRendition; - this._node = this.getItemDom(this._component.call(null, _attrs)); - } - - _getDomNode() { - if (!this._node) - throw new Error("The instance of the widget isn't created"); - return this._node; - } - -} +/** + * @deprecated use FunctionRendition + */ +export const FunctionComponentContext = FunctionRendition; diff --git a/src/main/ts/tsx/FunctionRendition.ts b/src/main/ts/tsx/FunctionRendition.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/tsx/FunctionRendition.ts @@ -0,0 +1,29 @@ +import { argumentNotNull } from "@implab/core-amd/safe"; +import { RenditionBase } from "./RenditionBase"; + +export class FunctionRendition extends RenditionBase { + private _component: (props: any) => any; + + private _node: Node | undefined; + + constructor(component: (props: any) => any) { + super(); + argumentNotNull(component, "component"); + + this._component = component; + } + + _create(attrs: object, children: any[]) { + const _attrs: any = attrs || {}; + _attrs.children = children.map(x => this.getItemDom(x)); + + this._node = this.getItemDom(this._component.call(null, _attrs)); + } + + _getDomNode() { + if (!this._node) + throw new Error("The instance of the widget isn't created"); + return this._node; + } + +} diff --git a/src/main/ts/tsx/HtmlElementContext.ts b/src/main/ts/tsx/HtmlElementContext.ts --- a/src/main/ts/tsx/HtmlElementContext.ts +++ b/src/main/ts/tsx/HtmlElementContext.ts @@ -1,37 +1,11 @@ -import dom = require("dojo/dom-construct"); -import { argumentNotEmptyString } from "@implab/core-amd/safe"; -import { BuildContextBase } from "./BuildContextBase"; - -export class HtmlElementContext extends BuildContextBase { - elementType: string; - - _element: HTMLElement | undefined; - - constructor(elementType: string) { - argumentNotEmptyString(elementType, "elementType"); - super(); - - this.elementType = elementType; - } +import { HtmlRendition } from "./HtmlRendition"; - _addChild(child: any): void { - if (!this._element) - throw new Error("The HTML element isn't created"); - dom.place(this.getItemDom(child), this._element); - } - - _create(attrs: object, children: any[]) { - this._element = dom.create(this.elementType, attrs); +/** + * @deprecated use HtmlRendition + */ +export type HtmlElementContext = HtmlRendition; - if (children) - children.forEach(v => this._addChild(v)); - } - - _getDomNode() { - if (!this._element) - throw new Error("The HTML element isn't created"); - - return this._element; - } - -} +/** + * @deprecated use HtmlRendition + */ +export const HtmlElementContext = HtmlRendition; diff --git a/src/main/ts/tsx/HtmlRendition.ts b/src/main/ts/tsx/HtmlRendition.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/tsx/HtmlRendition.ts @@ -0,0 +1,37 @@ +import dom = require("dojo/dom-construct"); +import { argumentNotEmptyString } from "@implab/core-amd/safe"; +import { RenditionBase } from "./RenditionBase"; + +export class HtmlRendition extends RenditionBase { + elementType: string; + + _element: HTMLElement | undefined; + + constructor(elementType: string) { + argumentNotEmptyString(elementType, "elementType"); + super(); + + this.elementType = elementType; + } + + _addChild(child: any): void { + if (!this._element) + throw new Error("The HTML element isn't created"); + dom.place(this.getItemDom(child), this._element); + } + + _create(attrs: object, children: any[]) { + this._element = dom.create(this.elementType, attrs); + + if (children) + children.forEach(v => this._addChild(v)); + } + + _getDomNode() { + if (!this._element) + throw new Error("The HTML element isn't created"); + + return this._element; + } + +} diff --git a/src/main/ts/tsx/RenditionBase.ts b/src/main/ts/tsx/RenditionBase.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/tsx/RenditionBase.ts @@ -0,0 +1,116 @@ +import { isNull, mixin } from "@implab/core-amd/safe"; +import { isPlainObject, isNode, isRendition, DojoNodePosition, Rendition, isInPage, isWidget, isDocumentFragmentNode, startupWidgets } from "./traits"; + +import dom = require("dojo/dom-construct"); +import registry = require("dijit/registry"); + + +export abstract class RenditionBase implements Rendition { + private _attrs = {}; + + private _children = new Array(); + + private _created: boolean = false; + + visitNext(v: any) { + if (this._created) + throw new Error("The Element is already created"); + + if (isNull(v) || typeof v === "boolean") + // skip null, undefined, booleans ( this will work: {value && {value}} ) + return; + + if (isPlainObject(v)) { + mixin(this._attrs, v); + } else if (v instanceof Array) { + v.forEach(x => this.visitNext(x)); + } else { + this._children.push(v); + } + } + + protected getItemDom(v: any) { + const tv = typeof v; + + if (tv === "string" || tv === "number" || v instanceof RegExp || v instanceof Date) { + // primitive types converted to the text nodes + return document.createTextNode(v.toString()); + } else if (isNode(v)) { + // nodes are kept as is + return v; + } else if (isRendition(v)) { + // renditions as instantinated + return v.getDomNode(); + } else if (isWidget(v)) { + // widgets are converted to it's markup + return v.domNode; + } else if (tv === "boolean" || v === null || v === undefined) { + // null | undefined | boolean are removed, converted to comments + return document.createComment(`[${tv} ${String(v)}]`); + } else { + // bug: explicit error otherwise + throw new Error("Invalid parameter: " + v); + } + } + + ensureCreated() { + if (!this._created) { + this._create(this._attrs, this._children); + this._children = []; + this._attrs = {}; + this._created = true; + } + } + + /** @deprecated will be removed in 1.0.0, use getDomNode() */ + getDomElement() { + return this.getDomNode(); + } + + /** Creates DOM node if not created. No additional actions are taken. */ + getDomNode() { + this.ensureCreated(); + return this._getDomNode(); + } + + /** Creates DOM node if not created, places it to the specified position + * and calls startup() method for all widgets contained by this node. + * + * @param {string | Node} refNode The reference node where the created + * DOM should be placed. + * @param {DojoNodePosition} position Optional parameter, specifies the + * position relative to refNode. Default is "last" (i.e. last child). + */ + placeAt(refNode: string | Node, position?: DojoNodePosition) { + const domNode = this.getDomNode(); + + const collect = (collection: HTMLCollection) => { + const items = []; + for (let i = 0, n = items.length; i < length; i++) { + items.push(collection[i]); + } + return items; + } + + const startup = (node: Node) => { + if (node.parentNode) { + const parentWidget = registry.getEnclosingWidget(node.parentNode); + if (parentWidget && parentWidget._started) + return startupWidgets(node); + } + if (isInPage(node)) + startupWidgets(node); + } + + const startupPending = isDocumentFragmentNode(domNode) ? collect(domNode.children) : [domNode] + + dom.place(domNode, refNode, position); + + startupPending.forEach(startup); + + } + + protected abstract _create(attrs: object, children: any[]): void; + + protected abstract _getDomNode(): TNode; +} diff --git a/src/main/ts/tsx/WidgetContext.ts b/src/main/ts/tsx/WidgetContext.ts --- a/src/main/ts/tsx/WidgetContext.ts +++ b/src/main/ts/tsx/WidgetContext.ts @@ -1,128 +1,11 @@ -import dom = require("dojo/dom-construct"); -import { argumentNotNull } from "@implab/core-amd/safe"; -import { BuildContextBase } from "./BuildContextBase"; -import { DojoNodePosition, isInPage, isWidget } from "./traits"; -import registry = require("dijit/registry"); -import ContentPane = require("dijit/layout/ContentPane"); - -// tslint:disable-next-line: class-name -export interface _Widget { - domNode: Node; - - containerNode?: Node; - - placeAt?(refNode: string | Node, position?: DojoNodePosition): void; - startup?(): void; - - addChild?(widget: any, index?: number): void; -} - -export type _WidgetCtor = new (attrs: any, srcNode?: string | Node) => _Widget; - -export class WidgetContext extends BuildContextBase { - readonly widgetClass: _WidgetCtor; - - _instance: _Widget | undefined; - - constructor(widgetClass: _WidgetCtor) { - super(); - argumentNotNull(widgetClass, "widgetClass"); - - this.widgetClass = widgetClass; - } - - _addChild(child: any): void { - const instance = this._getInstance(); - - if (instance.addChild) { - if (child instanceof WidgetContext) { - // layout containers add custom logic to addChild methods - instance.addChild(child.getWidgetInstance()); - } else if (isWidget(child)) { - instance.addChild(child); - } else { - if (!instance.containerNode) - 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 - dom.place(this.getItemDom(child), instance.containerNode); - } - } else { - if (!instance.containerNode) - 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 - dom.place(this.getItemDom(child), instance.containerNode); - } - } +import { WidgetRendition } from "./WidgetRendition"; - _create(attrs: any, children: any[]) { - if (this.widgetClass.prototype instanceof ContentPane) { - // a special case for the ContentPane this is for - // the compatibility with this 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(this.getItemDom(child))); - - // set the content property to the parameters of the widget - const _attrs = { ...attrs, content }; - this._instance = new this.widgetClass(_attrs); - } else { - this._instance = new this.widgetClass(attrs); - children.forEach(x => this._addChild(x)); - } - - } - - private _getInstance() { - if (!this._instance) - throw new Error("The instance of the widget isn't created"); - return this._instance; - } - - _getDomNode() { - if (!this._instance) - throw new Error("The instance of the widget isn't created"); - return this._instance.domNode; - } +/** + * @deprecated use WidgetRendition + */ +export type WidgetContext = WidgetRendition; - /** Overrides default placeAt implementation. Calls placeAt of the - * widget and then starts it. - * - * @param refNode A node or id of the node where the widget should be placed. - * @param position A position relative to refNode. - */ - placeAt(refNode: string | Node, position?: DojoNodePosition) { - this.ensureCreated(); - const instance = this._getInstance(); - if (typeof instance.placeAt === "function") { - instance.placeAt(refNode, position); - - // fix the dojo startup behavior when the widget is placed - // directly to the document and doesn't have any enclosing widgets - const parentWidget = instance.domNode.parentNode ? - registry.getEnclosingWidget(instance.domNode.parentNode) : null - if (!parentWidget && isInPage(instance.domNode)) - this._startup(); - } else { - // the widget doesn't have a placeAt method, strange but whatever - super.placeAt(refNode, position); - } - } - - _startup() { - const instance = this._getInstance(); - - if (typeof instance.startup === "function") - instance.startup(); - } - - getWidgetInstance() { - this.ensureCreated(); - return this._getInstance(); - } - -} +/** + * @deprecated use WidgetRendition + */ + export const WidgetContext = WidgetRendition; diff --git a/src/main/ts/tsx/WidgetRendition.ts b/src/main/ts/tsx/WidgetRendition.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/tsx/WidgetRendition.ts @@ -0,0 +1,121 @@ +import dom = require("dojo/dom-construct"); +import { argumentNotNull } from "@implab/core-amd/safe"; +import { RenditionBase } from "./RenditionBase"; +import { DojoNodePosition, isInPage, isWidget } from "./traits"; +import registry = require("dijit/registry"); +import ContentPane = require("dijit/layout/ContentPane"); + +// tslint:disable-next-line: class-name +export interface _Widget { + domNode: Node; + + containerNode?: Node; + + placeAt?(refNode: string | Node, position?: DojoNodePosition): void; + startup?(): void; + + addChild?(widget: any, index?: number): void; +} + +export type _WidgetCtor = new (attrs: any, srcNode?: string | Node) => _Widget; + +export class WidgetRendition extends RenditionBase { + readonly widgetClass: _WidgetCtor; + + _instance: _Widget | undefined; + + constructor(widgetClass: _WidgetCtor) { + super(); + argumentNotNull(widgetClass, "widgetClass"); + + this.widgetClass = widgetClass; + } + + _addChild(child: any): void { + const instance = this._getInstance(); + + if (instance.addChild) { + if (child instanceof WidgetRendition) { + // layout containers add custom logic to addChild methods + instance.addChild(child.getWidgetInstance()); + } else if (isWidget(child)) { + instance.addChild(child); + } else { + if (!instance.containerNode) + 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 + dom.place(this.getItemDom(child), instance.containerNode); + } + } else { + if (!instance.containerNode) + 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 + dom.place(this.getItemDom(child), instance.containerNode); + } + } + + protected _create(attrs: any, children: any[]) { + if (this.widgetClass.prototype instanceof ContentPane) { + // a special case for the ContentPane this is for + // the compatibility with this 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(this.getItemDom(child))); + + // set the content property to the parameters of the widget + const _attrs = { ...attrs, content }; + this._instance = new this.widgetClass(_attrs); + } else { + this._instance = new this.widgetClass(attrs); + children.forEach(x => this._addChild(x)); + } + + } + + private _getInstance() { + if (!this._instance) + throw new Error("The instance of the widget isn't created"); + return this._instance; + } + + protected _getDomNode() { + if (!this._instance) + throw new Error("The instance of the widget isn't created"); + return this._instance.domNode; + } + + /** Overrides default placeAt implementation. Calls placeAt of the + * widget and then starts it. + * + * @param refNode A node or id of the node where the widget should be placed. + * @param position A position relative to refNode. + */ + placeAt(refNode: string | Node, position?: DojoNodePosition) { + this.ensureCreated(); + const instance = this._getInstance(); + if (typeof instance.placeAt === "function") { + instance.placeAt(refNode, position); + + // fix the dojo startup behavior when the widget is placed + // directly to the document and doesn't have any enclosing widgets + const parentWidget = instance.domNode.parentNode ? + registry.getEnclosingWidget(instance.domNode.parentNode) : null + if (!parentWidget && isInPage(instance.domNode) && typeof instance.startup === "function") + instance.startup(); + } else { + // the widget doesn't have a placeAt method, strange but whatever + super.placeAt(refNode, position); + } + } + + getWidgetInstance() { + this.ensureCreated(); + return this._getInstance(); + } + +} diff --git a/src/main/ts/tsx/traits.ts b/src/main/ts/tsx/traits.ts --- a/src/main/ts/tsx/traits.ts +++ b/src/main/ts/tsx/traits.ts @@ -8,12 +8,17 @@ type _WidgetBaseConstructor = typeof _Wi export type DojoNodePosition = "first" | "after" | "before" | "last" | "replace" | "only" | number; -export interface BuildContext { +export interface Rendition { getDomNode(): TNode; placeAt(refNode: string | Node, position?: DojoNodePosition): void; } +/** + * @deprecated use Rendition + */ +export type BuildContext = Rendition; + export interface IRecursivelyDestroyable { destroyRecursive(): void; } @@ -54,10 +59,15 @@ export function isWidget(v: any): v is _ return v && "domNode" in v; } -export function isBuildContext(v: any): v is BuildContext { +export function isRendition(v: any): v is Rendition { return typeof v === "object" && typeof v.getDomElement === "function"; } +/** + * @deprecated use isRendition + */ +export const isBuildContext = isRendition; + export function isPlainObject(v: object) { if (typeof v !== "object") return false; @@ -116,15 +126,21 @@ export function emptyNode(target: Node) dom.empty(target); } -/** This function starts all widgets inside the DOM node if the target is a node, - * or starts widget itself if the target is the widget. +/** This function starts all widgets inside the DOM node if the target is a node + * or starts widget itself if the target is the widget. If the specified node + * associated with the widget that widget will be started. * * @param target DOM node to find and start widgets or the widget itself. */ export function startupWidgets(target: Node | _WidgetBase, skipNode?: Node) { if (isNode(target)) { - registry.findWidgets(target, skipNode).forEach(w => w.startup()); + const w = registry.byNode(target); + if (w) { + w.startup && w.startup(); + } else { + registry.findWidgets(target, skipNode).forEach(w => w.startup()); + } } else { - target.startup(); + target.startup && target.startup(); } } \ No newline at end of file