Skip to content

Instantly share code, notes, and snippets.

@AndrewJHart
Last active March 9, 2023 20:40
Show Gist options
  • Save AndrewJHart/11269535 to your computer and use it in GitHub Desktop.
Save AndrewJHart/11269535 to your computer and use it in GitHub Desktop.
Simple and elegant Backbone.js View Animations and Transitions for web and hybrid mobile apps. A Pen by Andrew J Hart.
<header role="navigation">
<h2>header bar</h2>
</header>
<footer class="footer" role="contentinfo">
<p>This demo illustrates a simple technique for using transitions with backbone views. Its a fork from a demo created by Mike Fowler as part of a post he wrote titled:
<a href="http://mikefowler.me/2013/11/18/page-transitions-in-backbone" target="page-transitions-in-backbone">“Page Transitions in Backbone”
</a>
</p>
</footer>
<script type="text/template" name="aside">
<aside class="settings">
<p><a href="#">Close Panel</a></p>
</aside>
</script>
<script type="text/template" name="home">
<h1>Home</h1>
<p><a href="#/detail">Detail &rarr;</a></p>
<ul class="list">
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
<li>text sample</li>
</ul>
</script>
<script type="text/template" name="detail">
<h1>Detail</h1>
<p><a href="#">&larr; Home</a></p>
<br>
<p>Lorem Ipsum and lots of other nonsenical things like non-words and double speak on animals that float
atop purple dinosaurs on tiny islands shaped like hotdogs with inverted mustard volcanos.</p>
<p>Lorem Ipsum and lots of other nonsenical things like non-words and double speak on animals that float
atop purple dinosaurs on tiny islands shaped like hotdogs with inverted mustard volcanos.</p>
<p>Lorem Ipsum and lots of other nonsenical things like non-words and double speak on animals that float
atop purple dinosaurs on tiny islands shaped like hotdogs with inverted mustard volcanos.</p>
<p>Lorem Ipsum and lots of other nonsenical things like non-words and double speak on animals that float
atop purple dinosaurs on tiny islands shaped like hotdogs with inverted mustard volcanos.</p>
</script>

Backbone.View Page Transitions in Backbone

An easy and effective way to implement page transitions into any Backbone.js application. Its been tested with Thorax by extending Thorax.View and tested with Marionette with various ways to implement it. There are also many other various attempts at animations/transitions with marionette that can be found on its issues page.

This originated from an idea and quest for a clean way to create a simple yet reusable animated backbone view. I found an excellent post by Mike Fowler titled Page Transitions in Backbone that provided a large part of the core and concept.

Forked from Mike Fowler's Pen Page Transitions in Backbone.

A Pen by Andrew J Hart on CodePen.

License.

// wrap in an IIFE for encapsulation
(function () {
// for simplicity create a global app object
window.app = {
Views: {},
Extensions: {},
Router: null,
init: function () {
// get an instance of the root App view
this.getInstance();
// start backbone hash change listener
Backbone.history.start();
},
// use a getter method to get the instance instead accessing this.instance
// directly. Also, this prevents multiple instantiation of the application
getInstance: function() {
// creates a new instance of the root App
// or returns the instance if it already exists
if (!this.instance) {
this.instance = new app.Views.App();
}
return this.instance;
}
};
// on dom ready trigger the app's init method
$(function() {
window.app.init();
});
// ------------------------
// code for the application
//
// define a single router on the global app object
app.Router = Backbone.Router.extend({
routes: {
'detail': 'detail',
'': 'home'
},
home: function () {
// make the Home view persist in memory and on the DOM
if (!this.homeView) {
this.homeView = new app.Views.Home();
}
// pass the view to the Layout View for handling animations etc..
app.getInstance().goto(this.homeView);
},
detail: function () {
var view = new app.Views.Detail();
app.getInstance().goto(view);
}
});
// Base view class for providing transition capabilities
// perhaps better named something like AnimView?
app.Extensions.View = Backbone.View.extend({
initialize: function () {
// this seems out of place even if this is the base view class for this app
// refactor to move the router instantiation to happen after backbone.history.start()
this.router = new app.Router();
},
// base render class that checks whether the the view is to be a 'page'
// aka meant for transitions; This is somewhat of an anti-pattern in that
// each view inheriting from this will have to trigger this render method
// with a 'super' call. A better remedy is to provide a check for a method
// like onRender() and trigger it with correct context so that views which
// inherit from this can provide an onRender() method for any additional
// rendering logic specific to that view.
render: function(options) {
// as part of refactor, show the current instance of the view using render
console.debug('Render triggered for the ' + this.className + ' View with cid: ' + this.cid);
options = options || {};
if (options.page === true) {
this.$el.addClass('page');
}
// From comment above, refactoring to use onRender() instead of override
if (_.isFunction(this.onRender())) {
// trigger whatever current/caller view's onRender() method
this.onRender();
}
return this;
},
transitionIn: function (callback) {
var view = this;
var transitionIn = function () {
view.$el.addClass(view.animateIn+' animated');
view.$el.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd animationend', function () {
view.$el.removeClass(view.animateIn+' animated');
if (_.isFunction(callback)) {
callback();
console.log('Callback triggered on transitionend for TransitionIn method');
}
});
};
// setting the page class' css to position: fixed; obviates the need
// for this and still allows transitions to work perfectly since pos
// is absolute during animation
_.delay(transitionIn, 0);
},
transitionOut: function (callback) {
var view = this;
view.$el.addClass(view.animateOut+' animated');
view.$el.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd animationend', function () {
view.$el.removeClass(view.animateOut+' animated');
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
console.log('Callback triggered on transitionend for TransitionOut method');
}
});
}
});
// The root app view - attached to the body, allows handling DOM-wide events
// and then through use of pub/sub or mixin backbone events we can notify other views.
// Also, it acts as a Layout to contain all views that want to use transitions
app.Views.App = app.Extensions.View.extend({
el: 'body',
goto: function (view) {
// cache the current view and the new view
var previous = this.currentPage || null;
var next = view;
if (previous) {
previous.transitionOut(function () {
// only remove the old view if its not the Home view
if (previous.$el.hasClass('home')) {
console.log('Previous view is Home; not removing for it should persist');
} else {
// otherwise cleanup all other views since we dont want them to persist
previous.remove();
}
});
}
next.render({ page: true }); // render the next view
this.$el.append( next.$el ); // append the next view to the body (the el for this root view)
next.transitionIn();
this.currentPage = next;
}
});
// The initial view triggered on start (Home view)
app.Views.Home = app.Extensions.View.extend({
className: 'home',
template: null,
initialize: function(options) {
this.animateIn = 'fadeIn';
this.animateOut = 'iosFadeLeft';
// cache the template -- especially if your homeview may contain a collection or
// act like a CollectionView; This prevents us from having to re-create the view
// instance and re-fetch the collection if the apps primary purpose is focused around
// the list view.
this.template = _.template($('script[name=home]').html());
return this;
},
/*render: function () {
// fill this view's element with html from the template
this.$el.html(this.template());
// trigger the parent class render method to handle
return app.Extensions.View.prototype.render.apply(this, arguments);
return this;
}*/
onRender: function() {
console.log('HomeView#onRender method triggered');
// fill this view's element with html from the template
// and render it only once (we have no collection so no need to re-render since view persists)
if (this.$el.is(':empty'))
this.$el.html(this.template());
return this;
}
});
// the detail view triggered by /#detail
app.Views.Detail = app.Extensions.View.extend({
className: 'detail',
template: null,
initialize: function(options) {
this.animateIn = 'iosSlideInRight';
this.animateOut = 'slideOutRight';
// cache the template instead of grabbing it each time on render
// in case we decide to persist this view instead of removing it
this.template = _.template($('script[name=detail]').html());
},
/* replaced with onRender() so render work is accomplished in base class
render: function () {
this.$el.html(this.template());
// trigger the parent class' render (e.g. a call to 'super')
return app.Extensions.View.prototype.render.apply(this, arguments);
}*/
onRender: function() {
console.debug('DetailView#onRender method triggered');
// fill this view's element with html from the template
this.$el.html(this.template());
}
});
})(); // end of IIFE with invocation
@import "compass";
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font: 150% "helvetica neue", sans-serif;
color: #333;
/* add fixed to pos of body and set width & height */
position: fixed;
height: 100%;
width: 100%;
}
h1 {
font-weight: normal;
}
a {
color: #F2994B;
}
header[role=navigation] {
position: fixed;
top: 0;
left: 0;
padding: 0;
height: 44px;
width: 100%;
background: #ccc;
z-index: 2;
opacity: 0.9;
}
header h2 {
font-size: 1em;
text-align: center;
line-height: 5px;
font-weight: 100;
}
.page {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 1em;
margin-bottom: 1em;
text-align: center;
/*transition: transform 250ms ease-out;*/
/*transform: translate3d(100%, 0, 0);*/
/* ---------
* removing overflow issues with additional elements appended to the body meant changing
* the body to be pos:fixed & setting width & height of body to 100%, ergo w & h here too
*/
width: 100%;
height: 80%; /* hack temp.. should be 100% w/ margin or padding to offset footer */
/* scrolling */
overflow-y: scroll; /* has to be scroll */
-webkit-overflow-scrolling: touch;
}
.page a {
color: white;
}
.page.is-visible {
transform: translate3d(0, 0, 0);
}
aside.settings.page {
position: absolute;
top: 0;
bottom: 0;
left: 0;
padding: 0;
text-align: center;
/*transition: transform 250ms ease-out;*/
/*transform: translate3d(100%, 0, 0);*/
/* ---------
* removing overflow issues with additional elements appended to the body meant changing
* the body to be pos:fixed & setting width & height of body to 100%, ergo w & h here too
*/
width: 80%;
height: 100%;
z-index: 11;
/* scrolling */
overflow-y: scroll; /* has to be scroll */
-webkit-overflow-scrolling: touch;
}
.home {
color: white;
background: #F2CF66;
}
.detail {
color: white;
background: #F2994B;
}
.footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 0 5px;
background: white;
z-index: 2;
}
/* ios 7 style animations
* @goal is to be able to specify 2 properties on an AnimView (animationIn, animationOut)
* with the transition callback using the current views property to directly reference
* the css classname for animation
* Andrew Hart @andruwhart / Lead Developer for MSCNS www.mscns.com
*/
/* animations - borrowed from animate.css and tweaked to match ios 7 */
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
@-webkit-keyframes iosFadeLeft {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(-100px);
transform: translateX(-100px);
opacity: 0.9;
}
}
@keyframes iosFadeLeft {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(-100px);
transform: translateX(-100px);
opacity: 0.8;
}
}
.iosFadeLeft {
-webkit-animation-name: iosFadeLeft;
animation-name: iosFadeLeft;
-webkit-animation-timing-function: cubic-bezier(.1, .7, .1, 1);
animation-timing-function: cubic-bezier(.1, .7, .1, 1);
-webkit-animation-duration: 400ms;
animation-duration: 400ms;
}
@-webkit-keyframes iosSlideInRight {
0% {
opacity: 0.7;
-webkit-transform: translateX(2000px);
-ms-transform: translateX(2000px);
transform: translateX(2000px);
}
100% {
opacity: 1;
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
}
@keyframes iosSlideInRight {
0% {
opacity: 0.7;
-webkit-transform: translateX(2000px);
-ms-transform: translateX(2000px);
transform: translateX(2000px);
}
100% {
opacity: 1;
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
}
.iosSlideInRight {
-webkit-animation-name: iosSlideInRight;
animation-name: iosSlideInRight;
-webkit-animation-timing-function: cubic-bezier(.1, .7, .1, 1);
animation-timing-function: cubic-bezier(.1, .7, .1, 1);
-webkit-animation-duration: 400ms;
animation-duration: 400ms;
}
-webkit-keyframes slideOutRight {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
100% {
opacity: 0;
-webkit-transform: translateX(2000px);
transform: translateX(2000px);
}
}
@keyframes slideOutRight {
0% {
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
100% {
opacity: 0;
-webkit-transform: translateX(2000px);
-ms-transform: translateX(2000px);
transform: translateX(2000px);
}
}
.slideOutRight {
-webkit-animation-name: slideOutRight;
animation-name: slideOutRight;
}
@-webkit-keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fadeIn {
-webkit-animation-name: fadeIn;
animation-name: fadeIn;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment