[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
@@ -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>");
});
});