diff --git a/doc/qweb.md b/doc/qweb.md
index 942158fe..3e997093 100644
--- a/doc/qweb.md
+++ b/doc/qweb.md
@@ -497,10 +497,10 @@ directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
-At node creation:
+At node insertion:
-- the css classes `name-enter` and `name-enter-active` will be added before the
- node is added to the DOM,
+- the css classes `name-enter` and `name-enter-active` will be added directly
+ when the node is inserted into the DOM,
- on the next animation frame: the css class `name-enter` will be removed and the
class `name-enter-to` will be added (so they can be used to trigger css
transition effects),
@@ -535,6 +535,8 @@ For example, a simple fade in/out effect can be done with this:
}
```
+The `t-transition` directive can be combined with `t-widget`.
+
Note: more information on animations are available [here](doc/animations.md).
### `t-mounted` directive
diff --git a/src/qweb_core.ts b/src/qweb_core.ts
index fc417d4c..5376db42 100644
--- a/src/qweb_core.ts
+++ b/src/qweb_core.ts
@@ -269,6 +269,9 @@ export class QWeb {
// pollute the rendering context by adding some keys in it.
ctx.code.unshift(" let owner = context;");
}
+ if (ctx.shouldDefineUtils) {
+ ctx.code.unshift(" let utils = this.utils;");
+ }
if (!ctx.rootNode) {
throw new Error("A template should have one root node");
@@ -613,6 +616,7 @@ export class Context {
rootContext: Context;
caller: Element | undefined;
shouldDefineOwner: boolean = false;
+ shouldDefineUtils: boolean = false;
shouldProtectContext: boolean = false;
inLoop: boolean = false;
inPreTag: boolean = false;
diff --git a/src/qweb_extensions.ts b/src/qweb_extensions.ts
index 712612b4..8660504c 100644
--- a/src/qweb_extensions.ts
+++ b/src/qweb_extensions.ts
@@ -192,7 +192,10 @@ QWeb.addDirective({
* explanation of the code generated by the t-widget directive for the following
* situation:
* ```xml
- *
+ *
* ```
*
* ```js
@@ -252,6 +255,8 @@ QWeb.addDirective({
* }
* w4 = new W4(owner, props4);
*
+ * let utils = this.utils;
+ *
* // Whenever we rerender the parent widget, we need to be sure that we
* // are able to find the widget instance. To do that, we register it to
* // the parent cmap (children map). Note that the 'template' key is
@@ -281,15 +286,21 @@ QWeb.addDirective({
* // to synchronise the pvnode elm with the resulting elm
* let nvn = w4._mount(vnode, vn.elm);
* pvnode.elm = nvn.elm;
+ * // what follows is only present if there are animations on the widget
+ * utils.transitionInsert(vn.elm, "fade");
* },
* remove() {
- * // apparently, in some cases, it is necessary to call the destroy
- * // method here
- * w4.destroy();
+ * // override with empty function to prevent from removing the node
+ * // directly. It will be removed when destroy is called anyway, which
+ * // delays the removal if there are animations.
* },
* destroy() {
- * // and here...
- * w4.destroy();
+ * // if there are animations, we delay the call to destroy on the
+ * // widget, if not, we call it directly.
+ * let finalize = () => {
+ * w4.destroy();
+ * };
+ * utils.transitionRemove(vn.elm, "fade", finalize);
* }
* };
* // the pvnode is inserted at the correct position in the div's children
@@ -332,16 +343,20 @@ QWeb.addDirective({
atNodeEncounter({ ctx, value, node }): boolean {
ctx.addLine("//WIDGET");
ctx.rootContext.shouldDefineOwner = true;
+ ctx.rootContext.shouldDefineUtils = true;
let props = node.getAttribute("t-props");
let keepAlive = node.getAttribute("t-keepalive") ? true : false;
- // t-on- events...
+ // t-on- events and t-transition
const events: [string, string][] = [];
+ let transition: string = "";
const attributes = (node).attributes;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name;
if (name.startsWith("t-on-")) {
events.push([name.slice(5), attributes[i].textContent!]);
+ } else if (name === "t-transition") {
+ transition = attributes[i].textContent!;
}
}
@@ -378,13 +393,23 @@ QWeb.addDirective({
ctx.addLine(`const ${refKey} = ${ctx.formatExpression(ref)}`);
refExpr = `context.refs[${refKey}] = w${widgetID};`;
}
-
+ let transitionsInsertCode = "";
+ if (transition) {
+ transitionsInsertCode = `utils.transitionInsert(vn.elm, '${transition}');`;
+ }
let finalizeWidgetCode = `w${widgetID}.${
keepAlive ? "unmount" : "destroy"
- }()`;
+ }();`;
if (ref) {
- finalizeWidgetCode += `;delete context.refs[${refKey}]`;
+ finalizeWidgetCode += `delete context.refs[${refKey}];`; // FIXME: shouldn't we keep ref if keepAlive is true?
}
+ if (transition) {
+ finalizeWidgetCode = `let finalize = () => {
+ ${finalizeWidgetCode}
+ };
+ utils.transitionRemove(vn.elm, '${transition}', finalize);`;
+ }
+
let createHook = "";
let classAttr = node.getAttribute("class");
let tattClass = node.getAttribute("t-att-class");
@@ -447,8 +472,10 @@ QWeb.addDirective({
ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
}
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
+ // hack: specify empty remove hook to prevent the node from being removed from the DOM
+ // FIXME: click to re-add widget during remove transition -> leak
ctx.addLine(
- `def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn){let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}},remove(){${finalizeWidgetCode}},destroy(){${finalizeWidgetCode}}}});c${
+ `def${defID} = def${defID}.then(vnode=>{${createHook}let pvnode=h(vnode.sel, {key: ${templateID}, hook: {insert(vn) {let nvn=w${widgetID}._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;${refExpr}${transitionsInsertCode}},remove() {},destroy(vn) {${finalizeWidgetCode}}}});c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;w${widgetID}.__owl__.pvnode = pvnode;});`
);
diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/__snapshots__/component.test.ts.snap
index 57841099..f07dba6b 100644
--- a/tests/__snapshots__/component.test.ts.snap
+++ b/tests/__snapshots__/component.test.ts.snap
@@ -1,8 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`animations t-transition combined with t-widget 1`] = `
+"function anonymous(context,extra
+) {
+ let utils = this.utils;
+ let owner = context;
+ var h = this.utils.h;
+ let c1 = [], p1 = {key:1};
+ var vn1 = h('div', p1, c1);
+ //WIDGET
+ let _2_index = c1.length;
+ c1.push(null);
+ let def3;
+ let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
+ let props4 = {};
+ if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
+ w4.destroy();
+ w4 = false
+ }
+ if (!w4) {
+ let W4 = context.widgets['Child'];
+ if (!W4) {throw new Error(\`Cannot find the definition of widget \\"Child\\"\`)}
+ w4 = new W4(owner, props4);
+ context.__owl__.cmap[4] = w4.__owl__.id;
+ 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 = () => {
+ w4.destroy();
+ };
+ utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ } else {
+ 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;});
+ }
+ extra.promises.push(def3);
+ return vn1;
+}"
+`;
+
+exports[`animations t-transition combined with t-widget and t-if 1`] = `
+"function anonymous(context,extra
+) {
+ let utils = this.utils;
+ let owner = context;
+ var h = this.utils.h;
+ let c1 = [], p1 = {key:1};
+ var vn1 = h('div', p1, c1);
+ if (context['state'].display) {
+ //WIDGET
+ let _2_index = c1.length;
+ c1.push(null);
+ let def3;
+ let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
+ let props4 = {};
+ if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
+ w4.destroy();
+ w4 = false
+ }
+ if (!w4) {
+ let W4 = context.widgets['Child'];
+ if (!W4) {throw new Error(\`Cannot find the definition of widget \\"Child\\"\`)}
+ w4 = new W4(owner, props4);
+ context.__owl__.cmap[4] = w4.__owl__.id;
+ 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 = () => {
+ w4.destroy();
+ };
+ utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ } else {
+ 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;});
+ }
+ extra.promises.push(def3);
+ }
+ return vn1;
+}"
+`;
+
exports[`class and style attributes with t-widget dynamic t-att-style is properly added and updated on widget root el 1`] = `
"function anonymous(context,extra
) {
+ let utils = this.utils;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -24,7 +101,7 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
- def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
@@ -37,6 +114,7 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
exports[`class and style attributes with t-widget t-att-class is properly added/removed on widget root el 1`] = `
"function anonymous(context,extra
) {
+ let utils = this.utils;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -62,7 +140,7 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
if (_5[k]) {
vn.elm.classList.add(k);
}
- }}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ }}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let cl=w4.el.classList;for (let k in _5) {if (_5[k]) {cl.add(k)} else {cl.remove(k)}}let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
@@ -75,6 +153,7 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
exports[`composition sub widgets with some state rendered in a loop 1`] = `
"function anonymous(context,extra
) {
+ let utils = this.utils;
let owner = context;
context = Object.create(context);
var h = this.utils.h;
@@ -109,7 +188,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
w7 = new W7(owner, props7);
context.__owl__.cmap[key8] = w7.__owl__.id;
def6 = w7._prepare();
- def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn){let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w7.destroy()},destroy(){w7.destroy()}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
+ def6 = def6.then(vnode=>{let pvnode=h(vnode.sel, {key: key8, hook: {insert(vn) {let nvn=w7._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w7.destroy();}}});c1[_5_index]=pvnode;w7.__owl__.pvnode = pvnode;});
} else {
def6 = w7._updateProps(props7, extra.forceUpdate, extra.patchQueue);
def6 = def6.then(()=>{if (w7.__owl__.isDestroyed) {return};let pvnode=w7.__owl__.pvnode;c1[_5_index]=pvnode;});
@@ -123,6 +202,7 @@ exports[`composition sub widgets with some state rendered in a loop 1`] = `
exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
"function anonymous(context,extra
) {
+ let utils = this.utils;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -144,7 +224,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
w4 = new W4(owner, props4);
context.__owl__.cmap[key5] = w4.__owl__.id;
def3 = w4._prepare();
- def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn){let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ def3 = def3.then(vnode=>{let pvnode=h(vnode.sel, {key: key5, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
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;});
@@ -157,6 +237,7 @@ exports[`random stuff/miscellaneous snapshotting compiled code 1`] = `
exports[`random stuff/miscellaneous t-props should not be undefined (snapshotting) 1`] = `
"function anonymous(context,extra
) {
+ let utils = this.utils;
let owner = context;
var h = this.utils.h;
let c1 = [], p1 = {key:1};
@@ -177,7 +258,7 @@ exports[`random stuff/miscellaneous t-props should not be undefined (snapshottin
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
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;},remove(){w4.destroy()},destroy(){w4.destroy()}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ 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;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
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;});
diff --git a/tests/__snapshots__/untitled.js b/tests/__snapshots__/untitled.js
new file mode 100644
index 00000000..b1409550
--- /dev/null
+++ b/tests/__snapshots__/untitled.js
@@ -0,0 +1,33 @@
+function anonymous(context, extra) {
+ var h = this.utils.h;
+ let c1 = [], p1 = {key:1};
+ var vn1 = h('div', p1, c1);
+ if (context['state'].display) {
+ //WIDGET
+ let _2_index = c1.length;
+ c1.push(null);
+ let def3;
+ let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
+ let props4 = {};
+ if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode && props4 !== w4.__owl__.renderProps) {
+ w4.destroy();
+ w4 = false
+ }
+ if (!w4) {
+ let W4 = context.widgets['Child'];
+ if (!W4) {throw new Error(`Cannot find the definition of widget "Child"`)}
+ w4 = new W4(owner, props4);
+ context.__owl__.cmap[4] = w4.__owl__.id;
+ 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;this.utils.transitionInsert(vn.elm, 'chimay');},remove: () => {},destroy: (vn) => {let finalize = () => {
+ w4.destroy();
+ };
+ this.utils.transitionRemove(vn.elm, 'chimay', finalize);}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ } else {
+ 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;});
+ }
+ extra.promises.push(def3);
+ }
+
+}
diff --git a/tests/component.test.ts b/tests/component.test.ts
index cd7addef..d40f715a 100644
--- a/tests/component.test.ts
+++ b/tests/component.test.ts
@@ -1778,7 +1778,6 @@ describe("random stuff/miscellaneous", () => {
"C:_patch",
"E:willUnmount",
"E:destroy",
- "E:destroy", // maybe should look into this
"F:_patch(from _mount)",
"F:mounted",
"D:_patch",
@@ -2281,7 +2280,7 @@ describe("t-mounted directive", () => {
widget.f = jest.fn();
await widget.mount(fixture);
- patchNextFrame((cb) => cb());
+ patchNextFrame(cb => cb());
expect(widget.f).toHaveBeenCalledTimes(0);
widget.state.flag = true;
@@ -2426,4 +2425,100 @@ describe("animations", () => {
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
expect(spanNode.className).toBe("");
});
+
+ test("t-transition combined with t-widget", async () => {
+ expect.assertions(5);
+
+ env.qweb.addTemplate(
+ "Parent",
+ `
`
+ );
+ env.qweb.addTemplate("Child", `blue`);
+ class Parent extends Widget {
+ widgets = { Child: Child };
+ }
+ class Child extends Widget {}
+ const widget = new Parent(env);
+
+ let def = makeDeferred();
+ var spanNode;
+ patchNextFrame(cb => {
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+ cb(performance.now());
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+ def.resolve();
+ });
+ await widget.mount(fixture);
+ spanNode = widget.el!.children[0];
+
+ expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+
+ await def; // wait for the mocked repaint to be done
+ spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
+ expect(fixture.innerHTML).toBe('blue
');
+ });
+
+ test("t-transition combined with t-widget and t-if", async () => {
+ expect.assertions(8);
+
+ env.qweb.addTemplate(
+ "Parent",
+ `
`
+ );
+ env.qweb.addTemplate("Child", `blue`);
+ class Parent extends Widget {
+ widgets = { Child: Child };
+ state = { display: true };
+ }
+ class Child extends Widget {}
+ const widget = new Parent(env);
+
+ let def = makeDeferred();
+ var spanNode;
+ patchNextFrame(cb => {
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+ cb(performance.now());
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+ def.resolve();
+ });
+ await widget.mount(fixture);
+ spanNode = widget.el!.children[0];
+
+ expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+
+ await def; // wait for the mocked repaint to be done
+ spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
+ expect(fixture.innerHTML).toBe('blue
');
+
+ // remove span from the DOM
+ def = makeDeferred();
+ widget.state.display = false;
+ patchNextFrame(cb => {
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+ cb(performance.now());
+ expect(fixture.innerHTML).toBe(
+ 'blue
'
+ );
+ def.resolve();
+ });
+ await def; // wait for the mocked repaint to be done
+ spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
+ expect(fixture.innerHTML).toBe("");
+ });
});