Skip to content

Instantly share code, notes, and snippets.

@AndrewJHart
Created February 22, 2015 19:58
Show Gist options
  • Save AndrewJHart/18346c820d8175ea3760 to your computer and use it in GitHub Desktop.
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…
// 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