From 2bed1cfbd1563e03e4faadee8dae709405628909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 25 Oct 2019 15:35:10 +0200 Subject: [PATCH] [REF] asyncroot: create asyncroot component This component replaces the t-asyncroot directive. --- doc/component.md | 8 +- doc/misc.md | 23 ++ doc/readme.md | 3 + src/component/directive.ts | 37 +-- src/index.ts | 2 + src/misc/async_root.ts | 19 ++ .../__snapshots__/component.test.ts.snap | 243 ------------------ tests/component/component.test.ts | 166 ------------ tests/misc/async_root.test.ts | 189 ++++++++++++++ 9 files changed, 253 insertions(+), 437 deletions(-) create mode 100644 doc/misc.md create mode 100644 src/misc/async_root.ts create mode 100644 tests/misc/async_root.test.ts diff --git a/doc/component.md b/doc/component.md index fd7ecc94..ee728f30 100644 --- a/doc/component.md +++ b/doc/component.md @@ -1164,8 +1164,8 @@ Here are a few tips on how to work with asynchronous components: 3. Lazy loading external libraries is a good use case for async rendering. This is mostly fine, because we can assume that it will only takes a fraction of a second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs)) -4. For all the other cases, the `t-asyncroot` directive (to use alongside - `t-component`) is there to help you. When this directive is met, a new rendering +4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When + this component is met, a new rendering sub tree is created, such that the rendering of that component (and its children) is not tied to the rendering of the rest of the interface. It can be used on an asynchronous component, to prevent it from delaying the @@ -1177,7 +1177,9 @@ Here are a few tips on how to work with asynchronous components: ```xml
- + + +
``` diff --git a/doc/misc.md b/doc/misc.md new file mode 100644 index 00000000..5a8c1a17 --- /dev/null +++ b/doc/misc.md @@ -0,0 +1,23 @@ +# 🦉 Miscellaneous 🦉 + +## `AsyncRoot` + +When this component is used, a new rendering sub tree is created, such that the +rendering of that component (and its children) is not tied to the rendering of +the rest of the interface. It can be used on an asynchronous component, to +prevent it from delaying the rendering of the whole interface, or on a +synchronous one, such that its rendering isn't delayed by other (asynchronous) +components. Note that this directive has no effect on the first rendering, but +only on subsequent ones (triggered by state or props changes). + +```xml +
+ + + + +
+``` + +The `AsyncRoot` assumes that there is exactly one root node inside it. It can +be a dom node or a component. diff --git a/doc/readme.md b/doc/readme.md index 9d2d75a4..e79f3942 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -29,6 +29,8 @@ owl useStore useDispatch useGetters + misc + AsyncRoot router Link RouteComponent @@ -53,6 +55,7 @@ Note that for convenience, the `useState` hook is also exported at the root of t - [Context](context.md) - [Event Bus](event_bus.md) - [Hooks](hooks.md) +- [Misc](misc.md) - [Observer](observer.md) - [QWeb](qweb.md) - [Router](router.md) diff --git a/src/component/directive.ts b/src/component/directive.ts index 49c2717e..c4cd0f2b 100644 --- a/src/component/directive.ts +++ b/src/component/directive.ts @@ -186,7 +186,7 @@ QWeb.utils.defineProxy = function defineProxy(target, source) { QWeb.addDirective({ name: "component", - extraNames: ["props", "keepalive", "asyncroot"], + extraNames: ["props", "keepalive"], priority: 100, atNodeEncounter({ ctx, value, node, qweb }): boolean { ctx.addLine("//COMPONENT"); @@ -196,7 +196,6 @@ QWeb.addDirective({ ctx.rootContext.shouldDefineUtils = true; let keepAlive = node.getAttribute("t-keepalive") ? true : false; let hasDynamicProps = node.getAttribute("t-props") ? true : false; - let async = node.getAttribute("t-asyncroot") ? true : false; // t-on- events and t-transition const events: [string, string[], string, string][] = []; @@ -355,26 +354,20 @@ QWeb.addDirective({ ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); } let shouldProxy = false; - if (async || keepAlive) { + if (keepAlive) { ctx.addLine( `const fiber${componentID} = Object.assign(Object.create(extra.fiber), {patchQueue: []});` ); } - if (async) { - ctx.addLine( - `c${ctx.parentNode}.push(w${componentID} && w${componentID}.__owl__.pvnode || null);` - ); + if (ctx.parentNode) { + ctx.addLine(`c${ctx.parentNode}.push(null);`); } else { - if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push(null);`); - } else { - let id = ctx.generateID(); - ctx.rootContext.rootNode = id; - shouldProxy = true; - ctx.rootContext.shouldDefineResult = true; - ctx.addLine(`let vn${id} = {};`); - ctx.addLine(`result = vn${id};`); - } + let id = ctx.generateID(); + ctx.rootContext.rootNode = id; + shouldProxy = true; + ctx.rootContext.shouldDefineResult = true; + ctx.addLine(`let vn${id} = {};`); + ctx.addLine(`result = vn${id};`); } if (hasDynamicProps) { const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!); @@ -469,7 +462,7 @@ QWeb.addDirective({ ctx.addElse(); // need to update component - let patchQueueCode = async || keepAlive ? `fiber${componentID}` : "extra.fiber"; + let patchQueueCode = keepAlive ? `fiber${componentID}` : "extra.fiber"; if (keepAlive) { // if we have t-keepalive="1", the component could be unmounted, but then // we __updateProps is called. This is ok, but we do not want to call @@ -499,13 +492,7 @@ QWeb.addDirective({ ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`); } - if (async) { - ctx.addLine( - `def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, fiber${componentID}));` - ); - } else { - ctx.addLine(`extra.promises.push(def${defID});`); - } + ctx.addLine(`extra.promises.push(def${defID});`); return true; } diff --git a/src/index.ts b/src/index.ts index fe11375a..93722273 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { QWeb } from "./qweb/index"; import * as _store from "./store"; import * as _utils from "./utils"; import * as _tags from "./tags"; +import {AsyncRoot} from "./misc/async_root"; import * as _hooks from "./hooks"; import * as _context from "./context"; import { Link } from "./router/link"; @@ -26,6 +27,7 @@ export const router = { Router, RouteComponent, Link }; export const Store = _store.Store; export const utils = _utils; export const tags = _tags; +export const misc = { AsyncRoot}; export const hooks = Object.assign({}, _hooks, { useContext: _context.useContext, useDispatch: _store.useDispatch, diff --git a/src/misc/async_root.ts b/src/misc/async_root.ts new file mode 100644 index 00000000..604f582f --- /dev/null +++ b/src/misc/async_root.ts @@ -0,0 +1,19 @@ +import { Component } from "../component/component"; +import { xml } from "../tags"; + +/** + * AsyncRoot + * + * Owl is by default asynchronous, and the user interface will wait for all its + * subcomponents to be rendered before updating the DOM. This is most of the + * time what we want, but in some cases, it makes sense to "detach" a component + * from this coordination. This is the goal of the AsyncRoot component. + */ + +export class AsyncRoot extends Component { + static template = xml``; + + async __updateProps(nextProps, parentFiber) { + this.render(parentFiber.force); + } +} diff --git a/tests/component/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap index c4fd2348..512c6664 100644 --- a/tests/component/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -1,248 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`async rendering delayed component with t-asyncroot directive 1`] = ` -"function anonymous(context,extra -) { - let utils = this.constructor.utils; - let QWeb = this.constructor; - let parent = context; - let owner = context; - var h = this.h; - let c1 = [], p1 = {key:1}; - var vn1 = h('div', p1, c1); - let c2 = [], p2 = {key:2,on:{}}; - var vn2 = h('button', p2, c2); - c1.push(vn2); - extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {const fn = context['updateApp'];if (fn) { fn.call(owner, e); } else { context.updateApp; }}; - p2.on['click'] = extra.handlers['click' + 2]; - c2.push({text: \`Update App State\`}); - let _4 = {'children':true}; - let c5 = [], p5 = {key:5,class:_4}; - var vn5 = h('div', p5, c5); - c1.push(vn5); - //COMPONENT - let def7; - let templateId8 = \`__9__\`; - let w8 = templateId8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId8]] : false; - let _6_index = c5.length; - c5.push(null); - let props8 = {val:context['state'].val}; - if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) { - if (utils.shallowEqual(props8, w8.__owl__.currentFiber.props)) { - def7 = w8.__owl__.currentFiber.promise; - } else { - w8.destroy(); - w8 = false; - } - } - if (!w8) { - let componentKey8 = \`Child\`; - let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['Child']; - if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} - w8 = new W8(parent, props8); - parent.__owl__.cmap[templateId8] = w8.__owl__.id; - def7 = w8.__prepare(extra.fiber, undefined, undefined); - def7 = def7.then(vnode=>{if (w8.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;}); - } else { - def7 = def7 || w8.__updateProps(props8, extra.fiber, undefined, undefined); - def7 = def7.then(()=>{if (w8.__owl__.isDestroyed) {return};let pvnode=w8.__owl__.pvnode;c5[_6_index]=pvnode;}); - } - extra.promises.push(def7); - //COMPONENT - let def11; - let templateId12 = \`__13__\`; - let w12 = templateId12 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId12]] : false; - let _10_index = c5.length; - const fiber12 = Object.assign(Object.create(extra.fiber), {patchQueue: []}); - c5.push(w12 && w12.__owl__.pvnode || null); - let props12 = {val:context['state'].val}; - if (w12 && w12.__owl__.currentFiber && !w12.__owl__.vnode) { - if (utils.shallowEqual(props12, w12.__owl__.currentFiber.props)) { - def11 = w12.__owl__.currentFiber.promise; - } else { - w12.destroy(); - w12 = false; - } - } - if (!w12) { - let componentKey12 = \`AsyncChild\`; - let W12 = context.constructor.components[componentKey12] || QWeb.components[componentKey12]|| context['AsyncChild']; - if (!W12) {throw new Error('Cannot find the definition of component \\"' + componentKey12 + '\\"')} - w12 = new W12(parent, props12); - parent.__owl__.cmap[templateId12] = w12.__owl__.id; - def11 = w12.__prepare(extra.fiber, undefined, undefined); - def11 = def11.then(vnode=>{if (w12.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId12, hook: {insert(vn) {let nvn=w12.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w12.destroy();}}});c5[_10_index]=pvnode;w12.__owl__.pvnode = pvnode;}); - } else { - def11 = def11 || w12.__updateProps(props12, fiber12, undefined, undefined); - def11 = def11.then(()=>{if (w12.__owl__.isDestroyed) {return};let pvnode=w12.__owl__.pvnode;c5[_10_index]=pvnode;}); - } - def11.then(w12.__applyPatchQueue.bind(w12, fiber12)); - return vn1; -}" -`; - -exports[`async rendering fast component with t-asyncroot directive 1`] = ` -"function anonymous(context,extra -) { - let utils = this.constructor.utils; - let QWeb = this.constructor; - let parent = context; - let owner = context; - var h = this.h; - let c1 = [], p1 = {key:1}; - var vn1 = h('div', p1, c1); - let c2 = [], p2 = {key:2,on:{}}; - var vn2 = h('button', p2, c2); - c1.push(vn2); - extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {const fn = context['updateApp'];if (fn) { fn.call(owner, e); } else { context.updateApp; }}; - p2.on['click'] = extra.handlers['click' + 2]; - c2.push({text: \`Update App State\`}); - let _4 = {'children':true}; - let c5 = [], p5 = {key:5,class:_4}; - var vn5 = h('div', p5, c5); - c1.push(vn5); - //COMPONENT - let def7; - let templateId8 = \`__9__\`; - let w8 = templateId8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId8]] : false; - let _6_index = c5.length; - const fiber8 = Object.assign(Object.create(extra.fiber), {patchQueue: []}); - c5.push(w8 && w8.__owl__.pvnode || null); - let props8 = {val:context['state'].val}; - if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) { - if (utils.shallowEqual(props8, w8.__owl__.currentFiber.props)) { - def7 = w8.__owl__.currentFiber.promise; - } else { - w8.destroy(); - w8 = false; - } - } - if (!w8) { - let componentKey8 = \`Child\`; - let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['Child']; - if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} - w8 = new W8(parent, props8); - parent.__owl__.cmap[templateId8] = w8.__owl__.id; - def7 = w8.__prepare(extra.fiber, undefined, undefined); - def7 = def7.then(vnode=>{if (w8.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;}); - } else { - def7 = def7 || w8.__updateProps(props8, fiber8, undefined, undefined); - def7 = def7.then(()=>{if (w8.__owl__.isDestroyed) {return};let pvnode=w8.__owl__.pvnode;c5[_6_index]=pvnode;}); - } - def7.then(w8.__applyPatchQueue.bind(w8, fiber8)); - //COMPONENT - let def11; - let templateId12 = \`__13__\`; - let w12 = templateId12 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId12]] : false; - let _10_index = c5.length; - c5.push(null); - let props12 = {val:context['state'].val}; - if (w12 && w12.__owl__.currentFiber && !w12.__owl__.vnode) { - if (utils.shallowEqual(props12, w12.__owl__.currentFiber.props)) { - def11 = w12.__owl__.currentFiber.promise; - } else { - w12.destroy(); - w12 = false; - } - } - if (!w12) { - let componentKey12 = \`AsyncChild\`; - let W12 = context.constructor.components[componentKey12] || QWeb.components[componentKey12]|| context['AsyncChild']; - if (!W12) {throw new Error('Cannot find the definition of component \\"' + componentKey12 + '\\"')} - w12 = new W12(parent, props12); - parent.__owl__.cmap[templateId12] = w12.__owl__.id; - def11 = w12.__prepare(extra.fiber, undefined, undefined); - def11 = def11.then(vnode=>{if (w12.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId12, hook: {insert(vn) {let nvn=w12.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w12.destroy();}}});c5[_10_index]=pvnode;w12.__owl__.pvnode = pvnode;}); - } else { - def11 = def11 || w12.__updateProps(props12, extra.fiber, undefined, undefined); - def11 = def11.then(()=>{if (w12.__owl__.isDestroyed) {return};let pvnode=w12.__owl__.pvnode;c5[_10_index]=pvnode;}); - } - extra.promises.push(def11); - return vn1; -}" -`; - -exports[`async rendering t-component with t-asyncroot directive: mixed re-renderings 1`] = ` -"function anonymous(context,extra -) { - let utils = this.constructor.utils; - let QWeb = this.constructor; - let parent = context; - let owner = context; - var h = this.h; - let c1 = [], p1 = {key:1}; - var vn1 = h('div', p1, c1); - let c2 = [], p2 = {key:2,on:{}}; - var vn2 = h('button', p2, c2); - c1.push(vn2); - extra.handlers['click' + 2] = extra.handlers['click' + 2] || function (e) {const fn = context['updateApp'];if (fn) { fn.call(owner, e); } else { context.updateApp; }}; - p2.on['click'] = extra.handlers['click' + 2]; - c2.push({text: \`Update App State\`}); - let _4 = {'children':true}; - let c5 = [], p5 = {key:5,class:_4}; - var vn5 = h('div', p5, c5); - c1.push(vn5); - //COMPONENT - let def7; - let templateId8 = \`__9__\`; - let w8 = templateId8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId8]] : false; - let _6_index = c5.length; - c5.push(null); - let props8 = {val:context['state'].val}; - if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) { - if (utils.shallowEqual(props8, w8.__owl__.currentFiber.props)) { - def7 = w8.__owl__.currentFiber.promise; - } else { - w8.destroy(); - w8 = false; - } - } - if (!w8) { - let componentKey8 = \`Child\`; - let W8 = context.constructor.components[componentKey8] || QWeb.components[componentKey8]|| context['Child']; - if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')} - w8 = new W8(parent, props8); - parent.__owl__.cmap[templateId8] = w8.__owl__.id; - def7 = w8.__prepare(extra.fiber, undefined, undefined); - def7 = def7.then(vnode=>{if (w8.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId8, hook: {insert(vn) {let nvn=w8.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w8.destroy();}}});c5[_6_index]=pvnode;w8.__owl__.pvnode = pvnode;}); - } else { - def7 = def7 || w8.__updateProps(props8, extra.fiber, undefined, undefined); - def7 = def7.then(()=>{if (w8.__owl__.isDestroyed) {return};let pvnode=w8.__owl__.pvnode;c5[_6_index]=pvnode;}); - } - extra.promises.push(def7); - //COMPONENT - let def11; - let templateId12 = \`__13__\`; - let w12 = templateId12 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[templateId12]] : false; - let _10_index = c5.length; - const fiber12 = Object.assign(Object.create(extra.fiber), {patchQueue: []}); - c5.push(w12 && w12.__owl__.pvnode || null); - let props12 = {val:context['state'].val}; - if (w12 && w12.__owl__.currentFiber && !w12.__owl__.vnode) { - if (utils.shallowEqual(props12, w12.__owl__.currentFiber.props)) { - def11 = w12.__owl__.currentFiber.promise; - } else { - w12.destroy(); - w12 = false; - } - } - if (!w12) { - let componentKey12 = \`AsyncChild\`; - let W12 = context.constructor.components[componentKey12] || QWeb.components[componentKey12]|| context['AsyncChild']; - if (!W12) {throw new Error('Cannot find the definition of component \\"' + componentKey12 + '\\"')} - w12 = new W12(parent, props12); - parent.__owl__.cmap[templateId12] = w12.__owl__.id; - def11 = w12.__prepare(extra.fiber, undefined, undefined); - def11 = def11.then(vnode=>{if (w12.__owl__.isDestroyed){return}let pvnode=h(vnode.sel, {key: templateId12, hook: {insert(vn) {let nvn=w12.__mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w12.destroy();}}});c5[_10_index]=pvnode;w12.__owl__.pvnode = pvnode;}); - } else { - def11 = def11 || w12.__updateProps(props12, fiber12, undefined, undefined); - def11 = def11.then(()=>{if (w12.__owl__.isDestroyed) {return};let pvnode=w12.__owl__.pvnode;c5[_10_index]=pvnode;}); - } - def11.then(w12.__applyPatchQueue.bind(w12, fiber12)); - return vn1; -}" -`; - exports[`basic widget properties reconciliation alg works for t-foreach in t-foreach 1`] = ` "function anonymous(context,extra ) { diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index e6200bce..6bba1ec1 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -2721,172 +2721,6 @@ describe("async rendering", () => { expect(destroyCount).toBe(0); }); - test("delayed component with t-asyncroot directive", async () => { - env.qweb.addTemplates(` - -
- -
- - -
-
- -
- `); - - let def; - class Child extends Widget {} - class AsyncChild extends Child { - willUpdateProps() { - return def; - } - } - class Parent extends Widget { - static components = { Child, AsyncChild }; - state = useState({ val: 0 }); - - updateApp() { - this.state.val++; - } - } - - const parent = new Parent(env); - await parent.mount(fixture); - - expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); - expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); - - // click on button to increment Parent counter - def = makeDeferred(); - fixture.querySelector("button")!.click(); - await nextTick(); - - expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); - - def.resolve(); - await nextTick(); - - expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); - }); - - test("fast component with t-asyncroot directive", async () => { - env.qweb.addTemplates(` - -
- -
- - -
-
- -
- `); - - let def; - class Child extends Widget {} - class AsyncChild extends Child { - willUpdateProps() { - return def; - } - } - class Parent extends Widget { - static components = { Child, AsyncChild }; - state = useState({ val: 0 }); - - updateApp() { - this.state.val++; - } - } - - const parent = new Parent(env); - await parent.mount(fixture); - - expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); - expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); - - // click on button to increment Parent counter - def = makeDeferred(); - fixture.querySelector("button")!.click(); - await nextTick(); - - expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); - - def.resolve(); - await nextTick(); - - expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); - }); - - test("t-component with t-asyncroot directive: mixed re-renderings", async () => { - env.qweb.addTemplates(` - -
- -
- - -
-
- - / - -
- `); - - let def; - class Child extends Widget { - state = useState({ val: 0 }); - - increment() { - this.state.val++; - } - } - class AsyncChild extends Child { - willUpdateProps() { - return def; - } - } - class Parent extends Widget { - static components = { Child, AsyncChild }; - state = useState({ val: 0 }); - - updateApp() { - this.state.val++; - } - } - - const parent = new Parent(env); - await parent.mount(fixture); - - expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); - expect(fixture.querySelector(".children")!.innerHTML).toBe("0/00/0"); - - // click on button to increment Parent counter - def = makeDeferred(); - fixture.querySelector("button")!.click(); - await nextTick(); - - expect(fixture.querySelector(".children")!.innerHTML).toBe("0/10/0"); - - // click on each Child to increment their local counter - const children = parent.el!.querySelectorAll("span"); - children[0]!.click(); - await nextTick(); - expect(fixture.querySelector(".children")!.innerHTML).toBe("1/10/0"); - - children[1]!.click(); - await nextTick(); - expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/0"); - - // finalize first re-rendering (coming from the props update) - def.resolve(); - await nextTick(); - - expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/1"); - }); - test("rendering component again in next microtick", async () => { class Child extends Widget {} diff --git a/tests/misc/async_root.test.ts b/tests/misc/async_root.test.ts new file mode 100644 index 00000000..1de43e6a --- /dev/null +++ b/tests/misc/async_root.test.ts @@ -0,0 +1,189 @@ +import { AsyncRoot } from "../../src/misc/async_root"; +import { useState } from "../../src/hooks"; +import { xml } from "../../src/tags"; +import { makeDeferred, makeTestFixture, makeTestEnv, nextTick } from "../helpers"; +import { Env, Component } from "../../src/component/component"; + +//------------------------------------------------------------------------------ +// Setup and helpers +//------------------------------------------------------------------------------ + +// We create before each test: +// - fixture: a div, appended to the DOM, intended to be the target of dom +// manipulations. Note that it is removed after each test. +// - env: a WEnv, necessary to create new components + +let fixture: HTMLElement; +let env: Env; + +beforeEach(() => { + fixture = makeTestFixture(); + env = makeTestEnv(); +}); + +afterEach(() => { + fixture.remove(); +}); + +describe("Asyncroot", () => { + test("delayed component with AsyncRoot component", async () => { + let def; + class Child extends Component { + static template = xml``; + } + class AsyncChild extends Child { + willUpdateProps() { + return def; + } + } + class Parent extends Component { + static template = xml` +
+ +
+ + + + +
+
`; + static components = { Child, AsyncChild, AsyncRoot }; + state = useState({ val: 0 }); + + updateApp() { + this.state.val++; + } + } + + const parent = new Parent(env); + await parent.mount(fixture); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); + + // click on button to increment Parent counter + def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); + + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); + }); + + test("fast component with AsyncRoot", async () => { + let def; + class Child extends Component { + static template = xml``; + } + class AsyncChild extends Child { + willUpdateProps() { + return def; + } + } + + class Parent extends Component { + static template = xml` +
+ +
+ + + + +
+
`; + static components = { Child, AsyncChild, AsyncRoot }; + state = useState({ val: 0 }); + + updateApp() { + this.state.val++; + } + } + + const parent = new Parent(env); + await parent.mount(fixture); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("00"); + + // click on button to increment Parent counter + def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("10"); + + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("11"); + }); + + test("asyncroot component: mixed re-renderings", async () => { + let def; + class Child extends Component { + static template = xml` + + / + `; + state = useState({ val: 0 }); + + increment() { + this.state.val++; + } + } + class AsyncChild extends Child { + willUpdateProps() { + return def; + } + } + class Parent extends Component { + static template = xml` +
+ +
+ + + + +
+
`; + static components = { Child, AsyncChild, AsyncRoot }; + state = useState({ val: 0 }); + + updateApp() { + this.state.val++; + } + } + + const parent = new Parent(env); + await parent.mount(fixture); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("0/00/0"); + + // click on button to increment Parent counter + def = makeDeferred(); + fixture.querySelector("button")!.click(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("0/10/0"); + + // click on each Child to increment their local counter + const children = parent.el!.querySelectorAll("span"); + children[0]!.click(); + await nextTick(); + expect(fixture.querySelector(".children")!.innerHTML).toBe("1/10/0"); + + children[1]!.click(); + await nextTick(); + expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/0"); + + // finalize first re-rendering (coming from the props update) + def.resolve(); + await nextTick(); + + expect(fixture.querySelector(".children")!.innerHTML).toBe("1/11/1"); + }); +});