##// 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
@@ -5,4 +5,3 description=Create HyperText with Typesc
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
@@ -21,9 +21,9 const sink = <T>(consumer: Consumer<T>)
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
@@ -78,6 +78,7 export function watch<T extends Stateful
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}) => {
@@ -85,6 +86,7 export function watch<T extends Stateful
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 )
@@ -1,7 +1,6
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;
@@ -15,13 +14,11 export class FunctionRendition extends R
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() {
@@ -1,14 +1,15
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");
@@ -20,13 +21,24 export class HtmlRendition extends Rendi
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() {
@@ -1,10 +1,11
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
@@ -28,7 +29,8 export class WatchRendition<T> extends R
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 }
@@ -37,12 +39,10 export class WatchRendition<T> extends R
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() {
@@ -4,7 +4,7 import { DojoNodePosition, isElementNode
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 {
@@ -15,10 +15,10 export interface _Widget {
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;
@@ -32,7 +32,7 export class WidgetRendition extends Ren
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) {
@@ -42,7 +42,7 export class WidgetRendition extends Ren
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) {
@@ -52,7 +52,7 export class WidgetRendition extends Ren
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 {
@@ -60,20 +60,20 export class WidgetRendition extends Ren
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 };
@@ -83,6 +83,11 export class WidgetRendition extends Ren
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() {
@@ -1,12 +1,53
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 */
@@ -16,14 +57,17 export const getScope = () => _scope;
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();
@@ -31,7 +75,7 export const render = async (rendition:
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());
@@ -40,7 +84,7 export const getItemDom = (v: unknown, s
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;
@@ -50,7 +94,7 export const getItemDom = (v: unknown, s
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 {
@@ -2,7 +2,6 import { IDestroyable } from "@implab/co
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;
@@ -14,7 +13,7 export type DojoNodePosition = "first" |
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 }
@@ -111,7 +110,7 export const destroy = (target: Node | I
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 }
@@ -169,7 +168,7 export const placeAt = (node: Node, refN
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);
@@ -75,6 +75,7 declare namespace JSX {
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 }
@@ -3,6 +3,8 plugins {
3 3 id "ivy-publish"
4 4 }
5 5
6 def container = "djx-playground"
7
6 8 configurations {
7 9 npmLocal
8 10 }
@@ -105,4 +107,17 task copyApp(type: Copy) {
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,7 +1,8
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
@@ -12,10 +13,37 export default class MainWidget extends
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