Skip to content

Instantly share code, notes, and snippets.

@funkatron
Last active December 15, 2015 00:09
Show Gist options
  • Save funkatron/5170838 to your computer and use it in GitHub Desktop.
Save funkatron/5170838 to your computer and use it in GitHub Desktop.
Showing a refactor to improve testability of a JavaScript app's global class. Before, we were doing a bunch of things inside one scope, and doing all the setup + kickstarting the app in the same scope. By breaking things out into separate chunks via function definitions, we can test each piece of setup individually without kickstarting the entir…
<script type="text/javascript" src="/static/js/testable_app_run.js"></script>
define([
'libs/jquery',
'libs/backbone',
'libs/handlebars',
'util/dnd_util',
'util/dnd_consts',
'routers/app_router',
'views/topnav_view',
'views/flashmessages_view',
'views/footer_view',
'models/prefs_model'
], function($, Backbone, Handlebars, DndUtil, DndConsts, AppRouter, TopnavView, FlashmessagesView, FooterView, PrefsModel) {
/**
* establish the app namespace
* @type {Object}
*/
var DndApp = {
attachToWindow: function() {
// make the DndApp global in the browser. THIS IS INTENTIONAL
window.DndApp = DndApp;
},
setupDefaultAvatars: function() {
DndApp.DEFAULT_AVATAR_URL = 'https://donenotdone.com/static/img/noavatar-large.gif';
DndApp.DEFAULT_AVATAR_URL_SMALL = 'https://donenotdone.com/static/img/noavatar-small.gif';
DndApp.DEFAULT_AVATAR_URL_MEDIUM = 'https://donenotdone.com/static/img/noavatar-medium.gif';
},
initVent: function() {
DndApp.vent = _.extend({}, Backbone.Events);
},
attachUtil: function() {
// set up a utility method namespace
DndApp.util = DndUtil;
},
attachConsts: function() {
// hook up constants
DndApp.consts = DndConsts;
},
setupConsole: function() {
// turn on/off the console based on window.DNDAPP_CONSOLE_ENABLED
DndApp.util.setupConsole();
},
bindAppEvents: function() {
// bind some events
DndApp.vent.on('api:error', function(code, url, message, xhr) {
// don't raise an error if this is a follower check
var regex_follow = /\/api\/users\/[a-z0-9_-]+\/following\/[a-z0-9_-]+/i;
if (code === 404 && url.match(regex_follow)) {
return;
}
console.error("API ERROR:", code, url, message, xhr);
DndApp.vent.trigger('flashmessage:add',
"Something went wrong while talking to the server. You might want to reload the page.",
"error");
});
},
bindDOMEvents: function() {
/**
* Bind some global events into vent triggers
* TODO This is hackey. I don't like binding stuff like this. We should
* do proper work with views and extending base objects
*/
$('#modal').on('click', function(e) {
if (e.target === e.currentTarget) { // we don't want children
DndApp.vent.trigger('click:modal', e);
}
});
/**
* bind a global jQuery ajax error listener. This will proxy the errors
* to an app.vent 'api:error'
*/
$(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError){
DndApp.vent.trigger('api:error', jqXHR.status, ajaxSettings.url, jqXHR.responseText, jqXHR);
});
},
loadBootstrap: function() {
// connect jsbootstrap to app
DndApp.bootstrap = window.jsbootstrap;
// connect to flashmessages
DndApp.flashmessages = window.jsflashmessages;
DndApp.prefs = new PrefsModel(DndApp.bootstrap.prefs,
DndApp.bootstrap.logged_in_user_id);
},
initAppSection: function() {
/**
* the current section of the app
*
* use DndApp.util.app_section() to get
* use DndApp.util.app_section('foo') to set
*
* @type {string}
*/
DndApp.app_section = null;
},
initRouter: function() {
// instantiate router
var app_router = new AppRouter();
// connect to app router
DndApp.router = app_router;
},
initCommonViews: function() {
// These appears everywhere in the app, so just instantiate them here
var topnav_view = new TopnavView();
var flashmessages_view = new FlashmessagesView();
var footer_view = new FooterView();
},
startHistory: function() {
/**
* KICKSTART MY HEART
*/
Backbone.history.start({pushState: true, root: DndApp.router.HISTORY_ROOT});
},
initialize: function() {
DndApp.attachToWindow();
DndApp.setupDefaultAvatars();
DndApp.initVent();
DndApp.attachUtil();
DndApp.attachConsts();
DndApp.setupConsole();
DndApp.bindDOMEvents();
DndApp.loadBootstrap();
DndApp.initAppSection();
DndApp.initRouter();
DndApp.bindAppEvents();
DndApp.initCommonViews();
},
run: function() {
DndApp.startHistory();
}
};
return DndApp;
});
require.config({
baseUrl: "/static/js"
});
require([
'testable_app'
], function(DndApp) {
DndApp.initialize();
window.DndApp.run();
});
<script type="text/javascript" src="/static/js/untestable_app.js"></script>
require.config({
baseUrl: "/static/js"
});
define([
'libs/jquery',
'libs/backbone',
'libs/handlebars',
'util/dnd_util',
'util/dnd_consts',
'routers/app_router',
'views/topnav_view',
'views/flashmessages_view',
'views/footer_view',
'models/prefs_model'
], function($, Backbone, Handlebars, DndUtil, DndConsts, AppRouter, TopnavView, FlashmessagesView, FooterView, PrefsModel) {
/**
* establish the app namespace
* @type {Object}
*/
var DndApp = {};
// make the DndApp global in the browser. THIS IS INTENTIONAL
window.DndApp = DndApp;
DndApp.DEFAULT_AVATAR_URL = 'https://donenotdone.com/static/img/noavatar-large.gif';
DndApp.DEFAULT_AVATAR_URL_SMALL = 'https://donenotdone.com/static/img/noavatar-small.gif';
DndApp.DEFAULT_AVATAR_URL_MEDIUM = 'https://donenotdone.com/static/img/noavatar-medium.gif';
// attach a global event listener
DndApp.vent = _.extend({}, Backbone.Events);
// set up a utility method namespace
DndApp.util = DndUtil;
// hook up constants
DndApp.consts = DndConsts;
// turn on/off the console based on window.DNDAPP_CONSOLE_ENABLED
DndApp.util.setupConsole();
/**
* Bind some global events into vent triggers
* TODO This is hackey. I don't like binding stuff like this. We should
* do proper work with views and extending base objects
*/
$('#modal').on('click', function(e) {
if (e.target === e.currentTarget) { // we don't want children
DndApp.vent.trigger('click:modal', e);
}
});
/**
* bind a global jQuery ajax error listener. This will proxy the errors
* to an app.vent 'api:error'
*/
$(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError){
DndApp.vent.trigger('api:error', jqXHR.status, ajaxSettings.url, jqXHR.responseText, jqXHR);
});
// connect jsbootstrap to app
DndApp.bootstrap = window.jsbootstrap;
// connect to flashmessages
DndApp.flashmessages = window.jsflashmessages;
DndApp.prefs = new PrefsModel(DndApp.bootstrap.prefs,
DndApp.bootstrap.logged_in_user_id);
/**
* the current section of the app
*
* use DndApp.util.app_section() to get
* use DndApp.util.app_section('foo') to set
*
* @type {string}
*/
DndApp.app_section = null;
// instantiate router
var app_router = new AppRouter();
// connect to app router
DndApp.router = app_router;
// bind some events
DndApp.vent.on('api:error', function(code, url, message, xhr) {
// don't raise an error if this is a follower check
var regex_follow = /\/api\/users\/[a-z0-9_-]+\/following\/[a-z0-9_-]+/i;
if (code === 404 && url.match(regex_follow)) {
return;
}
console.error("API ERROR:", code, url, message, xhr);
DndApp.vent.trigger('flashmessage:add',
"Something went wrong while talking to the server. You might want to reload the page.",
"error");
});
// These appears everywhere in the app, so just instantiate them here
var topnav_view = new TopnavView();
var flashmessages_view = new FlashmessagesView();
var footer_view = new FooterView();
/**
* KICKSTART MY HEART
*/
Backbone.history.start({pushState: true, root: DndApp.router.HISTORY_ROOT});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment