Created
February 22, 2015 19:58
-
-
Save AndrewJHart/18346c820d8175ea3760 to your computer and use it in GitHub Desktop.
Rough demonstration of creating transitionIn and transitionOut declarative properties on any of your backbone views to provide CSS powered animations into your view. Note this copy was used to extend the framework Thorax, but will little work should be easily made to work w/ backbone by itself. To make this work you will also need to create a Ro…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Definition would look like to create a detail view | |
/* | |
var DetailView = AnimView.extend({ | |
name: "detail", | |
template: template, | |
// classes for this view | |
className: 'detail', | |
// animation properties | |
animateIn: "iosSlideInRight", | |
animateOut: "slideOutRight", | |
events: { .. }, | |
methods.. etc.. | |
*/ | |
// usage from router when a detail route is triggered | |
/* | |
detail: function(params) { | |
var model = null, | |
pageView = null; | |
// use params to get model from our collection | |
model = someCollection.get(params); | |
// create the detail page-view that contains the header view, | |
// the footer view, and the actual content view nested within | |
// using the handlebars "view" helper | |
pageView = new DetailView({ | |
model: model | |
}); | |
// animate to this view | |
RootView.getInstance().goTo(pageView, { | |
page: true, // tells app to animate it & apply class "page" | |
}); | |
return this; | |
} | |
*/ | |
// require js version | |
define(['underscore', 'thorax'], function(_, Thorax) { | |
return AnimView = Thorax.View.extend({ | |
template: null, | |
wasRendered: false, | |
// base render class that checks whether the the view is to be a 'page' | |
render: function(options) { | |
// as part of refactor, show the current instance of the view using render | |
if (debug) { | |
console.log("Rendering " + this.getViewName() + " ID " + this.cid + " - render inherited from base class(AnimView)"); | |
} | |
// get existing options or init empty object | |
options = options || {}; | |
// is this a "page-view" view? | |
if (options.page === true) { | |
this.$el.addClass('page'); | |
} | |
// BeforeRender Hook for users (devs) to handle special cases like jQuery | |
// plugin instantiation, etc.. before the view & template are rendered | |
if (_.isFunction(this.beforeRender)) { | |
// trigger whatever current/caller view's beforeRender() method | |
this.beforeRender(); | |
} | |
// call the parent render since we're overriding it in backbone or thorax | |
Thorax.View.prototype.render.apply(this, arguments); | |
// Trigger any additional or special rendering a user may require | |
if (_.isFunction(this.afterRender)) { | |
// trigger whatever current/caller view's onRender() method | |
this.afterRender(); | |
} | |
if (!this.wasRendered) { | |
this.wasRendered = true; | |
} | |
return this; | |
}, | |
conservativeRender: function() { | |
// Hook before the view & template are rendered | |
if (_.isFunction(this.beforeRender)) { | |
// trigger whatever current/caller view's beforeRender() method | |
this.beforeRender(); | |
} | |
// Trigger any additional post-render cases for users (devs) | |
// to handle special cases like jQuery plugin instantiation, etc.. | |
if (_.isFunction(this.afterRender)) { | |
// trigger whatever current/caller view's onRender() method | |
this.afterRender(); | |
} | |
return this; | |
}, | |
transitionIn: function(options, callback) { | |
var view = this, | |
toggle = options.toggleIn || ''; // init toggle as empty classname | |
var transitionIn = function() { | |
view.$el.toggleClass(toggle).show().addClass(view.animateIn); // could = 'slideInRight animated' | |
view.$el.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd animationend', function() { | |
view.$el.removeClass(view.animateIn); | |
if (_.isFunction(callback)) { | |
callback(); | |
} | |
}); | |
}; | |
// setting the page class' css to position: fixed; obviates the need | |
// for this and still allows transitions to work perfectly | |
_.delay(transitionIn, 0); | |
}, | |
transitionOut: function(options, callback) { | |
var view = this, | |
toggle = options.toggleOut || ''; | |
// otherwise operate standard transitions | |
view.$el.toggleClass(toggle).addClass(view.animateOut); | |
view.$el.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd animationend', function() { | |
view.$el.removeClass(view.animateOut + ' animated').hide(); | |
if (_.isFunction(callback)) { | |
callback(); // hard to track bug! He's binding to transitionend each time transitionOut called | |
// resulting in the callback being triggered callback * num of times transitionOut | |
// has executed | |
} | |
}); | |
}, | |
// called from router methods | |
goTo: function(view, options) { | |
var options = options || {}, | |
attachType = options.attachType || "append", | |
previous = this.currentPage || null, // cache current view | |
next = view; // cache new view too | |
// if this is a *page* view then animate it.. | |
if (options.page === true) { | |
// check for a previous view before trying anything | |
if (previous) { | |
// Animate the previous view based on its animateOut property | |
// and pass it this function as a callback, after the animation | |
// has completed, so we can remove & destroy the previous view. | |
previous.transitionOut(options, function() { | |
// save the previous view's DOM el & state entirely | |
// if it has a `data-view-persist` attribute = true | |
if (previous.viewPersists === true || previous.$el.data('view-persist') == true) { | |
// this view does not get removed | |
// although this view is not removed, provide a hook for | |
// a user to perform cleanup, remove classes, etc.. before | |
// the next view is animated in | |
if (_.isFunction(previous.beforeNextViewLoads)) { | |
previous.beforeNextViewLoads(); | |
} | |
} else { | |
// allow user to cleanup actions pre-removal w/ this hook | |
if (_.isFunction(previous.beforeRemove)) { | |
previous.beforeRemove(); | |
} | |
// allow user cleanup by defining onRemove callback | |
if (_.isFunction(previous.onRemove)) { | |
previous.onRemove(); | |
} | |
// allow user to trigger actions post-removal w/ this hook | |
if (_.isFunction(previous.afterRemove)) { | |
previous.afterRemove(); | |
} | |
} | |
}); | |
} | |
// if the new view has not already been rendered before | |
// then render it and append it the dom. Otherwise were | |
// performing 2 wasteful ops here: rendering again.. but | |
// more importantly: appending an existing view to an | |
// existing DOM that has the same view... | |
// This works because persistent views still exist so | |
// hasRendered will return the same value, whereas non-persistant | |
// views, like detail, were removed and hasRendered will be false. | |
if (!next.hasRendered()) { | |
// render the new view as a page | |
next.render({ | |
page: true | |
}); | |
// attach the new view to the DOM element belonging to | |
// (this) the base page view manager aka: root view | |
if (attachType) { | |
// NOTE: attachType = "append" or "prepend" | |
this.$el[attachType](next.$el); | |
} else { | |
// default to append'ing the view to DOM | |
this.$el.append(next.$el); | |
} | |
} else { | |
// persisting view has already been rendered once so | |
// call a *conservative render* to trigger hooks | |
next.conservativeRender(); | |
} | |
// animate the new view | |
next.transitionIn(options, function() { | |
// if a previous view does exist & is disposable then drop it. | |
if (previous) { | |
// on transition new view in delete any disposable views | |
if (!previous.viewPersists || !previous.$el.data('view-persist')) { | |
// allow user to cleanup actions pre-removal w/ this hook | |
if (_.isFunction(previous.beforeRemove)) { | |
previous.beforeRemove(); | |
} | |
// allow user cleanup by defining onRemove callback | |
if (_.isFunction(previous.onRemove)) { | |
previous.onRemove(); | |
} | |
// remove the previous view (copied from LayoutView) | |
remove(); | |
// allow user to trigger actions post-removal w/ this hook | |
if (_.isFunction(previous.afterRemove)) { | |
previous.afterRemove(); | |
} | |
} | |
} | |
}); | |
} else { // Not a *page* view or pane so apply no transitions | |
// check for a previous view before acting | |
if (previous) { | |
if (previous.$el.data('view-persist') == true) { | |
// even though no anim, persisting views still need callback | |
// before they are "closed" or removed from screen | |
if (_.isFunction(previous.beforeNextViewLoads)) { | |
previous.beforeNextViewLoads(); | |
} | |
} else { | |
// allow user to cleanup actions pre-removal w/ this hook | |
if (_.isFunction(previous.beforeRemove)) { | |
previous.beforeRemove(); | |
} | |
// remove the previous view, its children, & publish event | |
remove(); | |
// allow user to trigger actions post-removal w/ this hook | |
if (_.isFunction(previous.afterRemove)) { | |
previous.afterRemove(); | |
} | |
} | |
} | |
// render the new view | |
next.render({ | |
page: true // its still a "page view" just no transition hooks | |
}); | |
// append new view to the body (or the el for the root view) | |
this.$el.append(next.$el); | |
} | |
// assign the new view as the current view for next execution of goto | |
this.currentPage = next; | |
}, | |
hasRendered: function() { | |
return this.wasRendered; | |
}, | |
// get *just* the view's filename without path & extension | |
getViewName: function() { | |
return this.name; //.split('/').pop(); | |
}, | |
// get *just* the moduleName aka: the "path" w/o the filename | |
getModuleName: function() { | |
// same as above but use shift() to get 1st value from array | |
return this.name.split('/').shift(); | |
}, | |
// get current view's filename with extension | |
getFileName: function() { | |
return this.name.split('/').pop() + '.js'; | |
}, | |
// hook and delegate to base remove | |
onRemove: function() { | |
if (_.isFunction(this.onClose)) { | |
// added for backwards compat | |
this.onClose(); | |
} | |
if (_.isFunction(this.remove)) { | |
// trigger base class remove method | |
Thorax.View.prototype.remove.apply(this, arguments); | |
if (this.model) | |
this.model.unbind(); | |
if (this.collection) | |
this.collection.unbind(); | |
this.remove(); | |
} | |
} | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment