mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] component: call willPatch/patched hooks properly
Each component called its hooks properly individually, however the issue was that it was done component by component. However, in practice, we want the willPatch hooks to be done before ANY patch is actually performed, and the patched should be called AFTER every patch is applied. The trick is to propagate a list of patched components across various function calls. closes #64
This commit is contained in:
+29
-13
@@ -236,17 +236,33 @@ export class Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async render(force: boolean = false): Promise<void> {
|
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
|
||||||
if (this.__owl__.isDestroyed) {
|
if (this.__owl__.isDestroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const renderVDom = this._render(force);
|
const shouldCallPatchHooks: boolean = !patchQueue;
|
||||||
|
if (shouldCallPatchHooks) {
|
||||||
|
patchQueue = [];
|
||||||
|
}
|
||||||
|
const renderVDom = this._render(force, patchQueue);
|
||||||
const renderId = this.__owl__.renderId;
|
const renderId = this.__owl__.renderId;
|
||||||
const vnode = await renderVDom;
|
const vnode = await renderVDom;
|
||||||
if (renderId === this.__owl__.renderId) {
|
if (renderId === this.__owl__.renderId) {
|
||||||
// we only update the vnode and the actual DOM if no other rendering
|
// we only update the vnode and the actual DOM if no other rendering
|
||||||
// occurred between now and when the render method was initially called.
|
// occurred between now and when the render method was initially called.
|
||||||
|
if (shouldCallPatchHooks) {
|
||||||
|
for (let i = 0; i < patchQueue!.length; i++) {
|
||||||
|
const c = patchQueue![i];
|
||||||
|
c.__owl__.willPatchVal = c.willPatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
this._patch(vnode);
|
this._patch(vnode);
|
||||||
|
if (shouldCallPatchHooks) {
|
||||||
|
for (let i = patchQueue!.length - 1; i >= 0; i--) {
|
||||||
|
const c = patchQueue![i];
|
||||||
|
c.patched(c.__owl__.willPatchVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,10 +321,11 @@ export class Component<
|
|||||||
|
|
||||||
async updateProps(
|
async updateProps(
|
||||||
nextProps: Props,
|
nextProps: Props,
|
||||||
forceUpdate: boolean = false
|
forceUpdate: boolean = false,
|
||||||
|
patchQueue: any[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||||
return shouldUpdate ? this._updateProps(nextProps) : Promise.resolve();
|
return shouldUpdate ? this._updateProps(nextProps, patchQueue) : Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
set(target: any, key: string | number, value: any) {
|
set(target: any, key: string | number, value: any) {
|
||||||
@@ -319,21 +336,16 @@ export class Component<
|
|||||||
// Private
|
// Private
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
async _updateProps(nextProps: Props): Promise<void> {
|
async _updateProps(nextProps: Props, patchQueue: any[]): Promise<void> {
|
||||||
await this.willUpdateProps(nextProps);
|
await this.willUpdateProps(nextProps);
|
||||||
this.props = nextProps;
|
this.props = nextProps;
|
||||||
await this.render();
|
await this.render(false, patchQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
_patch(vnode) {
|
_patch(vnode) {
|
||||||
this.__owl__.renderPromise = null;
|
this.__owl__.renderPromise = null;
|
||||||
if (this.__owl__.vnode) {
|
if (this.__owl__.vnode) {
|
||||||
const isMounted = this.__owl__.isMounted;
|
|
||||||
const snapshot = isMounted && this.willPatch();
|
|
||||||
this.__owl__.vnode = patch(this.__owl__.vnode, vnode);
|
this.__owl__.vnode = patch(this.__owl__.vnode, vnode);
|
||||||
if (isMounted) {
|
|
||||||
this.patched(snapshot);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode);
|
this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode);
|
||||||
}
|
}
|
||||||
@@ -358,7 +370,10 @@ export class Component<
|
|||||||
return this.__owl__.renderPromise;
|
return this.__owl__.renderPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _render(force: boolean = false): Promise<VNode> {
|
async _render(force: boolean = false, patchQueue: any[] = []): Promise<VNode> {
|
||||||
|
if (this.__owl__.isMounted) {
|
||||||
|
patchQueue.push(this)
|
||||||
|
}
|
||||||
this.__owl__.renderId++;
|
this.__owl__.renderId++;
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
const template = this.inlineTemplate || this.template;
|
const template = this.inlineTemplate || this.template;
|
||||||
@@ -368,7 +383,8 @@ export class Component<
|
|||||||
let vnode = this.env.qweb.render(template, this, {
|
let vnode = this.env.qweb.render(template, this, {
|
||||||
promises,
|
promises,
|
||||||
handlers: this.__owl__.boundHandlers,
|
handlers: this.__owl__.boundHandlers,
|
||||||
forceUpdate: force
|
forceUpdate: force,
|
||||||
|
patchQueue,
|
||||||
});
|
});
|
||||||
if (this.__owl__.observer) {
|
if (this.__owl__.observer) {
|
||||||
this.__owl__.observer.allowMutations = true;
|
this.__owl__.observer.allowMutations = true;
|
||||||
|
|||||||
+2
-2
@@ -1060,7 +1060,7 @@ const widgetDirective: Directive = {
|
|||||||
ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise`);
|
ctx.addIf(`w${widgetID} && w${widgetID}.__owl__.renderPromise`);
|
||||||
ctx.addIf(`w${widgetID}.__owl__.isStarted`);
|
ctx.addIf(`w${widgetID}.__owl__.isStarted`);
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`
|
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
|
||||||
);
|
);
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
ctx.addLine(`isNew${widgetID} = true`);
|
ctx.addLine(`isNew${widgetID} = true`);
|
||||||
@@ -1076,7 +1076,7 @@ const widgetDirective: Directive = {
|
|||||||
ctx.addIf(`!def${defID}`);
|
ctx.addIf(`!def${defID}`);
|
||||||
ctx.addIf(`w${widgetID}`);
|
ctx.addIf(`w${widgetID}`);
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate);`
|
`def${defID} = w${widgetID}.updateProps(props${widgetID}, extra.forceUpdate, extra.patchQueue);`
|
||||||
);
|
);
|
||||||
ctx.addElse();
|
ctx.addElse();
|
||||||
ctx.addLine(
|
ctx.addLine(
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
|||||||
let isNew7 = !w7;
|
let isNew7 = !w7;
|
||||||
if (w7 && w7.__owl__.renderPromise) {
|
if (w7 && w7.__owl__.renderPromise) {
|
||||||
if (w7.__owl__.isStarted) {
|
if (w7.__owl__.isStarted) {
|
||||||
def6 = w7.updateProps(props7, extra.forceUpdate);
|
def6 = w7.updateProps(props7, extra.forceUpdate, extra.patchQueue);
|
||||||
} else {
|
} else {
|
||||||
isNew7 = true
|
isNew7 = true
|
||||||
if (props7 === w7.__owl__.renderProps) {
|
if (props7 === w7.__owl__.renderProps) {
|
||||||
@@ -43,7 +43,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
|
|||||||
}
|
}
|
||||||
if (!def6) {
|
if (!def6) {
|
||||||
if (w7) {
|
if (w7) {
|
||||||
def6 = w7.updateProps(props7, extra.forceUpdate);
|
def6 = w7.updateProps(props7, extra.forceUpdate, extra.patchQueue);
|
||||||
} else {
|
} else {
|
||||||
w7 = new context.widgets['ChildWidget'](owner, props7);
|
w7 = new context.widgets['ChildWidget'](owner, props7);
|
||||||
context.__owl__.cmap[key8] = w7.__owl__.id;
|
context.__owl__.cmap[key8] = w7.__owl__.id;
|
||||||
@@ -78,7 +78,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
|||||||
let isNew4 = !w4;
|
let isNew4 = !w4;
|
||||||
if (w4 && w4.__owl__.renderPromise) {
|
if (w4 && w4.__owl__.renderPromise) {
|
||||||
if (w4.__owl__.isStarted) {
|
if (w4.__owl__.isStarted) {
|
||||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
def3 = w4.updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
} else {
|
} else {
|
||||||
isNew4 = true
|
isNew4 = true
|
||||||
if (props4 === w4.__owl__.renderProps) {
|
if (props4 === w4.__owl__.renderProps) {
|
||||||
@@ -91,7 +91,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
|
|||||||
}
|
}
|
||||||
if (!def3) {
|
if (!def3) {
|
||||||
if (w4) {
|
if (w4) {
|
||||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
def3 = w4.updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
} else {
|
} else {
|
||||||
w4 = new context.widgets['child'](owner, props4);
|
w4 = new context.widgets['child'](owner, props4);
|
||||||
context.__owl__.cmap[key5] = w4.__owl__.id;
|
context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||||
@@ -124,7 +124,7 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
|
|||||||
let isNew4 = !w4;
|
let isNew4 = !w4;
|
||||||
if (w4 && w4.__owl__.renderPromise) {
|
if (w4 && w4.__owl__.renderPromise) {
|
||||||
if (w4.__owl__.isStarted) {
|
if (w4.__owl__.isStarted) {
|
||||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
def3 = w4.updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
} else {
|
} else {
|
||||||
isNew4 = true
|
isNew4 = true
|
||||||
if (props4 === w4.__owl__.renderProps) {
|
if (props4 === w4.__owl__.renderProps) {
|
||||||
@@ -137,7 +137,7 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
|
|||||||
}
|
}
|
||||||
if (!def3) {
|
if (!def3) {
|
||||||
if (w4) {
|
if (w4) {
|
||||||
def3 = w4.updateProps(props4, extra.forceUpdate);
|
def3 = w4.updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
} else {
|
} else {
|
||||||
w4 = new context.widgets['child'](owner, props4);
|
w4 = new context.widgets['child'](owner, props4);
|
||||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
|
|||||||
+85
-16
@@ -264,6 +264,55 @@ describe("lifecycle hooks", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("willPatch, patched hook are called on subsubwidgets, in proper order", async () => {
|
||||||
|
const steps: any[] = [];
|
||||||
|
|
||||||
|
class ParentWidget extends Widget {
|
||||||
|
inlineTemplate = `<div>><t t-widget="child" t-props="{n:state.n}"/></div>`;
|
||||||
|
widgets = { child: ChildWidget };
|
||||||
|
state = { n: 1 };
|
||||||
|
willPatch() {
|
||||||
|
steps.push("parent:willPatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("parent:patched");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ChildWidget extends Widget {
|
||||||
|
inlineTemplate = `<div><t t-widget="childchild" t-props="{n:props.n}"/></div>`;
|
||||||
|
widgets = { childchild: ChildChildWidget };
|
||||||
|
willPatch() {
|
||||||
|
steps.push("child:willPatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("child:patched");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ChildChildWidget extends Widget {
|
||||||
|
inlineTemplate = `<div><t t-esc="props.n"/></div>`;
|
||||||
|
willPatch() {
|
||||||
|
steps.push("childchild:willPatch");
|
||||||
|
}
|
||||||
|
patched() {
|
||||||
|
steps.push("childchild:patched");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const widget = new ParentWidget(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
expect(steps).toEqual([]);
|
||||||
|
widget.state.n = 2;
|
||||||
|
await nextTick();
|
||||||
|
widget.destroy();
|
||||||
|
expect(steps).toEqual([
|
||||||
|
"parent:willPatch",
|
||||||
|
"child:willPatch",
|
||||||
|
"childchild:willPatch",
|
||||||
|
"childchild:patched",
|
||||||
|
"child:patched",
|
||||||
|
"parent:patched"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
|
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
|
||||||
const steps: string[] = [];
|
const steps: string[] = [];
|
||||||
|
|
||||||
@@ -450,6 +499,12 @@ describe("lifecycle hooks", () => {
|
|||||||
|
|
||||||
test("willUpdateProps hook is called", async () => {
|
test("willUpdateProps hook is called", async () => {
|
||||||
let def = makeDeferred();
|
let def = makeDeferred();
|
||||||
|
class Parent extends Widget {
|
||||||
|
inlineTemplate =
|
||||||
|
'<span><t t-widget="Child" t-props="{n: state.n}"/></span>';
|
||||||
|
state = { n: 1 };
|
||||||
|
widgets = { Child: HookWidget };
|
||||||
|
}
|
||||||
class HookWidget extends Widget {
|
class HookWidget extends Widget {
|
||||||
inlineTemplate = '<span><t t-esc="props.n"/></span>';
|
inlineTemplate = '<span><t t-esc="props.n"/></span>';
|
||||||
|
|
||||||
@@ -458,15 +513,15 @@ describe("lifecycle hooks", () => {
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const widget = new HookWidget(env, { n: 1 });
|
const widget = new Parent(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<span>1</span>");
|
expect(fixture.innerHTML).toBe("<span><span>1</span></span>");
|
||||||
widget.updateProps({ n: 2 });
|
widget.state.n = 2;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<span>1</span>");
|
expect(fixture.innerHTML).toBe("<span><span>1</span></span>");
|
||||||
def.resolve();
|
def.resolve();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expect(fixture.innerHTML).toBe("<span>2</span>");
|
expect(fixture.innerHTML).toBe("<span><span>2</span></span>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("patched hook is called after updating State", async () => {
|
test("patched hook is called after updating State", async () => {
|
||||||
@@ -495,16 +550,23 @@ describe("lifecycle hooks", () => {
|
|||||||
test("patched hook is called after updateProps", async () => {
|
test("patched hook is called after updateProps", async () => {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
|
|
||||||
|
class Parent extends Widget {
|
||||||
|
inlineTemplate = '<div><t t-widget="Child" t-props="{a:state.a}"/></div>';
|
||||||
|
state = { a: 1 };
|
||||||
|
widgets = { Child: TestWidget };
|
||||||
|
}
|
||||||
|
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
patched() {
|
patched() {
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env, { a: 1 });
|
const widget = new Parent(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
expect(n).toBe(0);
|
expect(n).toBe(0);
|
||||||
|
|
||||||
await widget.updateProps({ a: 2 });
|
widget.state.a = 2;
|
||||||
|
await nextTick();
|
||||||
expect(n).toBe(1);
|
expect(n).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -528,20 +590,27 @@ describe("lifecycle hooks", () => {
|
|||||||
|
|
||||||
test("shouldUpdate hook prevent rerendering", async () => {
|
test("shouldUpdate hook prevent rerendering", async () => {
|
||||||
let shouldUpdate = false;
|
let shouldUpdate = false;
|
||||||
|
class Parent extends Widget {
|
||||||
|
inlineTemplate = `<div><t t-widget="Child" t-props="{val:state.val}"/></div>`;
|
||||||
|
state = { val: 42 };
|
||||||
|
widgets = { Child: TestWidget };
|
||||||
|
}
|
||||||
class TestWidget extends Widget {
|
class TestWidget extends Widget {
|
||||||
inlineTemplate = `<div><t t-esc="props.val"/></div>`;
|
inlineTemplate = `<div><t t-esc="props.val"/></div>`;
|
||||||
shouldUpdate() {
|
shouldUpdate() {
|
||||||
return shouldUpdate;
|
return shouldUpdate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const widget = new TestWidget(env, { val: 42 });
|
const widget = new Parent(env);
|
||||||
await widget.mount(fixture);
|
await widget.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
expect(fixture.innerHTML).toBe("<div><div>42</div></div>");
|
||||||
await widget.updateProps({ val: 123 });
|
widget.state.val = 123;
|
||||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>42</div></div>");
|
||||||
shouldUpdate = true;
|
shouldUpdate = true;
|
||||||
await widget.updateProps({ val: 666 });
|
widget.state.val = 666;
|
||||||
expect(fixture.innerHTML).toBe("<div>666</div>");
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div><div>666</div></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
test("sub widget (inside sub node): hooks are correctly called", async () => {
|
||||||
@@ -613,9 +682,9 @@ describe("lifecycle hooks", () => {
|
|||||||
|
|
||||||
// Not sure about this order. If you disagree, feel free to open an issue...
|
// Not sure about this order. If you disagree, feel free to open an issue...
|
||||||
expect(steps).toEqual([
|
expect(steps).toEqual([
|
||||||
|
"parent:willPatch",
|
||||||
"child:willPatch",
|
"child:willPatch",
|
||||||
"child:patched",
|
"child:patched",
|
||||||
"parent:willPatch",
|
|
||||||
"parent:patched"
|
"parent:patched"
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -1416,8 +1485,8 @@ describe("async rendering", () => {
|
|||||||
|
|
||||||
class ChildA extends Widget {
|
class ChildA extends Widget {
|
||||||
inlineTemplate = `<span>a<t t-esc="props.val"/></span>`;
|
inlineTemplate = `<span>a<t t-esc="props.val"/></span>`;
|
||||||
updateProps(props): Promise<void> {
|
updateProps(props, forceUpdate, fiber): Promise<void> {
|
||||||
return defA.then(() => super.updateProps(props));
|
return defA.then(() => super.updateProps(props, forceUpdate, fiber));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ChildB extends Widget {
|
class ChildB extends Widget {
|
||||||
|
|||||||
@@ -1097,12 +1097,17 @@ describe("t-key", () => {
|
|||||||
|
|
||||||
describe("debugging", () => {
|
describe("debugging", () => {
|
||||||
test("t-debug", () => {
|
test("t-debug", () => {
|
||||||
|
const consoleLog = console.log;
|
||||||
|
console.log = jest.fn()
|
||||||
qweb.addTemplate(
|
qweb.addTemplate(
|
||||||
"test",
|
"test",
|
||||||
`<div t-debug="1"><t t-if="true"><span t-debug="1">hey</span></t></div>`
|
`<div t-debug="1"><t t-if="true"><span t-debug="1">hey</span></t></div>`
|
||||||
);
|
);
|
||||||
qweb.render('test');
|
qweb.render('test');
|
||||||
expect(qweb.templates.test.toString()).toMatchSnapshot();
|
expect(qweb.templates.test.toString()).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(console.log).toHaveBeenCalledTimes(1);
|
||||||
|
console.log = consoleLog;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-log", () => {
|
test("t-log", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user