Skip to content

Instantly share code, notes, and snippets.

@jnunemaker
Created September 13, 2009 21:08
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save jnunemaker/186329 to your computer and use it in GitHub Desktop.
Save jnunemaker/186329 to your computer and use it in GitHub Desktop.
/**
IMPORTANT: Requires this version of jquery
until 1.3.3 comes out http://gist.github.com/186325
ALSO: This is very dirty still and has not been
abstracted for use. It is just solving our immediate problems.
Use cases that must pass (and should be tested someday):
* Clicking on links updates layout
* Click around a bit and then use back/forward buttons
* Make sure recursive loading is working (#/sections/23)
* Forms should send beforeSubmit, beforeSend, success, error, valid
and invalid events
* Safari should send connection close url to ajaxSubmit for file uploads
* Should fire events for page loading, progress for each step and loaded
*/
(function ($) {
// errors is an array of errors
// render :json => {:errors => @item.errors.full_messages}
function FormErrors(errors) {
var error_count = errors.length;
function errorUl() {
var lis = '';
errors.forEach(function(error) {
lis += '<li>' + error + '</li>';
});
return '<ul>' + lis + '</ul>';
}
function errorHeading() {
var error_str = error_count === 1 ? 'error' : 'errors';
return '<h2>' + error_count + ' ' + error_str + ' prevented this form from being saved</h2>';
}
this.html = function () {
var html = '';
html += '<div class="errorExplanation" id="errorExplanation">';
html += errorHeading();
html += errorUl();
html += '</div>';
return html;
};
}
$.fn.removeErrors = function () {
return this.each(function () {
$(this).find('.errorExplanation').remove();
});
};
$.fn.showErrors = function (errors) {
return this.each(function () {
$(this).removeErrors().prepend(new FormErrors(errors).html());
});
};
})(jQuery);
var Layout = {
live_path_regex: {'loading':[], 'success':[]},
current_xhr: null,
loading: false,
init: function() {
jQuery(function($) {
var needs_default_hash = !window.location.hash || (window.location.hash && !window.location.hash.match(/^#\/admin/));
if (needs_default_hash) {
if (App.site_exists) {
window.location.hash = '#/admin/dashboard';
} else {
window.location.hash = '#/admin/you';
}
}
window.currentHash = window.location.hash;
Layout.handlePageLoad();
Layout.addObservers();
Layout.makeBackFowardButtonsWork();
});
},
destroy: function(url, options) {
Layout.post(url, {'_method':'delete'}, options);
},
post: function(url, data, options) {
var ajax = {
type : 'post',
dataType : 'json',
url : url,
success : function (json) {
Layout.onSuccess(json);
if (options && options.success) {
options.success(json);
}
}
}
if (data) {
ajax['data'] = data;
}
$.ajax(ajax);
},
addObservers: function() {
Layout.observeHashChange();
Layout.observeLinks();
Layout.observeForms();
Layout.observeLivePath();
},
observeLinks: function() {
$("a[href^='#/']").live('click', function(event) {
if (Layout.loading) {
return false;
}
var $link = $(this),
loading = $link.attr('data-loading');
Layout.updateHashWithoutLoad($link.attr('href'));
$(document).trigger('hashchange');
if (loading) {
$(document).trigger('loading:indicator',[loading])
}
return false;
});
$('.remote_destroy').live('click', function(event) {
var $target = $(this);
confirmDialog('Are you sure you want to delete this?', {
ok: function() {
Layout.destroy($target.attr('href'), {
success: function(json) { $target.trigger('destroy:success', [json]); }
});
}
});
return false;
});
},
observeForms: function() {
$(document).bind('layout:success', function() {
$('form').removeErrors();
});
$('form').live('submit', function(event) {
var $form = $(this),
data_type = $form.attr('data-type') || 'json',
remote_form = $form.attr('action').substr(0, 2) == '#/';
if (!remote_form) {
return true;
}
$form.ajaxSubmit({
url : $form.attr('action').replace(/^#/, ''),
dataType : data_type,
data : {iframe: 1},
closeKeepAlive: $.browser.safari ? '/admin/connection_close' : false,
beforeSubmit: function(data, form, options) {
$form.trigger('form:beforeSubmit', [data, form, options]);
},
beforeSend: function() {
$form.trigger('form:beforeSend');
},
success: function(json) {
// hack for iframe file uploads
if (data_type == 'xml') {
json_str = $(json).find('response').text(),
json = JSON.parse(json_str);
}
if (json.errors) {
var $errors_container = $form,
data_error_placement = $form.attr('data-error-placement');
if (typeof(data_error_placement) !== undefined) {
$errors_container = $form.find(data_error_placement);
}
$errors_container.showErrors(json.errors);
$form.trigger('form:error', [json]);
} else {
Layout.onSuccess(json);
$form.trigger('form:success', [json]);
}
},
error: function(response, status, error) {
$form.trigger('form:error', [response, status, error]);
},
complete: function() {
$form.trigger('form:complete');
}
});
return false;
});
},
observeHashChange: function() {
$(document).bind('hashchange', Layout.reload);
},
updateHashWithoutLoad: function(location) {
window.currentHash = window.location.hash = location;
},
makeBackFowardButtonsWork: function() {
setInterval(function() {
var hash_is_new = window.location.hash && window.currentHash != window.location.hash;
if (hash_is_new) {
window.currentHash = window.location.hash;
Layout.handlePageLoad();
}
}, 300);
},
// Options are success and complete callbacks
load: function(path, options) {
if (Layout.current_xhr) {
Layout.current_xhr.abort();
}
path = path.replace(/^#/, '');
$(document).trigger('path:loading', [path]);
$(document).trigger('path:loading:' + path);
Layout.current_xhr = $.ajax({
url: path,
dataType: 'json',
success: function(json) {
Layout.onSuccess(json);
$(document).trigger('path:success', [path, json]);
$(document).trigger('path:success:' + path, [json]);
if (options && options.success) {
options.success();
}
},
complete: function() {
if (options && options.complete) {
options.complete();
}
}
});
},
// See Layout.load for options
reload: function() {
Layout.load(window.location.hash);
},
livePath: function(event, path, callback) {
if (typeof(test) === 'string') {
$(document).bind('path:' + event + ':' + path, callback);
} else {
Layout.live_path_regex[event].push([path, callback]);
}
},
observeLivePath: function() {
$(document).bind('path:loading', function(event, path) {
$(Layout.live_path_regex['loading']).each(function() {
if (matches = path.match(this[0])) {
this[1](matches);
}
});
});
$(document).bind('path:success', function(event, path, json) {
$(Layout.live_path_regex['success']).each(function() {
if (matches = path.match(this[0])) {
this[1](matches, json);
}
});
});
},
onSuccess: function(json) {
$('li.item.loading').removeClass('loading');
Layout.applyJSON(json);
$(document).trigger('layout:success');
Layout.current_xhr = null;
},
applyJSON: function(json) {
for(action in json) {
var selectors = json[action];
switch(action) {
case 'replace' : for(selector in selectors) $(selector).html(selectors[selector]); break;
case 'append' : for(selector in selectors) $(selector).append(selectors[selector]); break;
case 'prepend' : for(selector in selectors) $(selector).prepend(selectors[selector]); break;
case 'replaceWith' : for(selector in selectors) $(selector).replaceWith(selectors[selector]); break;
case 'insertBefore' : for(selector in selectors) $(selectors[selector]).insertBefore($(selector)); break;
case 'sidebar' : Sidebar.add(selectors); break;
case 'remove' : $(selectors.join(',')).remove(); break;
}
}
},
handlePageLoad: function() {
var segments = window.location.hash.replace(/^#\//, '').split('/'),
total = segments.length,
path = '';
$(document).trigger('page:loading');
function loadSectionsInOrder() {
var segment = segments.shift();
path += '/' + segment;
var onComplete = function() {
var loaded = total - segments.length,
finished = loaded == total;
$(document).trigger('page:progress', [total, loaded]);
if (finished) {
$(document).trigger('page:loaded');
} else {
loadSectionsInOrder();
}
};
Layout.load(path, {complete: onComplete});
}
// start the recursive loading of sections
loadSectionsInOrder();
}
};
Layout.init();
@wicz
Copy link

wicz commented Aug 12, 2010

Hello John, that's some neat solution! I am doing something similar, but using sammy.js. I was wondering how you were capable of testing your code. I'm using cucumber+capybara-envjs, but seems envjs has some issues with window.location.hash. Have you tried envjs? Any other insights? Any help will be much appreciated. Thanks in advance. And congrats for Harmonyapp!

@jnunemaker
Copy link
Author

As of yet we are doing a lot of manual quality assurance, which sucks. I have plans to dig into automated testing, but for now, we stick with the framework we have and it just works.

@wicz
Copy link

wicz commented Aug 12, 2010

Manual QA is exactly what I'm trying to avoid. I started a thread on the ruby-capybara about this issue. I'm working with sammy.js right now, but since your solution is based on the same window.location.hash aspects, you might be interested. http://groups.google.com/group/ruby-capybara/browse_thread/thread/77596b9ead7f4911

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment