##// 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 license=BSD-2-Clause
5 license=BSD-2-Clause
6 repository=http://hg.code.sf.net/p/implabjs/djx
6 repository=http://hg.code.sf.net/p/implabjs/djx
7 npmScope=implab
7 npmScope=implab
8 symbols=pack No newline at end of file
@@ -21,9 +21,9 const sink = <T>(consumer: Consumer<T>)
21 let done = false;
21 let done = false;
22
22
23 return {
23 return {
24 next: (value: T) => done && next(value),
24 next: (value: T) => !done && next(value),
25 error: (e: unknown) => done && (done = true, error(e)),
25 error: (e: unknown) => !done && (done = true, error(e)),
26 complete: () => done && (done = true, complete())
26 complete: () => !done && (done = true, complete())
27 };
27 };
28 }
28 }
29
29
@@ -78,6 +78,7 export function watch<T extends Stateful
78 prop: K,
78 prop: K,
79 render: (model: StatefulProps<T>[K]) => any
79 render: (model: StatefulProps<T>[K]) => any
80 ) {
80 ) {
81
81 return new WatchRendition(
82 return new WatchRendition(
82 render,
83 render,
83 observe(({next}) => {
84 observe(({next}) => {
@@ -85,6 +86,7 export function watch<T extends Stateful
85 prop,
86 prop,
86 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
87 (_prop, oldValue, newValue) => oldValue !== newValue && next(newValue)
87 );
88 );
89 next(target.get(prop));
88 return () => h.remove();
90 return () => h.remove();
89 })
91 })
90 )
92 )
@@ -1,7 +1,6
1 import { argumentNotNull } from "@implab/core-amd/safe";
1 import { argumentNotNull } from "@implab/core-amd/safe";
2 import { getItemDom } from "./render";
2 import { getItemDom } from "./render";
3 import { RenditionBase } from "./RenditionBase";
3 import { RenditionBase } from "./RenditionBase";
4 import { IScope } from "./Scope";
5
4
6 export class FunctionRendition extends RenditionBase<Node> {
5 export class FunctionRendition extends RenditionBase<Node> {
7 private _component: (...args: any[]) => any;
6 private _component: (...args: any[]) => any;
@@ -15,13 +14,11 export class FunctionRendition extends R
15 this._component = component;
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 const _attrs: any = attrs || {};
18 const _attrs: any = attrs || {};
20 const _children = children.map(x => getItemDom(x, scope));
19 const _children = children.map(x => getItemDom(x));
21 this._node = getItemDom(
20 this._node = getItemDom(
22 this._component.call(null, { ..._attrs, children: _children }),
21 this._component.call(null, { ..._attrs, children: _children }));
23 scope
24 );
25 }
22 }
26
23
27 protected _getDomNode() {
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 import { argumentNotEmptyString } from "@implab/core-amd/safe";
3 import { argumentNotEmptyString } from "@implab/core-amd/safe";
3 import { RenditionBase } from "./RenditionBase";
4 import { RenditionBase } from "./RenditionBase";
4 import { placeAt } from "./traits";
5 import { placeAt } from "./traits";
5 import { IScope } from "./Scope";
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 elementType: string;
10 elementType: string;
10
11
11 _element: HTMLElement | undefined;
12 _element: Element | undefined;
12
13
13 constructor(elementType: string) {
14 constructor(elementType: string) {
14 argumentNotEmptyString(elementType, "elementType");
15 argumentNotEmptyString(elementType, "elementType");
@@ -20,13 +21,24 export class HtmlRendition extends Rendi
20 _addChild(child: unknown, scope: IScope): void {
21 _addChild(child: unknown, scope: IScope): void {
21 if (!this._element)
22 if (!this._element)
22 throw new Error("The HTML element isn't created");
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 _create({ xmlns, ref, ...attrs }: { xmlns?: string, ref?: JSX.Ref<Element> }, children: unknown[], scope: IScope) {
27 this._element = dom.create(this.elementType, attrs);
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 children.forEach(v => this._addChild(v, scope));
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 _getDomNode() {
44 _getDomNode() {
@@ -1,10 +1,11
1 import { id as mid } from "module";
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 import { argumentNotNull } from "@implab/core-amd/safe";
3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getItemDom, render } from "./render";
4 import { getScope, render } from "./render";
5 import { RenditionBase } from "./RenditionBase";
5 import { RenditionBase } from "./RenditionBase";
6 import { IScope, Scope } from "./Scope";
6 import { Scope } from "./Scope";
7 import { Observable } from "../observable";
7 import { Observable } from "../observable";
8 import { destroy } from "./traits";
8
9
9 const trace = TraceSource.get(mid);
10 const trace = TraceSource.get(mid);
10
11
@@ -28,7 +29,8 export class WatchRendition<T> extends R
28 this._node = document.createComment("WatchRendition placeholder");
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 scope.own(this._scope);
34 scope.own(this._scope);
33 scope.own(this._subject.on({ next: this._onValue }));
35 scope.own(this._subject.on({ next: this._onValue }));
34 }
36 }
@@ -37,12 +39,10 export class WatchRendition<T> extends R
37 void this._render(value).catch( e => trace.error(e));
39 void this._render(value).catch( e => trace.error(e));
38
40
39 private async _render(value: T) {
41 private async _render(value: T) {
40 const prevNode = this._node;
41 this._scope.clean();
42 this._scope.clean();
42
43 const [refNode, ...rest] = await render(this._component(value), this._node, "replace", this._scope);
43 this._node = await render(this._component(value), this._scope);
44 this._node = refNode;
44
45 this._scope.own(() => rest.forEach(destroy));
45 this.placeAt(prevNode, "replace");
46 }
46 }
47
47
48 protected _getDomNode() {
48 protected _getDomNode() {
@@ -4,7 +4,7 import { DojoNodePosition, isElementNode
4 import registry = require("dijit/registry");
4 import registry = require("dijit/registry");
5 import ContentPane = require("dijit/layout/ContentPane");
5 import ContentPane = require("dijit/layout/ContentPane");
6 import { IScope } from "./Scope";
6 import { IScope } from "./Scope";
7 import { getItemDom, getScope } from "./render";
7 import { getItemDom, getScope, renderHook } from "./render";
8
8
9 // tslint:disable-next-line: class-name
9 // tslint:disable-next-line: class-name
10 export interface _Widget {
10 export interface _Widget {
@@ -15,10 +15,10 export interface _Widget {
15 placeAt?(refNode: string | Node, position?: DojoNodePosition): void;
15 placeAt?(refNode: string | Node, position?: DojoNodePosition): void;
16 startup?(): void;
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 export class WidgetRendition extends RenditionBase<Node> {
23 export class WidgetRendition extends RenditionBase<Node> {
24 readonly widgetClass: _WidgetCtor;
24 readonly widgetClass: _WidgetCtor;
@@ -32,7 +32,7 export class WidgetRendition extends Ren
32 this.widgetClass = widgetClass;
32 this.widgetClass = widgetClass;
33 }
33 }
34
34
35 _addChild(child: any, scope: IScope): void {
35 _addChild(child: unknown, scope: IScope): void {
36 const instance = this._getInstance();
36 const instance = this._getInstance();
37
37
38 if (instance.addChild) {
38 if (instance.addChild) {
@@ -42,7 +42,7 export class WidgetRendition extends Ren
42 } else if (isWidget(child)) {
42 } else if (isWidget(child)) {
43 instance.addChild(child);
43 instance.addChild(child);
44 } else {
44 } else {
45 const childDom = getItemDom(child, scope);
45 const childDom = getItemDom(child);
46 const w = isElementNode(childDom) ? registry.byNode(childDom) : undefined;
46 const w = isElementNode(childDom) ? registry.byNode(childDom) : undefined;
47
47
48 if (w) {
48 if (w) {
@@ -52,7 +52,7 export class WidgetRendition extends Ren
52 throw new Error("Failed to add DOM content. The widget doesn't have a containerNode");
52 throw new Error("Failed to add DOM content. The widget doesn't have a containerNode");
53
53
54 // the current widget isn't started, it's children shouldn't start too
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 } else {
58 } else {
@@ -60,20 +60,20 export class WidgetRendition extends Ren
60 throw new Error("The widget doesn't have neither addChild nor containerNode");
60 throw new Error("The widget doesn't have neither addChild nor containerNode");
61
61
62 // the current widget isn't started, it's children shouldn't start too
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 if (this.widgetClass.prototype instanceof ContentPane) {
68 if (this.widgetClass.prototype instanceof ContentPane) {
69 // a special case for the ContentPane this is for
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 // regular containers could be easily manipulated
71 // regular containers could be easily manipulated
72 // through `containerNode` property or `addChild` method.
72 // through `containerNode` property or `addChild` method.
73
73
74 // render children to the DocumentFragment
74 // render children to the DocumentFragment
75 const content = document.createDocumentFragment();
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 // set the content property to the parameters of the widget
78 // set the content property to the parameters of the widget
79 const _attrs = { ...attrs, content };
79 const _attrs = { ...attrs, content };
@@ -83,6 +83,11 export class WidgetRendition extends Ren
83 children.forEach(x => this._addChild(x, scope));
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 private _getInstance() {
93 private _getInstance() {
@@ -1,12 +1,53
1 import { IScope, Scope } from "./Scope";
1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { destroy, isNode, isRendition, isWidget, Rendition } from "./traits";
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 let _scope = Scope.dummy;
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 const endRender = () => {
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 /** Returns the current scope */
53 /** Returns the current scope */
@@ -16,14 +57,17 export const getScope = () => _scope;
16 * @param rendition The rendition to be rendered
57 * @param rendition The rendition to be rendered
17 * @param scope The scope
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 await beginRender();
61 await beginRender();
21 const prev = _scope;
62 const prev = _scope;
22 _scope = scope;
63 _scope = scope;
23 try {
64 try {
24 const node = getItemDom(rendition, scope);
65 const domNode = getItemDom(rendition);
25 scope.own(() => destroy(node));
66 const startupPending = isDocumentFragmentNode(domNode) ? collectNodes(domNode.children) : [domNode];
26 return node;
67 placeAt(domNode, refNode, position);
68 startupPending.forEach(autostartWidgets);
69
70 return startupPending;
27 } finally {
71 } finally {
28 _scope = prev;
72 _scope = prev;
29 endRender();
73 endRender();
@@ -31,7 +75,7 export const render = async (rendition:
31 }
75 }
32
76
33 /** Renders DOM element for different types of the argument. */
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 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
79 if (typeof v === "string" || typeof v === "number" || v instanceof RegExp || v instanceof Date) {
36 // primitive types converted to the text nodes
80 // primitive types converted to the text nodes
37 return document.createTextNode(v.toString());
81 return document.createTextNode(v.toString());
@@ -40,7 +84,7 export const getItemDom = (v: unknown, s
40 return v;
84 return v;
41 } else if (isRendition(v)) {
85 } else if (isRendition(v)) {
42 // renditions are instantiated
86 // renditions are instantiated
43 return v.getDomNode(scope);
87 return v.getDomNode();
44 } else if (isWidget(v)) {
88 } else if (isWidget(v)) {
45 // widgets are converted to it's markup
89 // widgets are converted to it's markup
46 return v.domNode;
90 return v.domNode;
@@ -50,7 +94,7 export const getItemDom = (v: unknown, s
50 } else if (v instanceof Array) {
94 } else if (v instanceof Array) {
51 // arrays will be translated to document fragments
95 // arrays will be translated to document fragments
52 const fragment = document.createDocumentFragment();
96 const fragment = document.createDocumentFragment();
53 v.map(item => getItemDom(item, scope))
97 v.map(item => getItemDom(item))
54 .forEach(node => fragment.appendChild(node));
98 .forEach(node => fragment.appendChild(node));
55 return fragment;
99 return fragment;
56 } else {
100 } else {
@@ -2,7 +2,6 import { IDestroyable } from "@implab/co
2 import { isDestroyable } from "@implab/core-amd/safe";
2 import { isDestroyable } from "@implab/core-amd/safe";
3 import _WidgetBase = require("dijit/_WidgetBase");
3 import _WidgetBase = require("dijit/_WidgetBase");
4 import registry = require("dijit/registry");
4 import registry = require("dijit/registry");
5 import { IScope } from "./Scope";
6
5
7 interface _WidgetBaseConstructor {
6 interface _WidgetBaseConstructor {
8 new <A = {}, E extends { [k in keyof E]: Event } = {}>(params?: Partial<_WidgetBase<E> & A>, srcNodeRef?: dojo.NodeOrString): _WidgetBase<E> & dojo._base.DeclareCreatedObject;
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 export type DojoNodeLocation = [Node, DojoNodePosition];
13 export type DojoNodeLocation = [Node, DojoNodePosition];
15
14
16 export interface Rendition<TNode extends Node = Node> {
15 export interface Rendition<TNode extends Node = Node> {
17 getDomNode(scope?: IScope): TNode;
16 getDomNode(): TNode;
18
17
19 placeAt(refNode: string | Node, position?: DojoNodePosition): void;
18 placeAt(refNode: string | Node, position?: DojoNodePosition): void;
20 }
19 }
@@ -111,7 +110,7 export const destroy = (target: Node | I
111 export const emptyNode = (target: Node) => {
110 export const emptyNode = (target: Node) => {
112 registry.findWidgets(target).forEach(destroy);
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 target.removeChild(c);
114 target.removeChild(c);
116 }
115 }
117 }
116 }
@@ -169,7 +168,7 export const placeAt = (node: Node, refN
169 parent && parent.insertBefore(node, ref.nextSibling);
168 parent && parent.insertBefore(node, ref.nextSibling);
170 break;
169 break;
171 case "first":
170 case "first":
172 ref.insertBefore(node,ref.firstChild);
171 ref.insertBefore(node, ref.firstChild);
173 break;
172 break;
174 case "last":
173 case "last":
175 ref.appendChild(node);
174 ref.appendChild(node);
@@ -75,6 +75,7 declare namespace JSX {
75 }
75 }
76
76
77 interface IntrinsicClassAttributes<T> {
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 id "ivy-publish"
3 id "ivy-publish"
4 }
4 }
5
5
6 def container = "djx-playground"
7
6 configurations {
8 configurations {
7 npmLocal
9 npmLocal
8 }
10 }
@@ -105,4 +107,17 task copyApp(type: Copy) {
105
107
106 task bundle {
108 task bundle {
107 dependsOn copyModules, processResourcesBundle, copyApp
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 } No newline at end of file
123 }
@@ -1,7 +1,8
1 import { djbase, djclass } from "@implab/djx/declare";
1 import { djbase, djclass } from "@implab/djx/declare";
2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
2 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 import { createElement } from "@implab/djx/tsx";
3 import { createElement, watch } from "@implab/djx/tsx";
4 import ProgressBar from "./ProgressBar";
4 import ProgressBar from "./ProgressBar";
5 import Button = require("dijit/form/Button");
5
6
6 const ref = <W extends DjxWidgetBase, K extends keyof W>(target: W, name: K) => (v: W[K]) => target.set(name, v);
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 progressBar?: ProgressBar;
14 progressBar?: ProgressBar;
14
15
16 count = 0;
17
18 showCounter = false;
19
15 render() {
20 render() {
16 return <div>
21 return <div className="tundra">
17 <h2 ref={ref(this, "titleNode")}>Hi!</h2>
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 </div>;
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 import MainWidget from "./MainWidget";
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 const w = new MainWidget();
6 const w = new MainWidget();
4 w.placeAt(document.body); No newline at end of file
7 w.placeAt(document.body);
General Comments 0
You need to be logged in to leave comments. Login now