├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── transitions.js └── transitions.js.map ├── package-lock.json ├── package.json ├── src └── index.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | 5 | env: 6 | - NODE_ENV=development 7 | 8 | install: 9 | - npm install 10 | 11 | script: 12 | - npm test 13 | 14 | notifications: 15 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zacharias Enochsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is only compatible with hyperapp v1, and I do not intend to maintain it any longer. Hence, I am archiving it. 2 | 3 | # Hyperapp Transitions 4 | 5 | [![Travis CI](https://api.travis-ci.org/hyperapp/transitions.svg?branch=master)](https://travis-ci.org/hyperapp/transitions) [![npm](https://img.shields.io/npm/v/@hyperapp/transitions.svg)](https://www.npmjs.org/package/@hyperapp/transitions) [![Slack](https://hyperappjs.herokuapp.com/badge.svg)](https://hyperappjs.herokuapp.com "Join us") 6 | 7 | Hyperapp Transitions lets you animate your [Hyperapp](https://github.com/hyperapp/hyperapp) components as they are appear, dissapear and move around on the page. Use it to provide your user with important cues and a smoother experience. 8 | 9 | * **CSS Based** — Anything you can `transition` with CSS can be animated, including background-color, opacity, position and scale. 10 | * **Animate Layouts** — Reorder nodes in a list or flexbox-layout, and watch them gracefully *slide* into place 11 | * **Composable** — Stack multiple transitions with different delay and duration for complex animation effects. 12 | 13 | ## Getting Started 14 | 15 | Simply wrap your component in one of the decorator components exported from Hyperapp Transitions: 16 | 17 | ```jsx 18 | import {Enter} from "@hyperapp/transitions" 19 | 20 | const Notification = ({key, message}) => ( 21 | 22 |
23 | {message} 24 |
25 |
26 | ) 27 | ``` 28 | 29 | Use it as you would any component. The difference is, now your newly added `Notifications` will not just suddenly appear, but rather fade and slide in from the right. 30 | 31 | ## Installation 32 | 33 | Install with npm or Yarn. 34 | 35 |
 36 | npm i @hyperapp/transitions
 37 | 
38 | 39 | Then with a module bundler like [Rollup](https://rollupjs.org) or [Webpack](https://webpack.js.org), use as you would anything else. 40 | 41 | ```js 42 | import {Enter, Exit, Move} from "@hyperapp/transitions" 43 | ``` 44 | 45 | If you don't want to set up a build environment, you can download Hyperapp Transitions from a CDN like [unpkg.com](https://unpkg.com/@hyperapp/transitions) and it will be globally available through the window.transitions object. 46 | 47 | ```html 48 | 49 | ``` 50 | 51 | ## Overview 52 | 53 | Hyperapp Transitions exports three components: `Enter`, `Exit` and `Move` described below. Each take a number of attributes for defining the animation. 54 | 55 | The components are *decorator-components* (a.k.a "higher-order components"), which means that they do not add anything to the virtual DOM tree, but rather modify and return their children. 56 | 57 | ## Enter 58 | 59 | Use the Enter component to make elements appear in an animated fashion, when they are added to the DOM. 60 | 61 | ### Attributes 62 | 63 | The Enter component takes the following attributes: 64 | 65 | #### `css` 66 | 67 | The css overrides the element should have *before* it begins to appear. This can also be a function, allowing you to defer the decision until right before the animation starts. 68 | 69 | Default: `{}` 70 | 71 | #### `time` 72 | 73 | The duration of the transition in milliseconds. 74 | 75 | Default: `300`. 76 | 77 | #### `easing` 78 | 79 | A string with the [timing function](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function) of the transition. 80 | 81 | Default: `"linear"`. 82 | 83 | #### `delay` 84 | 85 | Wait this amount of milliseconds before starting the transition. 86 | 87 | Default: `0`. 88 | 89 | ### Example: 90 | 91 | ```jsx 92 | //make messages pop and fade in: 93 | 97 |

some message

