##// END OF EJS Templates
refactoring, adding scope to rendering methods
cin -
r96:a316cfea8bb1 v1.3
parent child
Show More
@@ -0,0 +1,34
1 import { IDestroyable } from "@implab/core-amd/interfaces";
2
3 export interface Sink<T> {
4 next: (value: T) => void;
5 error: (e: unknown) => void;
6 complete: () => void;
7 }
8
9 export type Consumer<T> = Partial<Sink<T>>;
10
11 export type Producer<T> = (sink: Sink<T>) => (void | (() => void));
12
13 export interface Observable<T> {
14 on(sink: Partial<Sink<T>>): IDestroyable;
15 }
16
17 const noop = () => {};
18
19 const sink = <T>(consumer: Consumer<T>) => {
20 const { next = noop, error = noop, complete = noop } = consumer;
21 let done = false;
22
23 return {
24 next: (value: T) => done && next(value),
25 error: (e: unknown) => done && (done = true, error(e)),
26 complete: () => done && (done = true, complete())
27 };
28 }
29
30 export const observe = <T>(producer: Producer<T>) => ({
31 on: (consumer: Consumer<T>) => ({
32 destroy: producer(sink(consumer)) ?? noop
33 })
34 });
@@ -1,119 +1,114
1 import { Constructor, IDestroyable, IRemovable } from "@implab/core-amd/interfaces";
1 import { Constructor } from "@implab/core-amd/interfaces";
2 2 import { HtmlRendition } from "./tsx/HtmlRendition";
3 3 import { WidgetRendition } from "./tsx/WidgetRendition";
4 import { destroy, isWidgetConstructor, Rendition } from "./tsx/traits";
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 import { WatchRendition } from "./tsx/WatchRendition";
10 import { observe } from "./observable";
9 11
10 12 export function createElement<T extends Constructor | string | ((props: any) => Element)>(elementType: T, ...args: any[]): Rendition {
11 13 if (typeof elementType === "string") {
12 14 const ctx = new HtmlRendition(elementType);
13 15 if (args)
14 16 args.forEach(x => ctx.visitNext(x));
15 17
16 18 return ctx;
17 19 } else if (isWidgetConstructor(elementType)) {
18 20 const ctx = new WidgetRendition(elementType);
19 21 if (args)
20 22 args.forEach(x => ctx.visitNext(x));
21 23
22 24 return ctx;
23 25 } else if (typeof elementType === "function") {
24 26 const ctx = new FunctionRendition(elementType as (props: any) => Element);
25 27 if (args)
26 28 args.forEach(x => ctx.visitNext(x));
27 29
28 30 return ctx;
29 31 } else {
30 32 throw new Error(`The element type '${elementType}' is unsupported`);
31 33 }
32 34 }
33 35
34 36 export interface EventDetails<T = any> {
35 37 detail: T;
36 38 }
37 39
38 40 export interface EventSelector {
39 41 selectorTarget: HTMLElement;
40 42 target: HTMLElement;
41 43 }
42 44
43 45 export type DojoMouseEvent<T = any> = MouseEvent & EventSelector & EventDetails<T>;
44 46
45 47 type StatefulProps<T> = T extends Stateful<infer A> ? A : never;
46 48
47 type CleanFn = (instance: IRemovable | IDestroyable) => void;
48 49
49 50 /**
50 51 * Observers the property and calls render callback each change.
51 52 *
52 53 * @param target The target object which property will be observed.
53 54 * @param prop The name of the property.
54 55 * @param render The callback which will be called every time the value is changed
55 * @param cleanupOrOwner The object with method `own` or an callback to register lifecycle for the observer.
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 render: (model: W[K]) => any,
62 cleanupOrOwner?: { own: CleanFn } | CleanFn
61 render: (model: W[K]) => any
63 62 ): Rendition;
64 63 /**
65 64 * Observers the property and calls render callback each change.
66 65 *
67 66 * @param target The target object which property will be observed.
68 67 * @param prop The name of the property.
69 68 * @param render The callback which will be called every time the value is changed
70 * @param cleanupOrOwner The object with method `own` or an callback to register lifecycle for the observer.
71 69 * @returns Rendition which is created instantly
72 70 */
73 71 export function watch<T extends Stateful, K extends keyof StatefulProps<T>>(
74 72 target: T,
75 73 prop: K,
76 render: (model: StatefulProps<T>[K]) => any,
77 cleanupOrOwner?: { own: CleanFn } | CleanFn
74 render: (model: StatefulProps<T>[K]) => any
78 75 ): Rendition;
79 76 export function watch<T extends Stateful, K extends keyof StatefulProps<T> & string>(
80 77 target: T,
81 78 prop: K,
82 render: (model: StatefulProps<T>[K]) => any,
83 cleanupOrOwner: { own: CleanFn } | CleanFn = () => { }
79 render: (model: StatefulProps<T>[K]) => any
84 80 ) {
85 let rendition = new FunctionRendition(() => render(target.get(prop)));
86 const _own = cleanupOrOwner instanceof Function ? cleanupOrOwner : (x: IRemovable) => cleanupOrOwner.own(x);
87 _own(target.watch(prop, (_name, oldValue, newValue) => {
88 if (oldValue !== newValue) {
89 const newRendition = new FunctionRendition(() => render(newValue));
90 newRendition.placeAt(rendition.getDomNode(), "replace");
91 destroy(rendition.getDomNode());
92 rendition = newRendition;
93 }
94 }));
95 return rendition;
81 return new WatchRendition(
82 render,
83 observe(({next}) => {
84 const h = target.watch(
85 prop,
86 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
87 );
88 return () => h.remove();
89 })
90 )
96 91 }
97 92
98 93 /** Decorates the method which will be registered as the handle for the specified event.
99 94 * This decorator can be applied to DjxWidgetBase subclass methods.
100 95 *
101 96 * ```
102 97 * @on("click")
103 98 * _onClick(eventObj: MouseEvent) {
104 99 * // ...
105 100 * }
106 101 * ```
107 102 */
108 103 export const on = <E extends string>(...eventNames: E[]) =>
109 104 <K extends string,
110 105 T extends DjxWidgetBase<any, { [p in E]: EV }>,
111 106 EV extends Event
112 107 >(
113 108 target: T,
114 109 key: K,
115 110 _descriptor: TypedPropertyDescriptor<(eventObj: EV) => void> | TypedPropertyDescriptor<() => void>
116 111 ): any => {
117 112 const handlers = eventNames.map(eventName => ({ eventName, handlerMethod: key }));
118 113 target._eventHandlers = target._eventHandlers ? target._eventHandlers.concat(handlers) : handlers;
119 114 };
@@ -1,30 +1,33
1 1 import { argumentNotNull } from "@implab/core-amd/safe";
2 import { getItemDom } from "./Renderer";
2 3 import { RenditionBase } from "./RenditionBase";
4 import { IScope } from "./Scope";
3 5
4 6 export class FunctionRendition extends RenditionBase<Node> {
5 7 private _component: (...args: any[]) => any;
6 8
7 9 private _node: Node | undefined;
8 10
9 11 constructor(component: (...args: any[]) => any) {
10 12 super();
11 13 argumentNotNull(component, "component");
12 14
13 15 this._component = component;
14 16 }
15 17
16 protected _create(attrs: object, children: any[]) {
18 protected _create(attrs: object, children: any[], scope: IScope) {
17 19 const _attrs: any = attrs || {};
18 const _children = children.map(x => this.getItemDom(x));
19 this._node = this.getItemDom(
20 this._component.call(null, { ..._attrs, children: _children })
20 const _children = children.map(x => getItemDom(x, scope));
21 this._node = getItemDom(
22 this._component.call(null, { ..._attrs, children: _children }),
23 scope
21 24 );
22 25 }
23 26
24 27 protected _getDomNode() {
25 28 if (!this._node)
26 29 throw new Error("The instance of the widget isn't created");
27 30 return this._node;
28 31 }
29 32
30 33 }
@@ -1,36 +1,39
1 1 import dom = require("dojo/dom-construct");
2 2 import { argumentNotEmptyString } from "@implab/core-amd/safe";
3 3 import { RenditionBase } from "./RenditionBase";
4 import { placeAt } from "./traits";
5 import { IScope } from "./Scope";
6 import { getItemDom } from "./Renderer";
4 7
5 8 export class HtmlRendition extends RenditionBase<HTMLElement> {
6 9 elementType: string;
7 10
8 11 _element: HTMLElement | undefined;
9 12
10 13 constructor(elementType: string) {
11 14 argumentNotEmptyString(elementType, "elementType");
12 15 super();
13 16
14 17 this.elementType = elementType;
15 18 }
16 19
17 _addChild(child: any): void {
20 _addChild(child: unknown, scope: IScope): void {
18 21 if (!this._element)
19 22 throw new Error("The HTML element isn't created");
20 dom.place(this.getItemDom(child), this._element);
23 placeAt(getItemDom(child, scope), this._element);
21 24 }
22 25
23 _create(attrs: object, children: any[]) {
26 _create(attrs: object, children: unknown[], scope: IScope) {
24 27 this._element = dom.create(this.elementType, attrs);
25 28
26 children.forEach(v => this._addChild(v));
29 children.forEach(v => this._addChild(v, scope));
27 30 }
28 31
29 32 _getDomNode() {
30 33 if (!this._element)
31 34 throw new Error("The HTML element isn't created");
32 35
33 36 return this._element;
34 37 }
35 38
36 39 }
@@ -1,26 +1,49
1 import { Scope } from "./Scope";
2 import { destroy, Rendition } from "./traits";
1 import { IScope, Scope } from "./Scope";
2 import { destroy, isNode, isRendition, isWidget, Rendition } from "./traits";
3 3
4 4 let _scope = Scope.dummy;
5 5
6 6 const beginRender = async () => {
7 7 }
8 8
9 9 const endRender = () => {
10 10 }
11 11
12 12 export const getScope = () => _scope;
13 13
14 14 export const render = async (rendition: () => Rendition, scope = Scope.dummy) => {
15 15 await beginRender();
16 16 const prev = _scope;
17 17 _scope = scope;
18 18 try {
19 19 const node = rendition().getDomNode();
20 20 scope.own(() => destroy(node));
21 21 return node;
22 22 } finally {
23 23 _scope = prev;
24 24 endRender();
25 25 }
26 26 }
27
28 /** Renders DOM element for different types of the argument. */
29 export const getItemDom = (v: unknown, scope: IScope) => {
30 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
31 // primitive types converted to the text nodes
32 return document.createTextNode(v.toString());
33 } else if (isNode(v)) {
34 // nodes are kept as is
35 return v;
36 } else if (isRendition(v)) {
37 // renditions are instantiated
38 return v.getDomNode(scope);
39 } else if (isWidget(v)) {
40 // widgets are converted to it's markup
41 return v.domNode;
42 } else if (typeof v === "boolean" || v === null || v === undefined) {
43 // null | undefined | boolean are removed, converted to comments
44 return document.createComment(`[${typeof v} ${String(v)}]`);
45 } else {
46 // bug: explicit error otherwise
47 throw new Error("Invalid parameter: " + v);
48 }
49 }
@@ -1,117 +1,73
1 1 import { isNull, mixin } from "@implab/core-amd/safe";
2 import { isPlainObject, isNode, isRendition, DojoNodePosition, Rendition, isInPage, isWidget, isDocumentFragmentNode, startupWidgets } from "./traits";
2 import { isPlainObject, DojoNodePosition, Rendition, isDocumentFragmentNode, placeAt, collectNodes, autostartWidgets } from "./traits";
3 3
4 import dom = require("dojo/dom-construct");
5 import registry = require("dijit/registry");
6
4 import { IScope } from "./Scope";
5 import { getScope } from "./Renderer";
7 6
8 7 export abstract class RenditionBase<TNode extends Node> implements Rendition<TNode> {
9 8 private _attrs = {};
10 9
11 10 private _children = new Array();
12 11
13 12 private _created: boolean = false;
14 13
15 14 visitNext(v: any) {
16 15 if (this._created)
17 16 throw new Error("The Element is already created");
18 17
19 18 if (isNull(v) || typeof v === "boolean")
20 19 // skip null, undefined, booleans ( this will work: {value && <span>{value}</span>} )
21 20 return;
22 21
23 22 if (isPlainObject(v)) {
24 23 mixin(this._attrs, v);
25 24 } else if (v instanceof Array) {
26 25 v.forEach(x => this.visitNext(x));
27 26 } else {
28 27 this._children.push(v);
29 28 }
30 29 }
31 30
32 /** Renders DOM element for different types of the argument. */
33 protected getItemDom(v: any) {
34 const tv = typeof v;
35
36 if (tv === "string" || tv === "number" || v instanceof RegExp || v instanceof Date) {
37 // primitive types converted to the text nodes
38 return document.createTextNode(v.toString());
39 } else if (isNode(v)) {
40 // nodes are kept as is
41 return v;
42 } else if (isRendition(v)) {
43 // renditions are instantiated
44 return v.getDomNode();
45 } else if (isWidget(v)) {
46 // widgets are converted to it's markup
47 return v.domNode;
48 } else if (tv === "boolean" || v === null || v === undefined) {
49 // null | undefined | boolean are removed, converted to comments
50 return document.createComment(`[${tv} ${String(v)}]`);
51 } else {
52 // bug: explicit error otherwise
53 throw new Error("Invalid parameter: " + v);
54 }
55 }
56
57 ensureCreated() {
31 ensureCreated(scope: IScope) {
58 32 if (!this._created) {
59 this._create(this._attrs, this._children);
33 this._create(this._attrs, this._children, scope);
60 34 this._children = [];
61 35 this._attrs = {};
62 36 this._created = true;
63 37 }
64 38 }
65 39
66 40 /** Is rendition was instantiated to the DOM node */
67 41 isCreated() {
68 42 return this._created;
69 43 }
70 44
71 45 /** Creates DOM node if not created. No additional actions are taken. */
72 getDomNode() {
73 this.ensureCreated();
46 getDomNode(scope?: IScope) {
47 this.ensureCreated(scope || getScope());
74 48 return this._getDomNode();
75 49 }
76 50
77 51 /** Creates DOM node if not created, places it to the specified position
78 52 * and calls startup() method for all widgets contained by this node.
79 53 *
80 54 * @param {string | Node} refNode The reference node where the created
81 55 * DOM should be placed.
82 56 * @param {DojoNodePosition} position Optional parameter, specifies the
83 57 * position relative to refNode. Default is "last" (i.e. last child).
84 58 */
85 placeAt(refNode: string | Node, position?: DojoNodePosition) {
59 placeAt(refNode: string | Node, position: DojoNodePosition = "last") {
86 60 const domNode = this.getDomNode();
87 61
88 const collect = (collection: HTMLCollection) => {
89 const items = [];
90 for (let i = 0, n = collection.length; i < n; i++) {
91 items.push(collection[i]);
92 }
93 return items;
94 };
62 const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.children) : [domNode];
95 63
96 const startup = (node: Node) => {
97 if (node.parentNode) {
98 const parentWidget = registry.getEnclosingWidget(node.parentNode);
99 if (parentWidget && parentWidget._started)
100 return startupWidgets(node);
101 }
102 if (isInPage(node))
103 startupWidgets(node);
104 };
64 placeAt(domNode, refNode, position);
105 65
106 const startupPending = isDocumentFragmentNode(domNode) ? collect(domNode.children) : [domNode];
107
108 dom.place(domNode, refNode, position);
109
110 startupPending.forEach(startup);
66 startupPending.forEach(autostartWidgets);
111 67
112 68 }
113 69
114 protected abstract _create(attrs: object, children: any[]): void;
70 protected abstract _create(attrs: object, children: unknown[], scope: IScope): void;
115 71
116 72 protected abstract _getDomNode(): TNode;
117 73 }
@@ -1,58 +1,54
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 { place } from "dojo/dom-construct";
5 import { getScope, render } from "./Renderer";
4 import { render } from "./Renderer";
6 5 import { RenditionBase } from "./RenditionBase";
7 import { Scope } from "./Scope";
8 import { locateNode } from "./traits";
6 import { IScope, Scope } from "./Scope";
7 import { Observable } from "../observable";
9 8
10 9 const trace = TraceSource.get(mid);
11 10
12 11 export class WatchRendition<T> extends RenditionBase<Node> {
13 12 private readonly _factory: (arg: T) => any;
14 13
15 14 private _node: Node;
16 15
17 16 private readonly _scope = new Scope();
18 17
19 constructor(component: (arg: T) => any, subject: any) {
18 private readonly _subject: Observable<T>;
19
20 constructor(component: (arg: T) => any, subject: Observable<T>) {
20 21 super();
21 22 argumentNotNull(component, "component");
22 23
23 24 this._factory = component;
24 25
26 this._subject = subject;
27
25 28 this._node = document.createComment("WatchRendition placeholder");
26 29 }
27 30
28 protected _create(attrs: object, children: any[]) {
29 const _attrs: any = attrs || {};
30 const _children = children.map(x => this.getItemDom(x));
31 this._node = this.getItemDom(
32 this._factory.call(null, { ..._attrs, children: _children })
33 );
34
35 const scope = getScope();
31 protected _create(attrs: object, children: any[], scope: IScope) {
36 32 scope.own(this._scope);
37
38 // Ссли ΠΎΡ‚Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ»ΠΈ тСкст? ΠΈΠ»ΠΈ DocumentFragment
33 scope.own(this._subject.on({ next: this._onValue }))
39 34 }
40 35
36 private _onValue = (value: T) => void this._render(value).catch( e => trace.error(e));
37
41 38 private async _render(value: T) {
42 const [refNode, position] = locateNode(this._node);
39 const prevNode = this._node;
43 40 this._scope.clean();
44 41
45 42 this._node = await render(() => this._factory(value), this._scope);
46 43
47 if (refNode)
48 place(this._node, refNode, position);
44 this.placeAt(prevNode, "replace");
49 45 }
50 46
51 47 protected _getDomNode() {
52 48 if (!this._node)
53 49 throw new Error("The instance of the widget isn't created");
54 50 return this._node;
55 51 }
56 52
57 53
58 54 }
@@ -1,128 +1,129
1 import dom = require("dojo/dom-construct");
2 1 import { argumentNotNull } from "@implab/core-amd/safe";
3 import { RenditionBase } from "./RenditionBase";
4 import { DojoNodePosition, isElementNode, isInPage, isWidget } from "./traits";
2 import { getItemDom, RenditionBase } from "./RenditionBase";
3 import { DojoNodePosition, isElementNode, isInPage, isWidget, placeAt } from "./traits";
5 4 import registry = require("dijit/registry");
6 5 import ContentPane = require("dijit/layout/ContentPane");
6 import { IScope } from "./Scope";
7 import { getScope } from "./Renderer";
7 8
8 9 // tslint:disable-next-line: class-name
9 10 export interface _Widget {
10 11 domNode: Node;
11 12
12 13 containerNode?: Node;
13 14
14 15 placeAt?(refNode: string | Node, position?: DojoNodePosition): void;
15 16 startup?(): void;
16 17
17 18 addChild?(widget: any, index?: number): void;
18 19 }
19 20
20 21 export type _WidgetCtor = new (attrs: any, srcNode?: string | Node) => _Widget;
21 22
22 23 export class WidgetRendition extends RenditionBase<Node> {
23 24 readonly widgetClass: _WidgetCtor;
24 25
25 26 _instance: _Widget | undefined;
26 27
27 28 constructor(widgetClass: _WidgetCtor) {
28 29 super();
29 30 argumentNotNull(widgetClass, "widgetClass");
30 31
31 32 this.widgetClass = widgetClass;
32 33 }
33 34
34 _addChild(child: any): void {
35 _addChild(child: any, scope: IScope): void {
35 36 const instance = this._getInstance();
36 37
37 38 if (instance.addChild) {
38 39 if (child instanceof WidgetRendition) {
39 40 // layout containers add custom logic to addChild methods
40 instance.addChild(child.getWidgetInstance());
41 instance.addChild(child.getWidgetInstance(scope));
41 42 } else if (isWidget(child)) {
42 43 instance.addChild(child);
43 44 } else {
44 const childDom = this.getItemDom(child);
45 const childDom = getItemDom(child, scope);
45 46 const w = isElementNode(childDom) ? registry.byNode(childDom) : undefined;
46 47
47 48 if (w) {
48 49 instance.addChild(w);
49 50 } else {
50 51 if (!instance.containerNode)
51 52 throw new Error("Failed to add DOM content. The widget doesn't have a containerNode");
52 53
53 54 // the current widget isn't started, it's children shouldn't start too
54 dom.place(this.getItemDom(child), instance.containerNode);
55 placeAt(getItemDom(child,scope), instance.containerNode, "last");
55 56 }
56 57 }
57 58 } else {
58 59 if (!instance.containerNode)
59 60 throw new Error("The widget doesn't have neither addChild nor containerNode");
60 61
61 62 // the current widget isn't started, it's children shouldn't start too
62 dom.place(this.getItemDom(child), instance.containerNode);
63 placeAt(getItemDom(child, scope), instance.containerNode, "last");
63 64 }
64 65 }
65 66
66 protected _create(attrs: any, children: any[]) {
67 protected _create(attrs: any, children: any[], scope: IScope) {
67 68 if (this.widgetClass.prototype instanceof ContentPane) {
68 69 // a special case for the ContentPane this is for
69 70 // the compatibility with this heavy widget, all
70 71 // regular containers could be easily manipulated
71 72 // through `containerNode` property or `addChild` method.
72 73
73 74 // render children to the DocumentFragment
74 75 const content = document.createDocumentFragment();
75 children.forEach(child => content.appendChild(this.getItemDom(child)));
76 children.forEach(child => content.appendChild(getItemDom(child, scope)));
76 77
77 78 // set the content property to the parameters of the widget
78 79 const _attrs = { ...attrs, content };
79 80 this._instance = new this.widgetClass(_attrs);
80 81 } else {
81 82 this._instance = new this.widgetClass(attrs);
82 children.forEach(x => this._addChild(x));
83 children.forEach(x => this._addChild(x, scope));
83 84 }
84 85
85 86 }
86 87
87 88 private _getInstance() {
88 89 if (!this._instance)
89 90 throw new Error("The instance of the widget isn't created");
90 91 return this._instance;
91 92 }
92 93
93 94 protected _getDomNode() {
94 95 if (!this._instance)
95 96 throw new Error("The instance of the widget isn't created");
96 97 return this._instance.domNode;
97 98 }
98 99
99 100 /** Overrides default placeAt implementation. Calls placeAt of the
100 101 * widget and then starts it.
101 102 *
102 103 * @param refNode A node or id of the node where the widget should be placed.
103 104 * @param position A position relative to refNode.
104 105 */
105 106 placeAt(refNode: string | Node, position?: DojoNodePosition) {
106 this.ensureCreated();
107 this.ensureCreated(getScope());
107 108 const instance = this._getInstance();
108 109 if (typeof instance.placeAt === "function") {
109 110 instance.placeAt(refNode, position);
110 111
111 112 // fix the dojo startup behavior when the widget is placed
112 113 // directly to the document and doesn't have any enclosing widgets
113 114 const parentWidget = instance.domNode.parentNode ?
114 115 registry.getEnclosingWidget(instance.domNode.parentNode) : null;
115 116 if (!parentWidget && isInPage(instance.domNode) && typeof instance.startup === "function")
116 117 instance.startup();
117 118 } else {
118 119 // the widget doesn't have a placeAt method, strange but whatever
119 120 super.placeAt(refNode, position);
120 121 }
121 122 }
122 123
123 getWidgetInstance() {
124 this.ensureCreated();
124 getWidgetInstance(scope?: IScope) {
125 this.ensureCreated(scope || getScope());
125 126 return this._getInstance();
126 127 }
127 128
128 129 }
@@ -1,206 +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 dom = require("dojo/dom-construct");
5 import { IScope } from "./Scope";
6 6
7 7 type _WidgetBaseConstructor = typeof _WidgetBase;
8 8
9 9 export type DojoNodePosition = "first" | "after" | "before" | "last" | "replace" | "only" | number;
10 10
11 export type DojoNodeLocation = [Node | null, DojoNodePosition];
11 export type DojoNodeLocation = [Node, DojoNodePosition];
12 12
13 13 export interface Rendition<TNode extends Node = Node> {
14 getDomNode(): TNode;
14 getDomNode(scope?: IScope): TNode;
15 15
16 16 placeAt(refNode: string | Node, position?: DojoNodePosition): void;
17 17 }
18 18
19 19 /**
20 20 * @deprecated use Rendition
21 21 */
22 22 export type BuildContext<TNode extends Node = Node> = Rendition<TNode>;
23 23
24 24 export interface IRecursivelyDestroyable {
25 25 destroyRecursive(): void;
26 26 }
27 27
28 export function isNode(el: any): el is Node {
29 return el && el.nodeName && el.nodeType;
30 }
28 export const isNode = (el: unknown): el is Node => !!(el && (el as Node).nodeName && (el as Node).nodeType);
31 29
32 export function isElementNode(el: any): el is Element {
33 return isNode(el) && el.nodeType === 1;
34 }
30 export const isElementNode = (el: unknown): el is Element => isNode(el) && el.nodeType === 1;
35 31
36 export function isTextNode(el: any): el is Text {
37 return isNode(el) && el.nodeType === 3;
38 }
32 export const isTextNode = (el: unknown): el is Text => isNode(el) && el.nodeType === 3;
39 33
40 export function isProcessingInstructionNode(el: any): el is ProcessingInstruction {
41 return isNode(el) && el.nodeType === 7;
42 }
34 export const isProcessingInstructionNode = (el: unknown): el is ProcessingInstruction => isNode(el) && el.nodeType === 7;
43 35
44 export function isCommentNode(el: any): el is Comment {
45 return isNode(el) && el.nodeType === 8;
46 }
36 export const isCommentNode = (el: unknown): el is Comment => isNode(el) && el.nodeType === 8;
47 37
48 export function isDocumentNode(el: any): el is Document {
49 return isNode(el) && el.nodeType === 9;
50 }
38 export const isDocumentNode = (el: unknown): el is Document => isNode(el) && el.nodeType === 9;
51 39
52 export function isDocumentTypeNode(el: any): el is DocumentType {
53 return isNode(el) && el.nodeType === 10;
54 }
40 export const isDocumentTypeNode = (el: unknown): el is DocumentType => isNode(el) && el.nodeType === 10;
55 41
56 export function isDocumentFragmentNode(el: any): el is DocumentFragment {
57 return isNode(el) && el.nodeType === 11;
58 }
42 export const isDocumentFragmentNode = (el: any): el is DocumentFragment => isNode(el) && el.nodeType === 11;
59 43
60 export function isWidget(v: any): v is _WidgetBase {
61 return v && "domNode" in v;
62 }
44 export const isWidget = (v: unknown): v is _WidgetBase => !!(v && "domNode" in (v as _WidgetBase));
63 45
64 export function isRendition(v: any): v is Rendition {
65 return v && typeof v.getDomElement === "function";
66 }
46 export const isRendition = (v: unknown): v is Rendition => !!(v && typeof (v as Rendition).getDomNode === "function");
67 47
68 48 /**
69 49 * @deprecated use isRendition
70 50 */
71 51 export const isBuildContext = isRendition;
72 52
73 export function isPlainObject(v: object) {
53 export const isPlainObject = (v: object) => {
74 54 if (typeof v !== "object")
75 55 return false;
76 56
77 57 const vp = Object.getPrototypeOf(v);
78 58 return !vp || vp === Object.prototype;
79 59 }
80 60
81 export function isWidgetConstructor(v: any): v is _WidgetBaseConstructor {
82 return typeof v === "function" && v.prototype && (
61 export const isWidgetConstructor = (v: unknown): v is _WidgetBaseConstructor =>
62 typeof v === "function" && v.prototype && (
83 63 "domNode" in v.prototype ||
84 64 "buildRendering" in v.prototype
85 65 );
86 }
66
87 67
88 68 /** Tests whether the specified node is placed in visible dom.
89 69 * @param {Node} node The node to test
90 70 */
91 export function isInPage(node: Node) {
92 return (node === document.body) ? false : document.body.contains(node);
93 }
71 export const isInPage = (node: Node) => node === document.body ? false : document.body.contains(node);
94 72
95 export function isRecursivelyDestroyable(target: any): target is IRecursivelyDestroyable {
96 return target && typeof target.destroyRecursive === "function";
97 }
73 export const isRecursivelyDestroyable = (target: unknown): target is IRecursivelyDestroyable =>
74 !!(target && typeof (target as IRecursivelyDestroyable).destroyRecursive === "function");
75
98 76
99 77
100 78 /** Destroys DOM Node with all contained widgets.
101 79 * If the specified node is the root node of a widget, then the
102 80 * widget will be destroyed.
103 81 *
104 82 * @param target DOM Node or widget to destroy
105 83 */
106 export function destroy(target: Node | IDestroyable | IRecursivelyDestroyable) {
84 export const destroy = (target: Node | IDestroyable | IRecursivelyDestroyable) => {
107 85 if (isRecursivelyDestroyable(target)) {
108 86 target.destroyRecursive();
109 87 } else if (isDestroyable(target)) {
110 88 target.destroy();
111 89 } else if (isNode(target)) {
112 90 if (isElementNode(target)) {
113 91 const w = registry.byNode(target);
114 92 if (w) {
115 93 w.destroyRecursive();
116 94 } else {
117 registry.findWidgets(target).forEach(destroy);
118 dom.destroy(target);
95 emptyNode(target);
96 const parent = target.parentNode;
97 if (parent)
98 parent.removeChild(target);
119 99 }
120 100 }
121 101 }
122 102 }
123 103
124 104 /** Empties a content of the specified node and destroys all contained widgets.
125 105 *
126 * @param target DOM node to .
106 * @param target DOM node to empty.
127 107 */
128 export function emptyNode(target: Node) {
108 export const emptyNode = (target: Node) => {
129 109 registry.findWidgets(target).forEach(destroy);
130 dom.empty(target);
110
111 for(let c; c = target.lastChild;){ // intentional assignment
112 target.removeChild(c);
113 }
131 114 }
132 115
133 116 /** This function starts all widgets inside the DOM node if the target is a node
134 117 * or starts widget itself if the target is the widget. If the specified node
135 118 * associated with the widget that widget will be started.
136 119 *
137 120 * @param target DOM node to find and start widgets or the widget itself.
138 121 */
139 export function startupWidgets(target: Node | _WidgetBase, skipNode?: Node) {
122 export const startupWidgets = (target: Node | _WidgetBase, skipNode?: Node) => {
140 123 if (isNode(target)) {
141 124 if (isElementNode(target)) {
142 125 const w = registry.byNode(target);
143 126 if (w) {
144 127 if (w.startup)
145 128 w.startup();
146 129 } else {
147 130 registry.findWidgets(target, skipNode).forEach(x => x.startup());
148 131 }
149 132 }
150 133 } else {
151 134 if (target.startup)
152 135 target.startup();
153 136 }
154 137 }
155 138
156 export function locateNode(node: Node): DojoNodeLocation {
157 const next = node.nextSibling;
158 return next ?
159 [next, "before"] :
160 [node.parentNode, "last"];
139 /** Places the specified DOM node at the specified location.
140 *
141 * @param node The node which should be placed
142 * @param refNodeOrId The reference node where the created
143 * DOM should be placed.
144 * @param position Optional parameter, specifies the
145 * position relative to refNode. Default is "last" (i.e. last child).
146 */
147 export const placeAt = (node: Node, refNodeOrId: string | Node, position: DojoNodePosition = "last") => {
148 const ref = typeof refNodeOrId == "string" ? document.getElementById(refNodeOrId) : refNodeOrId;
149 if (!ref)
150 return;
151
152 const parent = ref.parentNode;
153
154 const insertBefore = (node: Node, refNode: Node | null) => parent && parent.insertBefore(node, refNode);
155
156 if (typeof position == "number") {
157 if (ref.childNodes.length <= position) {
158 ref.appendChild(node);
159 } else {
160 ref.insertBefore(node, ref.childNodes[position]);
161 }
162 } else {
163 switch (position) {
164 case "before":
165 insertBefore(node, ref);
166 break;
167 case "after":
168 insertBefore(node, ref.nextSibling);
169 break;
170 case "first":
171 insertBefore(node, parent && parent.firstChild);
172 break;
173 case "last":
174 insertBefore(node, null);
175 break;
176 case "only":
177 emptyNode(ref);
178 ref.appendChild(node);
179 break;
180 case "replace":
181 if (parent)
182 parent.replaceChild(node, ref);
183 destroy(ref);
184 break;
185 }
186 }
161 187 }
162 188
163 export const placeAt = (node: Node, refNodeOrId: string | Node, position: DojoNodePosition) => {
164 const collect = (collection: HTMLCollection) => {
189 /** Collects nodes from collection to an array.
190 *
191 * @param collection The collection of nodes.
192 * @returns The array of nodes.
193 */
194 export const collectNodes = (collection: HTMLCollection) => {
165 195 const items = [];
166 196 for (let i = 0, n = collection.length; i < n; i++) {
167 197 items.push(collection[i]);
168 198 }
169 199 return items;
170 200 };
171 201
172 const startup = (node: Node) => {
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) => {
173 207 if (node.parentNode) {
174 208 const parentWidget = registry.getEnclosingWidget(node.parentNode);
175 209 if (parentWidget && parentWidget._started)
176 210 return startupWidgets(node);
177 211 }
178 212 if (isInPage(node))
179 213 startupWidgets(node);
180 };
181
182 const ref = typeof refNodeOrId == "string" ? document.getElementById(refNodeOrId) : refNodeOrId;
183 if (!ref)
184 return;
185
186 const parent = ref.parentNode;
187
188 if (typeof position == "number") {
189
190 } else {
191 switch(position) {
192 case "before":
193 if (parent)
194 parent.insertBefore(node,ref);
195 case "after":
196 if (parent)
197 parent.insertBefore(node, ref.nextSibling);
198 }
199 }
200
201 const startupPending = isDocumentFragmentNode(node) ? collect(node.children) : [node];
202
203 dom.place(node, refNodeOrId, position);
204
205 startupPending.forEach(startup);
206 } No newline at end of file
214 }; No newline at end of file
@@ -1,68 +1,80
1 1 /// <reference path="./css.d.ts"/>
2 2
3 3 declare namespace JSX {
4 4
5 interface DjxIntrinsicAttributes {
5 type Ref<T> = (value: T) => void;
6
7 interface DjxIntrinsicAttributes<E> {
6 8 /** alias for className */
7 9 class: string;
8 10
9 11 /** specifies the name of the property in the widget where the the
10 12 * reference to the current object will be stored
11 13 */
12 14 "data-dojo-attach-point": string;
13 15
14 16 /** specifies handlers map for the events */
15 17 "data-dojo-attach-event": string;
16 18
19 ref: Ref<E>;
20
17 21 /** @deprecated */
18 22 [attr: string]: any;
19 23 }
20 24
21 25 interface DjxIntrinsicElements {
22 26 }
23 27
24 28 type RecursivePartial<T> = T extends string | number | boolean | null | undefined | Function ?
25 29 T :
26 30 { [k in keyof T]?: RecursivePartial<T[k]> };
27 31
28 32 type MatchingMemberKeys<T, U> = {
29 33 [K in keyof T]: T[K] extends U ? K : never;
30 34 }[keyof T];
31 35 type NotMatchingMemberKeys<T, U> = {
32 36 [K in keyof T]: T[K] extends U ? never : K;
33 37 }[keyof T];
34 38
35 39 type ExtractMembers<T, U> = Pick<T, MatchingMemberKeys<T, U>>;
36 40
37 41 type ExcludeMembers<T, U> = Pick<T, NotMatchingMemberKeys<T, U>>;
38 42
39 43 type ElementAttrNames<E> = NotMatchingMemberKeys<E, (...args: any[]) => any>;
40 44
41 45 type ElementAttrType<E, K extends keyof any> = K extends keyof E ? RecursivePartial<E[K]> : string;
42 46
43 47
44 48 type ElementAttrNamesBlacklist = "children" | "getRootNode" | keyof EventTarget;
45 49
46 50 /** This type extracts keys of the specified parameter E by the following rule:
47 51 * 1. skips all ElementAttrNamesBlacklist
48 52 * 2. skips all methods except with the signature of event handlers
49 53 */
50 54 type AssignableElementAttrNames<E> = {
51 55 [K in keyof E]: K extends ElementAttrNamesBlacklist ? never :
52 56 ((evt: Event) => any) extends E[K] ? K :
53 57 E[K] extends ((...args: any[]) => any) ? never :
54 58 K;
55 59 }[keyof E];
56 60
57 61 type LaxElement<E extends object> =
58 62 Pick<E, AssignableElementAttrNames<E>> &
59 DjxIntrinsicAttributes;
63 DjxIntrinsicAttributes<E>;
60 64
61 65 type LaxIntrinsicElementsMap = {
62 66 [tag in keyof HTMLElementTagNameMap]: LaxElement<HTMLElementTagNameMap[tag]>
63 67 } & DjxIntrinsicElements;
64 68
65 69 type IntrinsicElements = {
66 70 [tag in keyof LaxIntrinsicElementsMap]: RecursivePartial<LaxIntrinsicElementsMap[tag]>;
67 71 }
72
73 interface ElementChildrenAttribute {
74 children: {};
68 75 }
76
77 interface IntrinsicClassAttributes<T> {
78 ref: (value: T) => void;
79 }
80 }
@@ -1,70 +1,70
1 1 import { djbase, djclass, bind, prototype, AbstractConstructor } from "../declare";
2 2
3 3 import { DjxWidgetBase } from "../tsx/DjxWidgetBase";
4 4 import { createElement, on } from "../tsx";
5 5
6 6 interface MyWidgetAttrs {
7 7 title: string;
8 8
9 9 counter: number;
10 10 }
11 11
12 12 interface MyWidgetEvents {
13 13 "count-inc": Event & {
14 14 detail: number;
15 15 };
16 16
17 17 "count-dec": Event & {
18 18 detail: number;
19 19 };
20 20 }
21 21
22 22
23 23 @djclass
24 24 export class MyWidget extends djbase(DjxWidgetBase as AbstractConstructor<DjxWidgetBase<MyWidgetAttrs, MyWidgetEvents>>) {
25 25
26 26 @bind({ node: "titleNode", type: "innerHTML" })
27 27 title = "";
28 28
29 29 @prototype()
30 30 counter = 0;
31 31
32 32 render() {
33 const Frame = (props: any) => <div>{props.children}</div>;
33 const Frame = ({children, ref}: {ref: JSX.Ref<HTMLDivElement>, children: any[]}) => <div ref={ref} >{children}</div>;
34 34 return <div className="myWidget" onsubmit={e => this._onSubmit(e)} tabIndex={3} style={{ alignContent: "center", border: "1px solid" }} >
35 35 <h1 data-dojo-attach-point="titleNode"></h1>
36 <Frame>
36 <Frame ref={ v => {}}>
37 37 <span class="up-button" onclick={e => this._onIncClick(e)}>[+]</span>
38 38 <span class="down-button" onclick={() => this._onDecClick()}>[-]</span>
39 39 </Frame>
40 40 </div>;
41 41 }
42 42
43 43 postCreate() {
44 44 super.postCreate();
45 45
46 46 this.on("click", () => {});
47 47 }
48 48
49 49 _onSubmit(e: Event) {
50 50 }
51 51
52 52 _onIncClick(e: MouseEvent) {
53 53 this.set("counter", this.counter + 1);
54 54
55 55 this.emit("count-inc", { bubbles: false });
56 56 }
57 57
58 58 _onDecClick() {
59 59 this.emit("count-dec", { bubbles: false, detail: this.counter });
60 60 }
61 61
62 62 @on("count-inc")
63 63 private _onCounterInc(evt: Event & { detail: number; x?: number; }) {
64 64 }
65 65
66 66 @on("click", "keydown")
67 67 protected _onClick(event: MouseEvent | KeyboardEvent) {
68 68
69 69 }
70 70 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now