mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] qweb: add preliminary support for transitions
Still very naive... Part of #66
This commit is contained in:
@@ -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
@@ -16,6 +16,7 @@
|
|||||||
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
|
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
|
||||||
- [`t-ref` directive](#t-ref-directive)
|
- [`t-ref` directive](#t-ref-directive)
|
||||||
- [`t-key` directive](#t-key-directive)
|
- [`t-key` directive](#t-key-directive)
|
||||||
|
- [`t-transition` directive](#t-transition)
|
||||||
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
|
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
|
||||||
- [White spaces](#white-spaces)
|
- [White spaces](#white-spaces)
|
||||||
- [Root nodes](#root-nodes)
|
- [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*: give a different identity to a component. Ex: thread id with
|
||||||
animations on add/remove message.
|
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`)
|
### Debugging (`t-debug` and `t-log`)
|
||||||
|
|
||||||
The javascript QWeb implementation provides two useful debugging directives:
|
The javascript QWeb implementation provides two useful debugging directives:
|
||||||
|
|||||||
@@ -11,4 +11,5 @@
|
|||||||
|
|
||||||
## Miscellaneous
|
## Miscellaneous
|
||||||
- [Quick Start](quick_start.md)
|
- [Quick Start](quick_start.md)
|
||||||
|
- [Animations](animations.md)
|
||||||
- [Comparison with React/Vue](comparison.md)
|
- [Comparison with React/Vue](comparison.md)
|
||||||
|
|||||||
+54
-4
@@ -61,6 +61,37 @@ const UTILS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return classes.join(" ");
|
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,
|
callDirective,
|
||||||
onDirective,
|
onDirective,
|
||||||
refDirective,
|
refDirective,
|
||||||
|
transitionDirective,
|
||||||
debugDirective,
|
debugDirective,
|
||||||
logDirective,
|
logDirective,
|
||||||
widgetDirective
|
widgetDirective
|
||||||
@@ -983,16 +1015,34 @@ const onDirective: Directive = {
|
|||||||
const refDirective: Directive = {
|
const refDirective: Directive = {
|
||||||
name: "ref",
|
name: "ref",
|
||||||
priority: 95,
|
priority: 95,
|
||||||
atNodeCreation({ ctx, node }) {
|
atNodeCreation({ ctx, nodeID, value }) {
|
||||||
let ref = node.getAttribute("t-ref")!;
|
ctx.addLine(`p${nodeID}.hook = {
|
||||||
ctx.addLine(`p${ctx.parentNode}.hook = {
|
|
||||||
create: (_, n) => context.refs[${ctx.formatExpression(
|
create: (_, n) => context.refs[${ctx.formatExpression(
|
||||||
ref
|
value
|
||||||
)}] = n.elm,
|
)}] = 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 = {
|
const debugDirective: Directive = {
|
||||||
name: "debug",
|
name: "debug",
|
||||||
priority: 99,
|
priority: 99,
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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`] = `
|
exports[`attributes dynamic attribute falsy variable 1`] = `
|
||||||
"function anonymous(context,extra
|
"function anonymous(context,extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
+18
-5
@@ -1,5 +1,5 @@
|
|||||||
import { patch } from "../src/vdom";
|
|
||||||
import { EvalContext, QWeb } from "../src/qweb";
|
import { EvalContext, QWeb } from "../src/qweb";
|
||||||
|
import { patch } from "../src/vdom";
|
||||||
import { normalize } from "./helpers";
|
import { normalize } from "./helpers";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -1098,12 +1098,12 @@ describe("t-key", () => {
|
|||||||
describe("debugging", () => {
|
describe("debugging", () => {
|
||||||
test("t-debug", () => {
|
test("t-debug", () => {
|
||||||
const consoleLog = console.log;
|
const consoleLog = console.log;
|
||||||
console.log = jest.fn()
|
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.fn.toString()).toMatchSnapshot();
|
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledTimes(1);
|
expect(console.log).toHaveBeenCalledTimes(1);
|
||||||
@@ -1112,7 +1112,7 @@ describe("debugging", () => {
|
|||||||
|
|
||||||
test("t-log", () => {
|
test("t-log", () => {
|
||||||
const consoleLog = console.log;
|
const consoleLog = console.log;
|
||||||
console.log = jest.fn()
|
console.log = jest.fn();
|
||||||
|
|
||||||
qweb.addTemplate(
|
qweb.addTemplate(
|
||||||
"test",
|
"test",
|
||||||
@@ -1121,10 +1121,23 @@ describe("debugging", () => {
|
|||||||
<t t-log="foo + 3"/>
|
<t t-log="foo + 3"/>
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
qweb.render('test');
|
qweb.render("test");
|
||||||
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
|
expect(qweb.templates.test.fn.toString()).toMatchSnapshot();
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledWith(45);
|
expect(console.log).toHaveBeenCalledWith(45);
|
||||||
console.log = consoleLog;
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user