98 |
99 | ``` 100 | 101 | ## Exit 102 | 103 | Use the Exit component to animate out elements before they are removed from the DOM. 104 | 105 | ### Attributes: 106 | 107 | The Exit component takes the following attributes: 108 | 109 | #### `css` 110 | 111 | The css overrides the element should have *after* it has left. This can also be a function, allowing you to defer the decision until right before the animation starts. 112 | 113 | Default: `{}` 114 | 115 | #### `time` 116 | 117 | The duration of the transition in milliseconds. 118 | 119 | Default: `300`. 120 | 121 | #### `easing` 122 | 123 | A string with the [timing function](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function) of the transition. 124 | 125 | Default: `"linear"` 126 | 127 | #### `delay` 128 | 129 | Wait this amount of milliseconds before beginning the transition. 130 | 131 | Default: `0`. 132 | 133 | #### `keep` 134 | 135 | When composing multiple Exit-transition components, set this to `true` on all but the last one, in order to prevent previous ones from removing the element when complete. 136 | 137 | Default: `false`. 138 | 139 | ### Example: 140 | 141 | ```jsx 142 | //make messages slide and fade out, and change backgroundcolor 143 | 147 |

some message

148 |
149 | ``` 150 | 151 | ## Move 152 | 153 | When the order of sibling nodes (items in a list, for example) changes, their elements are laid out in new positions on the page. When the sibling nodes are wrapped with the `Move` transition-component, they will glide smoothly to their new positions. 154 | 155 | Remember to key the nodes you wish to apply the `Move` transition to, so that the vdom engine is able to detct that it is the *order* that has changed, and not just all the attributes. 156 | 157 | ### Attributes: 158 | 159 | The `Move` component takes the following attributes: 160 | 161 | #### `time` 162 | 163 | The duration of the transition in milliseconds. 164 | 165 | Default: `300`. 166 | 167 | #### `easing` 168 | 169 | A string with the [timing function](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function) of the transition. 170 | 171 | Default: `"linear"`. 172 | 173 | ### Example: 174 | 175 | ```jsx 176 | 177 | 186 | ``` 187 | 188 | ## Composing Transitions 189 | 190 | The transition components work by adding handlers for the `oncreate`, `onremove` and/or `onupdate` lifecycle events to their children. If a child already has a handler for those lifecycle events, it is not overwritten. Rather, we compose the transition-handlers with existing lifecycle event handlers. 191 | 192 | This makes it possible to compose multiple transition components on top of eachother. 193 | 194 | ### Example: 195 | 196 | ```jsx 197 | const FadeInPopOut = (props, children) => ( 198 | 199 | 200 | {children} 201 | 202 | 203 | ) 204 | ``` 205 | 206 | ## Keys 207 | 208 | As a general rule: make sure to have keys on all nodes you apply transitions to. 209 | 210 | The lifecycle events which trigger the transitions, are based on *elements* - not virtual nodes. Without keys, Hyperapp's virtual DOM engine will often associate the a nodes with an unintended element in the real DOM, with unexpected transition-behavior as a consequence. 211 | 212 | 213 | ## Examples 214 | 215 | Please have a look at these live, editable examples, for some ideas of what is possible: 216 | 217 | - **[Toasts](https://codepen.io/zaceno/pen/QOYOZd)** - Combining Enter, Exit and Move transitions for an elegant notification display. 218 | - **[15-puzzle](https://codepen.io/zaceno/pen/XzOwPd)** - Slide squares around a grid using the Move transition 219 | - **[Carousel](https://codepen.io/zaceno/pen/ZawNmb)** - A situation you'll need deferred css for the Exit transition. 220 | 221 | ## Bugs & Questions 222 | 223 | Do you have questions? Or think you've found a bug? Please file an issue att https://github.com/hyperapp/transitions 224 | 225 | Or come join the us on the [Hyperapp Slack](https://hyperappjs.herokuapp.com)! 226 | 227 | ## License 228 | 229 | Hyperapp Transitions is MIT licensed. See [LICENSE](LICENSE.md). 230 | -------------------------------------------------------------------------------- /dist/transitions.js: -------------------------------------------------------------------------------- 1 | !function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(n.transitions={})}(this,function(n){"use strict";function t(n,t){Object.keys(t).forEach(function(e){n.style[e]=t[e]})}function e(){a.forEach(r)}function o(n){var t=n._x,e=n._y;if(!t)return"translate(0, 0)";var o=r(n);return"translate("+Math.floor(t-o.x)+"px, "+Math.floor(e-o.y)+"px)"}function r(n){var t=n.getBoundingClientRect();return n._x=t.left,n._y=t.top,{x:t.left,y:t.top}}function u(n,e,o,r,u){var i=e.easing||"linear",f=e.time||300,c=e.delay||0;t(n,o),setTimeout(function(){requestAnimationFrame(function(){t(n,r),n.style.transition="all "+i+" "+f+"ms",setTimeout(function(){n.style.transition=null,u&&u()},f)})},c)}function i(n,t,e,r){"function"==typeof e&&(e=e()),function(n){var t=a.indexOf(n);-1!==t&&a.splice(t,1)}(n);var i=o(n);e.transform=i+(e.transform?" "+e.transform:""),u(n,t,{transform:i},e,r)}function f(){}function c(n){return function(t,e){var o=n(t||{});return e.filter(function(n){return!!n.attributes}).map(function(n){return["oncreate","onupdate","onremove"].forEach(function(t){n.attributes[t]=function(n,t){return n?t?function(e,o){return n&&n(e,o),t&&t(e,o),f}:n:t}(n.attributes[t],o[t])}),n})}}addEventListener("resize",e),addEventListener("scroll",e);var a=[],s=c(function(n){return{oncreate:function(n){!function(n){a.indexOf(n)>-1||(a.push(n),setTimeout(function(){r(n)},0))}(n)}}}),l=c(function(n){return{onupdate:function(t){!function(n,t){u(n,t,{transform:o(n)},{transform:null})}(t,n)}}}),d=c(function(n){return{onremove:function(t,e){e=e||function(){!function(n){n.parentNode.removeChild(n)}(t)},i(t,n,n.css||{},!n.keep&&e)}}}),p=c(function(n){return{oncreate:function(t){!function(n,t,e){"function"==typeof e&&(e=e()),u(n,t,e,Object.keys(e).reduce(function(n,t){return n[t]=null,n},{}),function(){r(n)})}(t,n,n.css||{})}}});n.Enter=p,n.Move=function(n,t){return l(n,s(null,t))},n.Exit=function(n,t){return d(n,s(null,t))},Object.defineProperty(n,"__esModule",{value:!0})}); 2 | //# sourceMappingURL=transitions.js.map -------------------------------------------------------------------------------- /dist/transitions.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["dist/transitions.js"],"names":["global","factory","exports","module","define","amd","transitions","this","setStyle","el","attr","Object","keys","forEach","name","style","updateAllTracked","trackingRegistry","updateTracking","invertLastMove","x","_x","y","_y","n","Math","floor","rect","getBoundingClientRect","left","top","runTransition","before","after","ondone","easing","time","delay","setTimeout","requestAnimationFrame","transition","runExit","css","done","i","indexOf","splice","unregisterTracking","translation","transform","noop","transitionComponent","handlersFn","children","handlers","filter","child","attributes","map","f1","f2","composeHandlers","addEventListener","_track","oncreate","push","registerTracking","_move","onupdate","runMove","_exit","onremove","parentNode","removeChild","removeElement","keep","Enter","reduce","o","runEnter","Move","Exit","defineProperty","value"],"mappings":"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","sourcesContent":["(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n\ttypeof define === 'function' && define.amd ? define(['exports'], factory) :\n\t(factory((global.transitions = {})));\n}(this, (function (exports) { 'use strict';\n\naddEventListener('resize', updateAllTracked);\naddEventListener('scroll', updateAllTracked);\n\nvar trackingRegistry = [];\n\nfunction removeElement (el) {\n el.parentNode.removeChild(el);\n}\n\nfunction setStyle (el, attr) {\n Object.keys(attr)\n .forEach(function (name) {\n el.style[name] = attr[name];\n });\n}\n\nfunction registerTracking (el) {\n if (trackingRegistry.indexOf(el) > -1 ) return\n trackingRegistry.push(el);\n setTimeout(function () {\n updateTracking(el);\n }, 0);\n}\n\nfunction unregisterTracking(el) {\n var i = trackingRegistry.indexOf(el);\n if (i === -1) return\n trackingRegistry.splice(i, 1);\n}\n\nfunction updateAllTracked () {\n trackingRegistry.forEach(updateTracking); \n} \n\nfunction invertLastMove (el) {\n var x = el._x;\n var y = el._y;\n if (!x) return 'translate(0, 0)'\n var n = updateTracking(el);\n var dx = Math.floor(x - n.x);\n var dy = Math.floor(y - n.y);\n return 'translate(' + dx + 'px, ' + dy + 'px)'\n}\n\nfunction updateTracking (el) {\n var rect = el.getBoundingClientRect();\n el._x = rect.left;\n el._y = rect.top;\n return {x: rect.left, y: rect.top}\n}\n\nfunction runTransition (el, attr, before, after, ondone) {\n var easing = attr.easing || 'linear';\n var time = attr.time || 300;\n var delay = attr.delay || 0;\n setStyle(el, before);\n setTimeout(function () {\n requestAnimationFrame(function () {\n setStyle(el, after);\n el.style.transition = 'all ' + easing + ' ' + time + 'ms';\n setTimeout(function () {\n el.style.transition = null;\n ondone && ondone();\n }, time);\n });\n }, delay);\n}\n\nfunction runEnter (el, attr, css) {\n if (typeof css === 'function') css = css();\n runTransition(\n el, attr,\n css,\n Object.keys(css).reduce(function (o, n) {\n o[n] = null;\n return o\n }, {}),\n function () { updateTracking(el); }\n );\n}\n\nfunction runMove (el, attr) {\n runTransition(\n el, attr,\n {transform: invertLastMove(el)},\n {transform: null}\n );\n}\n\nfunction runExit (el, attr, css, done) {\n if (typeof css === 'function') css = css();\n unregisterTracking(el);\n var translation = invertLastMove(el);\n css.transform = translation + (css.transform ? (' ' + css.transform) : '');\n runTransition(\n el, attr,\n {transform: translation},\n css,\n done\n );\n}\n \nfunction noop () {}\n \nfunction composeHandlers (f1, f2) {\n if (!f1) return f2\n if (!f2) return f1\n return function (el, done) {\n f1 && f1(el, done);\n f2 && f2(el, done);\n return noop\n }\n}\n\nfunction transitionComponent (handlersFn) {\n return function (attr, children) {\n var handlers = handlersFn(attr || {});\n return children\n .filter(function (child) { return !!child.attributes})\n .map(function (child) {\n ['oncreate', 'onupdate', 'onremove'].forEach(function (n) {\n child.attributes[n] = composeHandlers(child.attributes[n], handlers[n]);\n }); \n return child\n })\n }\n}\n\nvar _track = transitionComponent(function (attr) { \n return {oncreate: function (el) { registerTracking(el);} }\n});\n\nvar _move = transitionComponent(function (attr) { \n return { onupdate: function (el) { runMove(el, attr); } }\n});\n\nvar _exit = transitionComponent(function (attr) {\n return {\n onremove: function (el, done) {\n done = done || function () { removeElement(el); };\n runExit(el, attr, attr.css || {}, !attr.keep && done);\n }\n }\n});\n\nvar Enter = transitionComponent(function (attr) {\n return { oncreate: function (el) { runEnter(el, attr, attr.css || {}); } }\n});\n\nvar Move = function (attr, children) {\n return _move(attr, _track(null, children))\n};\n\nvar Exit = function (attr, children) {\n return _exit(attr, _track(null, children))\n};\n\nexports.Enter = Enter;\nexports.Move = Move;\nexports.Exit = Exit;\n\nObject.defineProperty(exports, '__esModule', { value: true });\n\n})));\n//# sourceMappingURL=transitions.js.map\n"]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperapp-transitions", 3 | "version": "1.0.2", 4 | "description": "Transitions for Hyperapp", 5 | "main": "dist/transitions.js", 6 | "module": "src/index.js", 7 | "scripts": { 8 | "umd": "rollup src/index.js -f umd -n transitions -m -o dist/transitions.js", 9 | "minify": "uglifyjs dist/transitions.js -o dist/transitions.js -mc --source-map includeSources,url=transitions.js.map", 10 | "build": "npm run umd && npm run minify", 11 | "test": "npm run build && ava test/*.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/HyperappCommunity/transitions.git" 16 | }, 17 | "author": "Zacharias Enochsson", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/HyperappCommunity/transitions/issues" 21 | }, 22 | "homepage": "https://github.com/HyperappCommunity/transitions#readme", 23 | "devDependencies": { 24 | "ava": "^1.0.0-beta.3", 25 | "hyperapp": "^1.1.2", 26 | "jsdom": "^11.6.2", 27 | "rollup": "^0.52.2", 28 | "uglify-js": "^3.2.2" 29 | }, 30 | "dependencies": {} 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | addEventListener('resize', updateAllTracked) 2 | addEventListener('scroll', updateAllTracked) 3 | 4 | var trackingRegistry = [] 5 | 6 | function removeElement (el) { 7 | el.parentNode.removeChild(el) 8 | } 9 | 10 | function setStyle (el, attr) { 11 | Object.keys(attr) 12 | .forEach(function (name) { 13 | el.style[name] = attr[name] 14 | }) 15 | } 16 | 17 | function registerTracking (el) { 18 | if (trackingRegistry.indexOf(el) > -1 ) return 19 | trackingRegistry.push(el) 20 | setTimeout(function () { 21 | updateTracking(el) 22 | }, 0) 23 | } 24 | 25 | function unregisterTracking(el) { 26 | var i = trackingRegistry.indexOf(el) 27 | if (i === -1) return 28 | trackingRegistry.splice(i, 1) 29 | } 30 | 31 | function updateAllTracked () { 32 | trackingRegistry.forEach(updateTracking) 33 | } 34 | 35 | function invertLastMove (el) { 36 | var x = el._x 37 | var y = el._y 38 | if (!x) return 'translate(0, 0)' 39 | var n = updateTracking(el) 40 | var dx = Math.floor(x - n.x) 41 | var dy = Math.floor(y - n.y) 42 | return 'translate(' + dx + 'px, ' + dy + 'px)' 43 | } 44 | 45 | function updateTracking (el) { 46 | var rect = el.getBoundingClientRect() 47 | el._x = rect.left 48 | el._y = rect.top 49 | return {x: rect.left, y: rect.top} 50 | } 51 | 52 | function runTransition (el, attr, before, after, ondone) { 53 | var easing = attr.easing || 'linear' 54 | var time = attr.time || 300 55 | var delay = attr.delay || 0 56 | setStyle(el, before) 57 | setTimeout(function () { 58 | requestAnimationFrame(function () { 59 | setStyle(el, after) 60 | el.style.transition = 'all ' + easing + ' ' + time + 'ms' 61 | setTimeout(function () { 62 | el.style.transition = null 63 | ondone && ondone() 64 | }, time) 65 | }) 66 | }, delay) 67 | } 68 | 69 | function runEnter (el, attr, css) { 70 | if (typeof css === 'function') css = css() 71 | runTransition( 72 | el, attr, 73 | css, 74 | Object.keys(css).reduce(function (o, n) { 75 | o[n] = null 76 | return o 77 | }, {}), 78 | function () { updateTracking(el) } 79 | ) 80 | } 81 | 82 | function runMove (el, attr) { 83 | runTransition( 84 | el, attr, 85 | {transform: invertLastMove(el)}, 86 | {transform: null} 87 | ) 88 | } 89 | 90 | function runExit (el, attr, css, done) { 91 | if (typeof css === 'function') css = css() 92 | unregisterTracking(el) 93 | var translation = invertLastMove(el) 94 | css.transform = translation + (css.transform ? (' ' + css.transform) : '') 95 | runTransition( 96 | el, attr, 97 | {transform: translation}, 98 | css, 99 | done 100 | ) 101 | } 102 | 103 | function noop () {} 104 | 105 | function composeHandlers (f1, f2) { 106 | if (!f1) return f2 107 | if (!f2) return f1 108 | return function (el, done) { 109 | f1 && f1(el, done) 110 | f2 && f2(el, done) 111 | return noop 112 | } 113 | } 114 | 115 | function transitionComponent (handlersFn) { 116 | return function (attr, children) { 117 | var handlers = handlersFn(attr || {}) 118 | return children 119 | .filter(function (child) { return !!child.attributes}) 120 | .map(function (child) { 121 | ['oncreate', 'onupdate', 'onremove'].forEach(function (n) { 122 | child.attributes[n] = composeHandlers(child.attributes[n], handlers[n]) 123 | }) 124 | return child 125 | }) 126 | } 127 | } 128 | 129 | var _track = transitionComponent(function (attr) { 130 | return {oncreate: function (el) { registerTracking(el)} } 131 | }) 132 | 133 | var _move = transitionComponent(function (attr) { 134 | return { onupdate: function (el) { runMove(el, attr) } } 135 | }) 136 | 137 | var _exit = transitionComponent(function (attr) { 138 | return { 139 | onremove: function (el, done) { 140 | done = done || function () { removeElement(el) } 141 | runExit(el, attr, attr.css || {}, !attr.keep && done) 142 | } 143 | } 144 | }) 145 | 146 | var Enter = transitionComponent(function (attr) { 147 | return { oncreate: function (el) { runEnter(el, attr, attr.css || {}) } } 148 | }) 149 | 150 | var Move = function (attr, children) { 151 | return _move(attr, _track(null, children)) 152 | } 153 | 154 | var Exit = function (attr, children) { 155 | return _exit(attr, _track(null, children)) 156 | } 157 | 158 | export {Enter, Move, Exit} -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import {JSDOM} from 'jsdom' 2 | const dom = new JSDOM('') 3 | global.window = dom.window 4 | global.document = dom.window.document 5 | global.addEventListener = dom.window.addEventListener 6 | global.requestAnimationFrame = fn => setTimeout(fn, 0) 7 | 8 | import test from 'ava' 9 | import {h, app} from 'hyperapp' 10 | const {Enter, Exit, Move} = require('../dist/transitions.js') 11 | 12 | function createContainer () { 13 | const el = document.createElement('div') 14 | document.body.appendChild(el) 15 | return el 16 | } 17 | 18 | test.cb('Enter', t => { 19 | const el = createContainer() 20 | app({}, {}, _ => h('main', {}, [ 21 | Enter({css:{prop:'foo'}, time: 100, easing: 'bar', delay: 100}, [ 22 | h('div', {id: 'target'}, ['foo']) 23 | ]) 24 | ]), el) 25 | setTimeout(_ => { 26 | setTimeout(_ => { 27 | t.is(el.querySelector('#target').style.prop, 'foo') 28 | t.falsy(el.querySelector('#target').style.transition) 29 | setTimeout(_ => { 30 | t.falsy(el.querySelector('#target').style.prop) 31 | t.is(el.querySelector('#target').style.transition, 'all bar 100ms') 32 | setTimeout(_ => { 33 | t.falsy(el.querySelector('#target').style.transition) 34 | t.falsy(el.querySelector('#target').style.prop) 35 | t.end() 36 | }, 100) 37 | }, 100) 38 | }, 50) 39 | }, 0) 40 | }) 41 | 42 | test.cb('Enter - deferred css', t => { 43 | const el = createContainer() 44 | app({}, {}, _ => h('main', {}, [ 45 | Enter({css:_ => ({prop: 'foo'})}, [ 46 | h('div', {id: 'target'}, ['foo']) 47 | ]) 48 | ]), el) 49 | setTimeout(_ => { 50 | setTimeout(_ => { 51 | t.is(el.querySelector('#target').style.prop, 'foo') 52 | t.end() 53 | }, 0) 54 | }, 0) 55 | }) 56 | 57 | 58 | 59 | test.cb('Exit', t => { 60 | const el = createContainer() 61 | const {toggle} = app( 62 | {show: true}, 63 | {toggle: _ => ({show: false})}, 64 | (state, actions) => h('main', {}, [ 65 | state.show && Exit({ 66 | css: {prop: 'foo'}, 67 | easing: 'bar', 68 | time: 100, 69 | delay: 100 70 | }, [ 71 | h('div', {id: 'target'}, ['foo']) 72 | ]) 73 | ]), 74 | el 75 | ) 76 | //render once 77 | setTimeout(_ => { 78 | toggle() //make element go away 79 | //render again 80 | setTimeout(_ => { 81 | t.falsy(el.querySelector('#target').style.prop) 82 | t.falsy(el.querySelector('#target').style.transition) 83 | setTimeout(_ => { 84 | t.is(el.querySelector('#target').style.prop, 'foo') 85 | t.is(el.querySelector('#target').style.transition, 'all bar 100ms') 86 | setTimeout(_ => { 87 | t.falsy(el.querySelector('#target')) 88 | t.end() 89 | }, 150) 90 | }, 100) 91 | }, 50) 92 | }, 0) 93 | }) 94 | 95 | 96 | 97 | test.cb('Exit - deferred css', t => { 98 | const el = createContainer() 99 | const {toggle} = app( 100 | {show: true}, 101 | {toggle: _ => ({show: false})}, 102 | (state, actions) => h('main', {}, [ 103 | state.show && Exit({ 104 | css: _ => ({prop: 'foo'}), 105 | easing: 'bar', 106 | time: 100, 107 | delay: 100 108 | }, [ 109 | h('div', {id: 'target'}, ['foo']) 110 | ]) 111 | ]), 112 | el 113 | ) 114 | //render once 115 | setTimeout(_ => { 116 | toggle() //make element go away 117 | setTimeout(_ => { 118 | t.is(el.querySelector('#target').style.prop, 'foo') 119 | t.end() 120 | }, 200) 121 | }, 0) 122 | }) 123 | 124 | 125 | test.cb('Exit - keep: true', t => { 126 | const el = createContainer() 127 | const {toggle} = app( 128 | {show: true}, 129 | {toggle: _ => ({show: false})}, 130 | (state, actions) => h('main', {}, [ 131 | state.show && Exit({ 132 | css: {prop: 'foo'}, 133 | time: 100, 134 | keep: true, 135 | }, [ 136 | h('div', {id: 'target'}, ['foo']) 137 | ]) 138 | ]), 139 | el 140 | ) 141 | //render once 142 | setTimeout(_ => { 143 | toggle() //make element go away 144 | //render again 145 | setTimeout(_ => { 146 | t.truthy(el.querySelector('#target')) 147 | t.end() 148 | }, 150) 149 | }, 0) 150 | }) 151 | 152 | test.cb('Move', t => { 153 | const el = createContainer() 154 | const {toggle} = app( 155 | {order: true}, 156 | {toggle: _ => ({order: false})}, 157 | (state, actions) => h('main', {}, Move({ 158 | time: 200, 159 | easing: 'bar' 160 | }, (state.order 161 | ? [ 162 | h('p', {key: 'first', id: 'first'}, []), 163 | h('p', {key: 'second', id: 'second'}, []), 164 | ] 165 | : [ 166 | h('p', {key: 'second', id: 'second'}, []), 167 | h('p', {key: 'first', id: 'first'}, []), 168 | ] 169 | ))), 170 | el 171 | ) 172 | setTimeout(_ => { 173 | toggle() 174 | setTimeout(_ => { 175 | t.is(el.querySelector('#first').style.transition, 'all bar 200ms') 176 | t.is(el.querySelector('#second').style.transition, 'all bar 200ms') 177 | setTimeout(_ => { 178 | t.falsy(el.querySelector('#first').style.transition) 179 | t.falsy(el.querySelector('#second').style.transition) 180 | t.end() 181 | }, 150) 182 | }, 100) 183 | }, 0) 184 | }) --------------------------------------------------------------------------------