mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] qweb: no more duplicated nodes during transitions
If a node was re-added while being removed (i.e. during the remove transition), the node was duplicated in the DOM for the delay of the remove transition. This rev. removes the old occurence directly in that case. Fixes #121
This commit is contained in:
committed by
Géry Debongnie
parent
1c0d4b5832
commit
3ebe985c3c
+22
-12
@@ -1,4 +1,5 @@
|
|||||||
import { QWeb, UTILS } from "./qweb_core";
|
import { QWeb, UTILS } from "./qweb_core";
|
||||||
|
import { VNode } from "./vdom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owl QWeb Extensions
|
* Owl QWeb Extensions
|
||||||
@@ -95,7 +96,17 @@ UTILS.nextFrame = function(cb: () => void) {
|
|||||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||||
};
|
};
|
||||||
|
|
||||||
UTILS.transitionInsert = function(elm: HTMLElement, name: string) {
|
UTILS.transitionInsert = function(vn: VNode, name: string) {
|
||||||
|
const elm = <HTMLElement>vn.elm;
|
||||||
|
// remove potential duplicated vnode that is currently being removed, to
|
||||||
|
// prevent from having twice the same node in the DOM during an animation
|
||||||
|
const dup =
|
||||||
|
elm.parentElement &&
|
||||||
|
elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||||
|
if (dup) {
|
||||||
|
dup.remove();
|
||||||
|
}
|
||||||
|
|
||||||
elm.classList.add(name + "-enter");
|
elm.classList.add(name + "-enter");
|
||||||
elm.classList.add(name + "-enter-active");
|
elm.classList.add(name + "-enter-active");
|
||||||
const finalize = () => {
|
const finalize = () => {
|
||||||
@@ -109,11 +120,10 @@ UTILS.transitionInsert = function(elm: HTMLElement, name: string) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
UTILS.transitionRemove = function(
|
UTILS.transitionRemove = function(vn: VNode, name: string, rm: () => void) {
|
||||||
elm: HTMLElement,
|
const elm = <HTMLElement>vn.elm;
|
||||||
name: string,
|
elm.setAttribute("data-owl-key", vn.key!);
|
||||||
rm: () => void
|
|
||||||
) {
|
|
||||||
elm.classList.add(name + "-leave");
|
elm.classList.add(name + "-leave");
|
||||||
elm.classList.add(name + "-leave-active");
|
elm.classList.add(name + "-leave-active");
|
||||||
const finalize = () => {
|
const finalize = () => {
|
||||||
@@ -170,8 +180,8 @@ QWeb.addDirective({
|
|||||||
atNodeCreation({ value, addNodeHook }) {
|
atNodeCreation({ value, addNodeHook }) {
|
||||||
let name = value;
|
let name = value;
|
||||||
const hooks = {
|
const hooks = {
|
||||||
insert: `this.utils.transitionInsert(vn.elm, '${name}');`,
|
insert: `this.utils.transitionInsert(vn, '${name}');`,
|
||||||
remove: `this.utils.transitionRemove(vn.elm, '${name}', rm);`
|
remove: `this.utils.transitionRemove(vn, '${name}', rm);`
|
||||||
};
|
};
|
||||||
for (let hookName in hooks) {
|
for (let hookName in hooks) {
|
||||||
addNodeHook(hookName, hooks[hookName]);
|
addNodeHook(hookName, hooks[hookName]);
|
||||||
@@ -297,7 +307,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
|
|||||||
* let nvn = w4._mount(vnode, vn.elm);
|
* let nvn = w4._mount(vnode, vn.elm);
|
||||||
* pvnode.elm = nvn.elm;
|
* pvnode.elm = nvn.elm;
|
||||||
* // what follows is only present if there are animations on the widget
|
* // what follows is only present if there are animations on the widget
|
||||||
* utils.transitionInsert(vn.elm, "fade");
|
* utils.transitionInsert(vn, "fade");
|
||||||
* },
|
* },
|
||||||
* remove() {
|
* remove() {
|
||||||
* // override with empty function to prevent from removing the node
|
* // override with empty function to prevent from removing the node
|
||||||
@@ -310,7 +320,7 @@ const T_WIDGET_MODS_CODE = Object.assign({}, MODS_CODE, {
|
|||||||
* let finalize = () => {
|
* let finalize = () => {
|
||||||
* w4.destroy();
|
* w4.destroy();
|
||||||
* };
|
* };
|
||||||
* utils.transitionRemove(vn.elm, "fade", finalize);
|
* utils.transitionRemove(vn, "fade", finalize);
|
||||||
* }
|
* }
|
||||||
* };
|
* };
|
||||||
* // the pvnode is inserted at the correct position in the div's children
|
* // the pvnode is inserted at the correct position in the div's children
|
||||||
@@ -422,7 +432,7 @@ QWeb.addDirective({
|
|||||||
}
|
}
|
||||||
let transitionsInsertCode = "";
|
let transitionsInsertCode = "";
|
||||||
if (transition) {
|
if (transition) {
|
||||||
transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`;
|
transitionsInsertCode = `utils.transitionInsert(vn, '${transition}');`;
|
||||||
}
|
}
|
||||||
let finalizeWidgetCode = `w${widgetID}.${
|
let finalizeWidgetCode = `w${widgetID}.${
|
||||||
keepAlive ? "unmount" : "destroy"
|
keepAlive ? "unmount" : "destroy"
|
||||||
@@ -434,7 +444,7 @@ QWeb.addDirective({
|
|||||||
finalizeWidgetCode = `let finalize = () => {
|
finalizeWidgetCode = `let finalize = () => {
|
||||||
${finalizeWidgetCode}
|
${finalizeWidgetCode}
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn.elm, '${transition}', finalize);`;
|
utils.transitionRemove(vn, '${transition}', finalize);`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let createHook = "";
|
let createHook = "";
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ exports[`animations t-transition combined with t-widget 1`] = `
|
|||||||
w4 = new W4(owner, props4);
|
w4 = new W4(owner, props4);
|
||||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4._prepare();
|
def3 = w4._prepare();
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||||
w4.destroy();
|
w4.destroy();
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
} else {
|
} else {
|
||||||
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
@@ -74,10 +74,10 @@ exports[`animations t-transition combined with t-widget and t-if 1`] = `
|
|||||||
w4 = new W4(owner, props4);
|
w4 = new W4(owner, props4);
|
||||||
context.__owl__.cmap[4] = w4.__owl__.id;
|
context.__owl__.cmap[4] = w4.__owl__.id;
|
||||||
def3 = w4._prepare();
|
def3 = w4._prepare();
|
||||||
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn.elm, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;utils.transitionInsert(vn, 'chimay');},remove() {},destroy(vn) {let finalize = () => {
|
||||||
w4.destroy();
|
w4.destroy();
|
||||||
};
|
};
|
||||||
utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
utils.transitionRemove(vn, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
|
||||||
} else {
|
} else {
|
||||||
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||||
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
|
||||||
@@ -96,10 +96,10 @@ exports[`animations t-transition with no delay/duration 1`] = `
|
|||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
this.utils.transitionInsert(vn.elm, 'jupiler');
|
this.utils.transitionInsert(vn, 'jupiler');
|
||||||
},
|
},
|
||||||
remove: (vn, rm) => {
|
remove: (vn, rm) => {
|
||||||
this.utils.transitionRemove(vn.elm, 'jupiler', rm);
|
this.utils.transitionRemove(vn, 'jupiler', rm);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
c1.push({text: \`blue\`});
|
c1.push({text: \`blue\`});
|
||||||
@@ -115,10 +115,10 @@ exports[`animations t-transition, on a simple node (insert) 1`] = `
|
|||||||
var vn1 = h('span', p1, c1);
|
var vn1 = h('span', p1, c1);
|
||||||
p1.hook = {
|
p1.hook = {
|
||||||
insert: vn => {
|
insert: vn => {
|
||||||
this.utils.transitionInsert(vn.elm, 'chimay');
|
this.utils.transitionInsert(vn, 'chimay');
|
||||||
},
|
},
|
||||||
remove: (vn, rm) => {
|
remove: (vn, rm) => {
|
||||||
this.utils.transitionRemove(vn.elm, 'chimay', rm);
|
this.utils.transitionRemove(vn, 'chimay', rm);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
c1.push({text: \`blue\`});
|
c1.push({text: \`blue\`});
|
||||||
|
|||||||
+131
-2
@@ -253,11 +253,11 @@ describe("animations", () => {
|
|||||||
widget.state.display = false;
|
widget.state.display = false;
|
||||||
patchNextFrame(cb => {
|
patchNextFrame(cb => {
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
'<div><span class="chimay-leave chimay-leave-active">blue</span></div>'
|
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="4">blue</span></div>'
|
||||||
);
|
);
|
||||||
cb();
|
cb();
|
||||||
expect(fixture.innerHTML).toBe(
|
expect(fixture.innerHTML).toBe(
|
||||||
'<div><span class="chimay-leave-active chimay-leave-to">blue</span></div>'
|
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="4">blue</span></div>'
|
||||||
);
|
);
|
||||||
def.resolve();
|
def.resolve();
|
||||||
});
|
});
|
||||||
@@ -287,4 +287,133 @@ describe("animations", () => {
|
|||||||
expect(widget.f).toHaveBeenCalledTimes(1);
|
expect(widget.f).toHaveBeenCalledTimes(1);
|
||||||
unpatchNextFrame();
|
unpatchNextFrame();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-transition, remove and re-add before transitionend", async () => {
|
||||||
|
expect.assertions(11);
|
||||||
|
|
||||||
|
env.qweb.addTemplates(
|
||||||
|
`<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<button t-on-click="toggle">Toggle</button>
|
||||||
|
<span t-if="state.flag" t-transition="chimay">blue</span>
|
||||||
|
</div>
|
||||||
|
</templates>`
|
||||||
|
);
|
||||||
|
class Parent extends Widget {
|
||||||
|
constructor(parent) {
|
||||||
|
super(parent);
|
||||||
|
this.state = { flag: false };
|
||||||
|
}
|
||||||
|
toggle() {
|
||||||
|
this.state.flag = !this.state.flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = new Parent(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
let button = widget.el!.querySelector("button");
|
||||||
|
|
||||||
|
let def = makeDeferred();
|
||||||
|
let phase = "enter";
|
||||||
|
patchNextFrame(cb => {
|
||||||
|
let spans = fixture.querySelectorAll("span");
|
||||||
|
expect(spans.length).toBe(1);
|
||||||
|
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||||
|
cb();
|
||||||
|
expect(spans[0].className).toBe(
|
||||||
|
`chimay-${phase}-active chimay-${phase}-to`
|
||||||
|
);
|
||||||
|
def.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// click display the span
|
||||||
|
button!.click();
|
||||||
|
await def; // wait for the mocked repaint to be done
|
||||||
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
// click to remove the span, and click again to re-add it before transitionend
|
||||||
|
def = makeDeferred();
|
||||||
|
phase = "leave";
|
||||||
|
button!.click();
|
||||||
|
|
||||||
|
await def; // wait for the mocked repaint to be done
|
||||||
|
def = makeDeferred();
|
||||||
|
phase = "enter";
|
||||||
|
button!.click();
|
||||||
|
|
||||||
|
await def; // wait for the mocked repaint to be done
|
||||||
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-transition combined with t-widget, remove and re-add before transitionend", async () => {
|
||||||
|
expect.assertions(11);
|
||||||
|
|
||||||
|
env.qweb.addTemplates(
|
||||||
|
`<templates>
|
||||||
|
<div t-name="Parent">
|
||||||
|
<button t-on-click="toggle">Toggle</button>
|
||||||
|
<t t-if="state.flag" t-widget="Child" t-transition="chimay"/>
|
||||||
|
</div>
|
||||||
|
<span t-name="Child">blue</span>
|
||||||
|
</templates>`
|
||||||
|
);
|
||||||
|
class Child extends Widget {}
|
||||||
|
class Parent extends Widget {
|
||||||
|
widgets = { Child };
|
||||||
|
constructor(parent) {
|
||||||
|
super(parent);
|
||||||
|
this.state = { flag: false };
|
||||||
|
}
|
||||||
|
toggle() {
|
||||||
|
this.state.flag = !this.state.flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = new Parent(env);
|
||||||
|
await widget.mount(fixture);
|
||||||
|
let button = widget.el!.querySelector("button");
|
||||||
|
|
||||||
|
let def = makeDeferred();
|
||||||
|
let phase = "enter";
|
||||||
|
patchNextFrame(cb => {
|
||||||
|
let spans = fixture.querySelectorAll("span");
|
||||||
|
expect(spans.length).toBe(1);
|
||||||
|
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||||
|
cb();
|
||||||
|
expect(spans[0].className).toBe(
|
||||||
|
`chimay-${phase}-active chimay-${phase}-to`
|
||||||
|
);
|
||||||
|
def.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// click display the span
|
||||||
|
button!.click();
|
||||||
|
await def; // wait for the mocked repaint to be done
|
||||||
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
// click to remove the span, and click again to re-add it before transitionend
|
||||||
|
def = makeDeferred();
|
||||||
|
phase = "leave";
|
||||||
|
button!.click();
|
||||||
|
|
||||||
|
await def; // wait for the mocked repaint to be done
|
||||||
|
def = makeDeferred();
|
||||||
|
phase = "enter";
|
||||||
|
button!.click();
|
||||||
|
|
||||||
|
await def; // wait for the mocked repaint to be done
|
||||||
|
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<div><button>Toggle</button><span class="">blue</span></div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ const ANIMATION_CSS = `button {
|
|||||||
|
|
||||||
.flash {
|
.flash {
|
||||||
background-position: center;
|
background-position: center;
|
||||||
transition: background 0.5s;
|
transition: background .6s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash:active {
|
.flash:active {
|
||||||
@@ -208,7 +208,7 @@ const ANIMATION_CSS = `button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity .5s;
|
transition: opacity .6s;
|
||||||
}
|
}
|
||||||
.fade-enter, .fade-leave-to {
|
.fade-enter, .fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user