[REF] asyncroot: create asyncroot component

This component replaces the t-asyncroot directive.
This commit is contained in:
Géry Debongnie
2019-10-25 15:35:10 +02:00
parent caa9ac5cb2
commit 2bed1cfbd1
9 changed files with 253 additions and 437 deletions
+5 -3
View File
@@ -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
<div t-name="ParentComponent">
<SyncChild />
<AsyncChild t-asyncroot="1"/>
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
+23
View File
@@ -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
<div t-name="ParentComponent">
<SyncChild />
<AsyncRoot>
<AsyncChild/>
</AsyncRoot>
</div>
```
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
be a dom node or a component.
+3
View File
@@ -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)
+12 -25
View File
@@ -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;
}
+2
View File
@@ -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,
+19
View File
@@ -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<any, any> {
static template = xml`<t t-slot="default"/>`;
async __updateProps(nextProps, parentFiber) {
this.render(parentFiber.force);
}
}
@@ -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
) {
-166
View File
@@ -2721,172 +2721,6 @@ describe("async rendering", () => {
expect(destroyCount).toBe(0);
});
test("delayed component with t-asyncroot directive", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncChild t-asyncroot="1" val="state.val"/>
</div>
</div>
<span t-name="Child"><t t-esc="props.val"/></span>
</templates>
`);
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("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("fast component with t-asyncroot directive", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child t-asyncroot="1" val="state.val"/>
<AsyncChild val="state.val"/>
</div>
</div>
<span t-name="Child"><t t-esc="props.val"/></span>
</templates>
`);
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("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("t-component with t-asyncroot directive: mixed re-renderings", async () => {
env.qweb.addTemplates(`
<templates>
<div t-name="Parent">
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncChild t-asyncroot="1" val="state.val"/>
</div>
</div>
<span t-name="Child" t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
</span>
</templates>
`);
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("<span>0/0</span><span>0/0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/1</span><span>0/0</span>");
// 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("<span>1/1</span><span>0/0</span>");
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/0</span>");
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
});
test("rendering component again in next microtick", async () => {
class Child extends Widget {}
+189
View File
@@ -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<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
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("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("fast component with AsyncRoot", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<AsyncRoot>
<Child val="state.val"/>
</AsyncRoot>
<AsyncChild val="state.val"/>
</div>
</div>`;
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("<span>0</span><span>0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>0</span>");
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1</span><span>1</span>");
});
test("asyncroot component: mixed re-renderings", async () => {
let def;
class Child extends Component<any, any> {
static template = xml`
<span t-on-click="increment">
<t t-esc="state.val"/>/<t t-esc="props.val"/>
</span>`;
state = useState({ val: 0 });
increment() {
this.state.val++;
}
}
class AsyncChild extends Child {
willUpdateProps() {
return def;
}
}
class Parent extends Component<any, any> {
static template = xml`
<div>
<button t-on-click="updateApp">Update App State</button>
<div class="children">
<Child val="state.val"/>
<AsyncRoot>
<AsyncChild val="state.val"/>
</AsyncRoot>
</div>
</div>`;
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("<span>0/0</span><span>0/0</span>");
// click on button to increment Parent counter
def = makeDeferred();
fixture.querySelector("button")!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>0/1</span><span>0/0</span>");
// 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("<span>1/1</span><span>0/0</span>");
children[1]!.click();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/0</span>");
// finalize first re-rendering (coming from the props update)
def.resolve();
await nextTick();
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
});
});