diff --git a/doc/animations.md b/doc/animations.md new file mode 100644 index 00000000..7dabb28d --- /dev/null +++ b/doc/animations.md @@ -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 + +``` + +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)). \ No newline at end of file diff --git a/doc/qweb.md b/doc/qweb.md index 6201f610..3adf36eb 100644 --- a/doc/qweb.md +++ b/doc/qweb.md @@ -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 +
+
Hello
+
+``` + +```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: diff --git a/doc/readme.md b/doc/readme.md index 8eb97eb0..4abfac38 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -11,4 +11,5 @@ ## Miscellaneous - [Quick Start](quick_start.md) +- [Animations](animations.md) - [Comparison with React/Vue](comparison.md) diff --git a/src/qweb.ts b/src/qweb.ts index f7bb8882..df636aec 100644 --- a/src/qweb.ts +++ b/src/qweb.ts @@ -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, diff --git a/tests/__snapshots__/qweb.test.ts.snap b/tests/__snapshots__/qweb.test.ts.snap index 71d98b75..b5402759 100644 --- a/tests/__snapshots__/qweb.test.ts.snap +++ b/tests/__snapshots__/qweb.test.ts.snap @@ -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`] = `"blue"`; + exports[`attributes dynamic attribute falsy variable 1`] = ` "function anonymous(context,extra ) { diff --git a/tests/qweb.test.ts b/tests/qweb.test.ts index ab6d69f8..aa95f61c 100644 --- a/tests/qweb.test.ts +++ b/tests/qweb.test.ts @@ -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", `
hey
` ); - 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", () => { ` ); - 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", + `
blue
` + ); + let dom: HTMLElement = renderToDOM(qweb, "test"); + expect(dom.innerHTML).toMatchSnapshot(); + }); +});