mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] component: solve a tricky concurrency rendering issue
When components were initially rendered more than once, in consecutive microtask ticks, the order of two actions was reversed: the assignation of the vnode to the __owl__.vnode property, and the assignation of __owl__.vnode to the final rendered vdom of the component. As a result, the widget was rendered as null (so, not present). The reason for this issue was that the renderPromise key was reassigned at some point.
This commit is contained in:
@@ -49,8 +49,13 @@ export interface Meta<T extends Env, Props> {
|
|||||||
cmap: { [key: number]: number };
|
cmap: { [key: number]: number };
|
||||||
|
|
||||||
renderId: number;
|
renderId: number;
|
||||||
|
|
||||||
|
// the renderProps and renderPromise keys are only useful for the "prepare"
|
||||||
|
// step of the lifecycle of a component. Once a component has been rendered
|
||||||
|
// and patched, it is no longer useful.
|
||||||
renderProps: Props | null;
|
renderProps: Props | null;
|
||||||
renderPromise: Promise<VNode> | null;
|
renderPromise: Promise<VNode> | null;
|
||||||
|
|
||||||
boundHandlers: { [key: number]: any };
|
boundHandlers: { [key: number]: any };
|
||||||
observer?: Observer;
|
observer?: Observer;
|
||||||
render?: CompiledTemplate;
|
render?: CompiledTemplate;
|
||||||
@@ -447,7 +452,6 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
|
|
||||||
__patch(vnode) {
|
__patch(vnode) {
|
||||||
const __owl__ = this.__owl__;
|
const __owl__ = this.__owl__;
|
||||||
__owl__.renderPromise = null;
|
|
||||||
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
const target = __owl__.vnode || document.createElement(vnode.sel!);
|
||||||
if (this.__owl__.classObj) {
|
if (this.__owl__.classObj) {
|
||||||
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
|
(<any>vnode).data.class = Object.assign((<any>vnode).data.class || {}, this.__owl__.classObj);
|
||||||
@@ -544,9 +548,7 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
|
|||||||
// parent component. With this, we make sure that the parent component will be
|
// parent component. With this, we make sure that the parent component will be
|
||||||
// able to patch itself properly after
|
// able to patch itself properly after
|
||||||
vnode.key = __owl__.id;
|
vnode.key = __owl__.id;
|
||||||
__owl__.renderProps = this.props;
|
return Promise.all(promises).then(() => vnode);
|
||||||
__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
|
||||||
return __owl__.renderPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2608,6 +2608,39 @@ describe("async rendering", () => {
|
|||||||
|
|
||||||
expect(fixture.querySelector(".children")!.innerHTML).toBe("<span>1/1</span><span>1/1</span>");
|
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 {}
|
||||||
|
|
||||||
|
class App extends Widget {
|
||||||
|
components = { Child };
|
||||||
|
|
||||||
|
async onClick() {
|
||||||
|
(env as any).flag = true;
|
||||||
|
this.render();
|
||||||
|
await Promise.resolve();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env.qweb.addTemplates(`
|
||||||
|
<templates>
|
||||||
|
<div t-name="Child">Child</div>
|
||||||
|
|
||||||
|
<div t-name="App">
|
||||||
|
<button t-on-click="onClick">Click</button>
|
||||||
|
<t t-if="env.flag"><Child/></t>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const app = new App(env);
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>Click</button></div>");
|
||||||
|
fixture.querySelector("button")!.click();
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><button>Click</button><div>Child</div></div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("updating environment", () => {
|
describe("updating environment", () => {
|
||||||
@@ -3839,7 +3872,6 @@ describe("top level sub widgets", () => {
|
|||||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test("sub widget is interactive", async () => {
|
test("sub widget is interactive", async () => {
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
@@ -3849,10 +3881,10 @@ describe("top level sub widgets", () => {
|
|||||||
<span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
|
<span t-name="Child"><button t-on-click="inc">click</button>child<t t-esc="state.val"/></span>
|
||||||
</templates>`);
|
</templates>`);
|
||||||
class Child extends Widget {
|
class Child extends Widget {
|
||||||
state = {val: 1};
|
state = { val: 1 };
|
||||||
inc() {
|
inc() {
|
||||||
this.state.val++;
|
this.state.val++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
components = { Child };
|
components = { Child };
|
||||||
@@ -3860,7 +3892,7 @@ describe("top level sub widgets", () => {
|
|||||||
const parent = new Parent(env);
|
const parent = new Parent(env);
|
||||||
await parent.mount(fixture);
|
await parent.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<span><button>click</button>child1</span>");
|
expect(fixture.innerHTML).toBe("<span><button>click</button>child1</span>");
|
||||||
const button = fixture.querySelector('button')!;
|
const button = fixture.querySelector("button")!;
|
||||||
button.click();
|
button.click();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<span><button>click</button>child2</span>");
|
expect(fixture.innerHTML).toBe("<span><button>click</button>child2</span>");
|
||||||
@@ -3907,7 +3939,7 @@ describe("top level sub widgets", () => {
|
|||||||
class Child extends Widget {}
|
class Child extends Widget {}
|
||||||
class OtherChild extends Widget {}
|
class OtherChild extends Widget {}
|
||||||
class Parent extends Widget {
|
class Parent extends Widget {
|
||||||
state = {flag: true}
|
state = { flag: true };
|
||||||
components = { Child, OtherChild };
|
components = { Child, OtherChild };
|
||||||
}
|
}
|
||||||
let parent = new Parent(env);
|
let parent = new Parent(env);
|
||||||
@@ -3916,7 +3948,5 @@ describe("top level sub widgets", () => {
|
|||||||
parent.state.flag = false;
|
parent.state.flag = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
|
expect(fixture.innerHTML).toBe("<div>CHILD 2</div>");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user