[IMP] qweb: add preliminary support for transitions

Still very naive...

Part of #66
This commit is contained in:
Géry Debongnie
2019-05-02 11:40:10 +02:00
parent 6350d9598d
commit 0da44033ac
6 changed files with 188 additions and 9 deletions
+39
View File
@@ -0,0 +1,39 @@
# 🦉 Animations 🦉
Animation is a complex topic. There are many different use cases, and many
solutions and technologies.
## Simple CSS effects
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
necessary: it just needs to render a DOM element with a specific class. For
example:
```xml
<button class="flash" t-on-click="doSomething">Click</button>
```
with the following CSS:
```css
.flash {
transition: background 0.5s;
}
.flash:active {
background-color: #41454a;
transition: background 0s;
}
```
will produce a nice flash effect whenever the user click (or activate with the
keyboard) the button.
## CSS Transitions (single element)
A more complex situation occurs when we want to transition an element in or out
of the page. For example, we may want a fade-in and fade-out effect.
The `t-transition` directive is here to help us (see [QWeb documentation](doc/qweb.md#t-transition-directive)).
+49
View File
@@ -16,6 +16,7 @@
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
- [`t-ref` directive](#t-ref-directive)
- [`t-key` directive](#t-key-directive)
- [`t-transition` directive](#t-transition)
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
- [White spaces](#white-spaces)
- [Root nodes](#root-nodes)
@@ -253,6 +254,54 @@ There are three main use cases:
- *animations*: give a different identity to a component. Ex: thread id with
animations on add/remove message.
### `t-transition` directive
To perform useful transition effects, whenever an element appears or disappears,
it is necessary to add/remove some css style or class at some precise moment in
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
directive is there to help.
Whenever a node has a `t-transition` directive, with a `name` value, the following
will happen:
At node creation:
- the css classes `name-enter` and `name-enter-active` will be added before the
node is added to 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),
- the css class `name-enter-active` will be removed whenever a css transition
ends.
At node destruction:
- the css classes `name-leave` and `name-leave-active` will be added before the
node is removed to the DOM,
- the css class `name-leave` will be removed on the next animation frame (so it
can be used to trigger css transition effects),
- the css class `name-leave-active` will be removed whenever a css transition
ends. Only then will the element be removed from the DOM.
For example, a simple fade in/out effect can be done with this:
```xml
<div>
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
</div>
```
```css
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
```
Note: you will find more information on animations in the [animation](doc/animations.md)
documentation.
### Debugging (`t-debug` and `t-log`)
The javascript QWeb implementation provides two useful debugging directives:
+1
View File
@@ -11,4 +11,5 @@
## Miscellaneous
- [Quick Start](quick_start.md)
- [Animations](animations.md)
- [Comparison with React/Vue](comparison.md)
+54 -4
View File
@@ -61,6 +61,37 @@ const UTILS = {
}
}
return classes.join(" ");
},
nextFrame(cb: () => void) {
requestAnimationFrame(() => requestAnimationFrame(cb));
},
transitionCreate(elm: HTMLElement, name: string) {
elm.classList.add(name + "-enter");
elm.classList.add(name + "-enter-active");
},
transitionInsert(elm: HTMLElement, name: string) {
const finalize = () => {
elm.classList.remove(name + "-enter-active");
elm.classList.remove(name + "-enter-to");
};
elm.addEventListener("transitionend", finalize);
this.nextFrame(() => {
elm.classList.remove(name + "-enter");
elm.classList.add(name + "-enter-to");
});
},
transitionRemove(elm: HTMLElement, name: string, rm: () => void) {
elm.classList.add(name + "-leave");
elm.classList.add(name + "-leave-active");
elm.addEventListener("transitionend", () => {
elm.classList.remove(name + "-leave-active");
elm.classList.remove(name + "-enter-to");
rm();
});
this.nextFrame(() => {
elm.classList.remove(name + "-leave");
elm.classList.add(name + "-leave-to");
});
}
};
//------------------------------------------------------------------------------
@@ -240,6 +271,7 @@ export class QWeb {
callDirective,
onDirective,
refDirective,
transitionDirective,
debugDirective,
logDirective,
widgetDirective
@@ -983,16 +1015,34 @@ const onDirective: Directive = {
const refDirective: Directive = {
name: "ref",
priority: 95,
atNodeCreation({ ctx, node }) {
let ref = node.getAttribute("t-ref")!;
ctx.addLine(`p${ctx.parentNode}.hook = {
atNodeCreation({ ctx, nodeID, value }) {
ctx.addLine(`p${nodeID}.hook = {
create: (_, n) => context.refs[${ctx.formatExpression(
ref
value
)}] = n.elm,
};`);
}
};
const transitionDirective: Directive = {
name: "transition",
priority: 96,
atNodeCreation({ ctx, value }) {
let name = value;
ctx.addLine(`p${ctx.parentNode}.hook = {
create: (_, n) => {
this.utils.transitionCreate(n.elm, '${name}');
},
insert: vn => {
this.utils.transitionInsert(vn.elm, '${name}');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, '${name}', rm);
}
};`);
}
};
const debugDirective: Directive = {
name: "debug",
priority: 99,
+27
View File
@@ -1,5 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`animations t-transition, on a simple node 1`] = `
"function anonymous(context,extra
) {
var h = this.utils.h;
var c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1);
var c2 = [], p2 = {key:2};
var vn2 = h('span', p2, c2);
c1.push(vn2);
p2.hook = {
create: (_, n) => {
this.utils.transitionCreate(n.elm, 'chimay');
},
insert: vn => {
this.utils.transitionInsert(vn.elm, 'chimay');
},
remove: (vn, rm) => {
this.utils.transitionRemove(vn.elm, 'chimay', rm);
}
};
c2.push({text: \`blue\`});
return vn1;
}"
`;
exports[`animations t-transition, on a simple node 2`] = `"<span class=\\"chimay-enter chimay-enter-active\\">blue</span>"`;
exports[`attributes dynamic attribute falsy variable 1`] = `
"function anonymous(context,extra
) {
+18 -5
View File
@@ -1,5 +1,5 @@
import { patch } from "../src/vdom";
import { EvalContext, QWeb } from "../src/qweb";
import { patch } from "../src/vdom";
import { normalize } from "./helpers";
//------------------------------------------------------------------------------
@@ -1098,12 +1098,12 @@ describe("t-key", () => {
describe("debugging", () => {
test("t-debug", () => {
const consoleLog = console.log;
console.log = jest.fn()
console.log = jest.fn();
qweb.addTemplate(
"test",
`<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.fn.toString()).toMatchSnapshot();
expect(console.log).toHaveBeenCalledTimes(1);
@@ -1112,7 +1112,7 @@ describe("debugging", () => {
test("t-log", () => {
const consoleLog = console.log;
console.log = jest.fn()
console.log = jest.fn();
qweb.addTemplate(
"test",
@@ -1121,10 +1121,23 @@ describe("debugging", () => {
<t t-log="foo + 3"/>
</div>`
);
qweb.render('test');
qweb.render("test");
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
expect(console.log).toHaveBeenCalledWith(45);
console.log = consoleLog;
});
});
describe("animations", () => {
test("t-transition, on a simple node", async () => {
// this test does not test much, because it is not easy to test timing
// transitions... we should do a little more effort for these tests.
qweb.addTemplate(
"test",
`<div><span t-transition="chimay">blue</span></div>`
);
let dom: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
expect(dom.innerHTML).toMatchSnapshot();
});
});