├── .versions ├── README.md ├── flow-transition-tests.js ├── flow-transition.js ├── package.js └── section.html /.versions: -------------------------------------------------------------------------------- 1 | base64@1.0.3 2 | binary-heap@1.0.3 3 | blaze@2.1.2 4 | blaze-tools@1.0.3 5 | callback-hook@1.0.3 6 | check@1.0.5 7 | coffeescript@1.0.6 8 | cosmos:browserify@0.5.0 9 | ddp@1.1.0 10 | deps@1.0.7 11 | ejson@1.0.6 12 | geojson-utils@1.0.3 13 | html-tools@1.0.4 14 | htmljs@1.0.4 15 | id-map@1.0.3 16 | jquery@1.11.3_2 17 | json@1.0.3 18 | kadira:flow-router@2.1.1 19 | local-test:mcissel:flow-transition@1.0.1 20 | logging@1.0.7 21 | mcissel:flow-transition@1.0.1 22 | meteor@1.1.6 23 | minifiers@1.1.5 24 | minimongo@1.0.8 25 | mongo@1.1.0 26 | observe-sequence@1.0.6 27 | ordered-dict@1.0.3 28 | random@1.0.3 29 | reactive-dict@1.1.0 30 | reactive-var@1.0.5 31 | retry@1.0.3 32 | spacebars-compiler@1.0.6 33 | templating@1.1.1 34 | tinytest@1.0.5 35 | tracker@1.0.7 36 | underscore@1.0.3 37 | velocityjs:velocityjs@1.2.1 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlowTransition (mcissel:flow-transition) 2 | 3 | ## A Blaze Layout Renderer and Transitioner 4 | A Blaze layout renderer that uses VelocityJS powered transitions. It currently works only with [Kadira Flow-Router](https://github.com/kadirahq/flow-router) (previously meteorhacks:flow-router). This renderer enables: 5 | 6 | * Rendering layouts to different sections (regions) using Blaze 7 | * Assign route-to-route transitions for each section 8 | * Transitions can be full page motion, or Velocity / VelocityUI Pack animations 9 | * Sections will only re-render when necessary or when explicitly requested 10 | * Not all sections have to be used on every route 11 | 12 | ## Demo 13 | 14 | [flowtransition.meteor.com/](http://flowtransition.meteor.com/) 15 | 16 | [https://github.com/mcissel/flowtransition-demo](https://github.com/mcissel/flowtransition-demo) 17 | 18 | ## Getting Started 19 | 20 | Add this package and kadira:flow-router (if you haven't already): 21 | ~~~ 22 | meteor add kadira:flow-router 23 | meteor add mcissel:flow-transition 24 | ~~~ 25 | 26 | You will need a few templates and a couple routes, before you can add a transition. 27 | 28 | ~~~html 29 | 30 | {{> section name="head"}} 31 | {{> section name="body"}} 32 | 33 | 34 | 37 | 38 | 41 | 42 | 47 | ~~~ 48 | 49 | And set up these routes. **Route names are required**, because that is how the transitions are assigned 50 | ~~~js 51 | if (Meteor.isClient) { 52 | FlowRouter.route("/", { 53 | name: "home", // required 54 | action: function() { 55 | FlowTransition.flow({body: "welcome"}); 56 | } 57 | }); 58 | 59 | FlowRouter.route("/articles", { 60 | name: "articles", // required 61 | action: function() { 62 | FlowTransition.flow({head: "header"}, {body: "articles"}); 63 | } 64 | }); 65 | } 66 | ~~~ 67 | 68 | Now you can set up these transitions by assigning the: 69 | 70 | * Section that will be animated (`section`) 71 | * Name of departure route (`from`) 72 | * Name of destination route (`to`) 73 | * Transition object (`txFull` or `txIn`/`txOut`) 74 | 75 | ~~~js 76 | FlowTransition.addTransition({ 77 | section: 'body', 78 | from: 'home', 79 | to: 'articles', 80 | txFull: 'left' // direction of motion, content will be moving left 81 | }); 82 | 83 | FlowTransition.addTransition({ 84 | section: 'head', 85 | from: 'home', 86 | to: 'articles', 87 | txIn: { 88 | hooks: {translateY: '-100%'}, 89 | properties: {translateY: [0, '-100%']}, 90 | options: { 91 | duration: 220, 92 | easing: 'spring', 93 | queue: false, 94 | complete: function() { 95 | console.log('You can add a callback function here'); 96 | } 97 | } 98 | } 99 | }); 100 | ~~~ 101 | 102 | ### Re-rendering 103 | When a section's content doesn't change, it will not re-render, UNLESS you assign a transition to that section for that route. For example, if you change your header template to: 104 | 105 | ~~~html 106 | 109 | 110 | 111 | ~~~ 112 | 113 | and add the following code: 114 | 115 | ~~~js 116 | FlowRouter.route("/about", 117 | name: "about", 118 | action: function() { 119 | FlowTransition.flow({head: "header"}, {body: "about_us"}); 120 | } 121 | }); 122 | 123 | FlowTransition.addTransition({ 124 | section: 'head', 125 | from: 'articles', 126 | to: 'about', 127 | txIn: 'flipBounceYIn' 128 | }); 129 | ~~~ 130 | 131 | In the above case, the `head` section is re-rendered when navigating from '/articles' to '/about', but it will not be re-rendered when navigating from '/' to '/about'. 132 | 133 | ~~~js 134 | ~~~ 135 | 136 | ## Transition Animations 137 | 138 | ### Full Screen (full section) Transitions 139 | 4 full screen transitions are included by default. Technically, they are "full section" transitions, wherein they use 100% of the width and height of the section. Also see: [CSS Suggestions](#css-suggestions) 140 | 141 | Use a txFull transition object. The name is one of `[left, right, down, up]` referring to the content's direction of travel. Use just the string, or use it with an `options` object, just like a VelocityJS UI Pack animation. A pair transitions using both forms: 142 | 143 | ~~~js 144 | FlowTransition.addTransition({ 145 | section: 'body', 146 | from: 'home', 147 | to: 'articles', 148 | txFull: 'left' 149 | }); 150 | 151 | FlowTransition.addTransition({ 152 | section: 'body', 153 | from: 'articles', 154 | to: 'home', 155 | txFull: 156 | properties: 'right', 157 | options: { 158 | duration: 350, 159 | easing: 'spring', 160 | queue: false 161 | } 162 | }); 163 | ~~~ 164 | 165 | Currently, if you use `txFull`, the Flow Transition package will not evaluate the `txIn` or `txOut` objects. 166 | 167 | ### VelocityJS Animations 168 | The last transition example in the [Getting Started](#getting-started) section shows a VelocityJS animation for the transion in object (`txIn`). Both `txIn` and `txOut` are optional. In that example, I did not include a transition out object (`txOut`) since there was no `head` content present in the `from` route. 169 | 170 | A transition object has three parts, each of which hold "key, value" objects: 171 | 172 | The `hooks` will be applied BEFORE the transition animation starts, via a $.Velocity.hook() call 173 | ``` 174 | hooks: {translateY: '-100%'}, 175 | ``` 176 | The `properties` object holds either a string name or animation properties. These are VelocityJS animation properties 177 | ``` 178 | properties: {translateY: [0, '-100%']}, // move to translateY = 0, from translateY = -100% 179 | ``` 180 | The `options` object is a set of VelocityJS options 181 | ``` 182 | options: {duration: 220, easing: 'spring', queue: false} 183 | ``` 184 | 185 | ## CSS Suggestions 186 | CSS info 187 | The `section` templates include a wrapper div with the class `transitionable-section`. This div is where Blaze renders the templates. You can also style them by using their id, which is the same as their section name. 188 | 189 | ~~~css 190 | body { 191 | margin: 0; 192 | overflow: hidden; 193 | } 194 | 195 | .transitionable-section { 196 | position: absolute; 197 | left: 0; 198 | right: 0; 199 | } 200 | 201 | #head { 202 | height: 40px; 203 | } 204 | 205 | #body { 206 | top: 40px; 207 | } 208 | ~~~ 209 | I suggest using at least the above CSS, plus: the inner templates should fill their parent section 210 | 211 | ## `@TODO:` 212 | - *Put a delay or stop all animations to fix the multiple animations bug 213 | - Offer default animation on a per section basis 214 | - Add automatic reverse animation for return routes 215 | - Allow for multiple parent layouts 216 | - Make some tests for tinyTest 217 | -------------------------------------------------------------------------------- /flow-transition-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('example', function (test) { 2 | test.equal(true, true); 3 | }); 4 | -------------------------------------------------------------------------------- /flow-transition.js: -------------------------------------------------------------------------------- 1 | FlowTransition = {}; 2 | FlowTransition.transitionStore = {}; 3 | FlowTransition._sections = {}; 4 | 5 | var _ready = false; 6 | 7 | /** 8 | * The animations will be registered like usual velocity animations, using 9 | * a two part array: 10 | * {translateX: [0, -10]}, {easing: "spring", duration: 325} 11 | * -or use a VelocityUI animation: 12 | * "transition.fadeIn", {delay: 400, duration: 325} 13 | * -or- use an built in txFull screen traversal: 14 | * "down", {easing: "spring", duration: 325} 15 | * You can use a txFull screen option "down", which refers to the direction 16 | * of travel of the template. It can be moving: down, up, left, or right 17 | */ 18 | 19 | Template.section.rendered = function() { 20 | section = this.data.name; 21 | FlowTransition._sections[section] = document.getElementById(section); 22 | }; 23 | 24 | function _setUiHooks(parentElement, transitions) { 25 | if (!parentElement) { 26 | return; 27 | } 28 | 29 | var uiHooks = {}; 30 | 31 | if (transitions) { 32 | 33 | if (transitions.txIn) { 34 | uiHooks.insertElement = function(node) { 35 | var _tx = transitions.txIn; 36 | 37 | // set up the hooks to apply properties before insertion 38 | _.each(_tx.hook, function(value, property) { 39 | $.Velocity.hook(node, property, value); 40 | }); 41 | 42 | // insert the new element 43 | $(node).prependTo(parentElement); 44 | 45 | // start the animation when the DOM is ready 46 | Meteor.defer(function() { 47 | $(node).velocity(_tx); 48 | }); 49 | }; 50 | } 51 | 52 | if (transitions.txOut) { 53 | uiHooks.removeElement = function(node) { 54 | var _tx = transitions.txOut; 55 | 56 | // callback = node.remove + user defined callback 57 | _tx.options = _tx.options || {}; 58 | _tx.options.complete = (function(complete) { 59 | 60 | return function() { 61 | if (complete) { 62 | complete.apply(complete, arguments); 63 | } 64 | $(node).remove(); 65 | }; 66 | 67 | })(_tx.options.complete); 68 | 69 | // set up the hooks to apply properties before insertion 70 | _.each(_tx.hook, function(value, property) { 71 | $.Velocity.hook(node, property, value); 72 | }); 73 | 74 | // start the animation when the DOM is ready 75 | Meteor.defer(function() { 76 | $(node).velocity(_tx); 77 | }); 78 | }; 79 | } 80 | } 81 | 82 | parentElement._uihooks = uiHooks; 83 | } 84 | 85 | function _attachDeepObject() { 86 | var missingKey = false; 87 | 88 | _.reduce(arguments, function(mem, key) { 89 | 90 | if (!key) { 91 | missingKey = true; 92 | } 93 | 94 | return mem = mem[key] = mem[key] || {}; 95 | 96 | }, this); 97 | 98 | return !missingKey; 99 | } 100 | 101 | function _attachFullPageAnimations() { 102 | var _txName, _options, _property, _value, _valueOpposite; 103 | 104 | // can either be a string name, or an object in the form {properties: {}, options: {}} 105 | this.txFull = (typeof this.txFull === 'string') ? {properties: this.txFull} : this.txFull; 106 | // if (typeof this.txFull === 'string') { 107 | // this.txFull = [this.txFull]; 108 | // } 109 | _txName = this.txFull.properties; 110 | _options = this.txFull.options || { 111 | duration: 350, 112 | easing: 'ease-out', 113 | queue: false 114 | }; 115 | 116 | // ensure that a valid full page transition is being requested 117 | if (_.indexOf(['down', 'up', 'left', 'right'], _txName) === -1) { 118 | console.log("'" + _txName + "' is not a registered txFull page transition"); 119 | return; 120 | } 121 | 122 | // get the property and value for the animation 123 | _property = (_txName === 'down' || _txName === 'up') ? 'translateY' : 'translateX'; 124 | _value = (_txName === 'down' || _txName === 'right') ? '-100%' : '100%'; 125 | _valueOpposite = (_txName === 'down' || _txName === 'right') ? '100%' : '-100%'; 126 | 127 | // attach the txIn and txOut objects 128 | this.txOut = {properties: {} }; 129 | this.txIn = {pre: {}, properties: {}, hooks: {}}; 130 | 131 | this.txIn.hooks[_property] = _value; 132 | this.txIn.properties[_property] = [0, _value]; 133 | 134 | this.txOut.properties[_property] = [_valueOpposite, 0]; 135 | this.txOut.options = this.txIn.options = _options; 136 | } 137 | 138 | // FlowTransition.transitionStore holds objects in the form: 139 | // [section][newRoute][oldRoute][txDirection]{ TRANSITION OBJECT } 140 | FlowTransition.addTransition = function(transition) { 141 | var _tx = transition; 142 | var _fts = FlowTransition.transitionStore; 143 | 144 | var attached = _attachDeepObject.apply(_fts, [_tx.section, _tx.to, _tx.from]); 145 | if (!attached) { 146 | console.log("A FlowTransition transition object must have the parameters:" + 147 | " section, from, to; and should have the parameters: txFull or txIn & txOut."); 148 | } 149 | 150 | if (_tx.txFull) { 151 | _attachFullPageAnimations.call(_tx); 152 | } 153 | if (_tx.txIn) { 154 | var _txIn = (typeof _tx.txIn === 'string') ? {properties: _tx.txIn} : _tx.txIn; 155 | _fts[_tx.section][_tx.to][_tx.from].txIn = _txIn; 156 | } 157 | if (_tx.txOut) { 158 | var _txOut = (typeof _tx.txOut === 'string') ? {properties: _tx.txOut} : _tx.txOut; 159 | _fts[_tx.section][_tx.to][_tx.from].txOut = _txOut; 160 | } 161 | }; 162 | 163 | FlowTransition.applyTransitions = function(newRoute, oldRoute) { 164 | var _fts = FlowTransition.transitionStore; 165 | var hasTransition = {}; 166 | 167 | _.each(FlowTransition._sections, function(parentElement, section) { 168 | 169 | // get the transition object or set it to null 170 | var transitions = oldRoute && _fts[section] && _fts[section][newRoute] && _fts[section][newRoute][oldRoute] && 171 | _fts[section][newRoute][oldRoute]; 172 | hasTransition[section] = transitions ? true : false; 173 | 174 | // when transitions is null, stale _uihooks will be removed from the parentElement 175 | _setUiHooks(parentElement, transitions); 176 | }); 177 | 178 | // let the caller know which sections have transitions 179 | return hasTransition; 180 | }; 181 | 182 | /** 183 | * This is the function to call externally to kick off a layout render. 184 | * 185 | * arguments = are in the form: 186 | * [{section1: contentTemplateName2}, {section2: contentTemplateName2}, ...] 187 | */ 188 | FlowTransition.flow = function() { 189 | var layoutAssignment = arguments; 190 | 191 | if (!_ready) { // make sure the initial Template sections are loaded 192 | Meteor.defer(function() { 193 | _ready = true; 194 | FlowTransition.flow.apply(this, layoutAssignment); 195 | }); 196 | return; 197 | } 198 | 199 | var _newLayout = _.extend.apply(null, layoutAssignment); 200 | 201 | var flowCurrent = FlowRouter.current(); 202 | var newRoute = flowCurrent.route.name; 203 | var oldRoute = flowCurrent.oldRoute ? flowCurrent.oldRoute.name : null; 204 | var hasTransition = FlowTransition.applyTransitions(newRoute, oldRoute); 205 | 206 | _.each(FlowTransition._sections, function(parentElement, section) { 207 | var oldNode = parentElement.firstElementChild; 208 | if (!_newLayout[section]) { 209 | if (oldNode) { 210 | Blaze.remove(Blaze.getView(oldNode)); 211 | } 212 | } 213 | else { 214 | var newContent = Template[_newLayout[section]]; 215 | var sameContent = (oldNode && (Blaze.getView(oldNode).name === newContent.viewName)); 216 | if (!sameContent || hasTransition[section]) { 217 | Blaze.render(newContent, parentElement); 218 | if (oldNode) { 219 | Blaze.remove(Blaze.getView(oldNode)); 220 | } 221 | } 222 | } 223 | }); 224 | }; 225 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'mcissel:flow-transition', 3 | version: '1.0.1', 4 | summary: 'A transition and layout renderer for FlowRouter', 5 | git: 'https://github.com/mcissel/FlowTransition', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.1'); 11 | api.use('blaze'); 12 | api.use('templating'); 13 | api.use('underscore'); 14 | api.use('velocityjs:velocityjs@1.2.1'); 15 | api.use('kadira:flow-router@2.1.1'); 16 | 17 | api.addFiles([ 18 | 'section.html', 19 | 'flow-transition.js' 20 | ], ['client']); 21 | 22 | api.export("FlowTransition", 'client'); 23 | }); 24 | 25 | Package.onTest(function(api) { 26 | api.use('tinytest'); 27 | api.use('mcissel:flow-transition'); 28 | api.addFiles('flow-transition-tests.js'); 29 | }); 30 | -------------------------------------------------------------------------------- /section.html: -------------------------------------------------------------------------------- 1 | 4 | --------------------------------------------------------------------------------