Building evented single page applications

Post on 07-Dec-2014

1,411 views 2 download

description

Building single page applications has always seemed like a dark art. Guess what? It's not. The key is really simple—let the URL dictate everything. I'll show how to use the window location's hash in combination with jQuery's event system to drive your entire application, from link clicks to form submissions to history management and beyond.

transcript

Ordered ListJohn NunemakerjQueryConf San Francisco, CA

April 25, 2010

BuildingSingle Page Applications

1. Why?2. What?3. How?

1. Why?

Because?

Because? NO!

SpeedOnly retrieve what changes

Perceived Speed

InteractivityDesktop in a browser

ExperienceBut the greatest of these is...

2. What?

3. How?

Goals

No reloads

No reloadsHistory/refresh

No reloadsHistory/refreshEasy

#

No reloadsHistory/refreshEasy

No reloadsHistory/refreshEasy

No reloadsHistory/refreshEasy

No reloadsHistory/refreshEasy

The EndI kid...

No reloads

<a href="#/items">Items</a>

$("a[href^='#/']").live('click', function (event) { window.location.hash = $(this).attr('href'); $(document).trigger('hashchange'); return false;});

$(document).bind('hashchange', Layout.reload);

var Layout = { reload: function() { Layout.load(window.location.hash); }};

var Layout = { load: function(path, options) { path = path.replace(/^#/, ''); // trigger path loading events $.ajax({ url : path, dataType : 'json', success : function(json) { Layout.onSuccess(json); // trigger path success events if (options && options.success) { options.success(); } }, complete : function() { if (options && options.complete) { options.complete(); } } }); }};

var Layout = { load: function(path, options) { path = path.replace(/^#/, ''); $(document).trigger('path:loading', [path]); $(document).trigger('path:loading:' + path); $.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(); } } }); }};

var Layout = { onSuccess: function(json) { Layout.applyJSON(json); // trigger layout success },};

No reloads

History/refresh

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);

#/org/groups/12/45/new

org groups 12 45 new

org groups 12 45 new

org groups 12 45 new

org groups 12 45 new

org groups 12 45 new

org groups 12 45 new

var Layout = { handlePageLoad: function() { var segments = window.location.hash.replace(/^#\//, '').split('/'), total = segments.length, path = ''; function loadSectionsInOrder() { var segment = segments.shift(); path += '/' + segment; var onComplete = function() { var loaded = total - segments.length, finished = loaded == total; if (!finished) { loadSectionsInOrder(); } }; Layout.load(path, {complete: onComplete}); } loadSectionsInOrder(); }};

var Layout = { 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}); } loadSectionsInOrder(); }};

$(document).bind('page:loading', function() { $('#harmony_loading').show(); $('#loading_progress').css('width', 0);});

$(document).bind('page:progress', function(e, total, loaded) { if (total != loaded) { var final_width = 114, new_width = (loaded/total) * final_width; $('#loading_progress').animate({width: new_width+'px'}, 200); }}); $(document).bind('page:loaded', function() { $('#loading_progress').animate({width:'114px'}, 300, 'linear', function() { $('#harmony_loading').hide(); });});

History/refresh

Easy

Rule #1Abuse events for better code separation and easier customization

$('form').live('submit', function(event) { var $form = $(this); $form.ajaxSubmit({ dataType: 'json', beforeSend: function() { // trigger before send }, success: function(json) { // if errors, show them, else apply json }, error: function(response, status, error) { // trigger error }, complete: function() { // trigger complete } }); return false;});

$('form').live('submit', function(event) { var $form = $(this); $form.ajaxSubmit({ dataType: 'json', beforeSend: function() { $form.trigger('form:beforeSend'); }, success: function(json) { if (json.errors) { $form.showErrors(json.errors); } 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;});

var Site = { onCreateSuccess: function(event, json) { Sidebar.highlight($('#site_' + json.id)) Layout.updateHashWithoutLoad(window.location.hash.replace(/new$/, json.id)); }, onUpdateSuccess: function(event, json) { Sidebar.highlight($('#site_' + json.id)) }};

$('form.new_site').live('form:success', Site.onCreateSuccess);$('form.edit_site').live('form:success', Site.onUpdateSuccess);

var Field = { onCreateSuccess: function(event, json) { $('#list_section_' + json.section_id) .addSectionField(json.field_title.toLowerCase()); }, onUpdateSuccess: function(event, json) { $('#list_section_' + json.section_id) .removeSectionField(json.old_title.toLowerCase()) .addSectionField(json.new_title.toLowerCase()); }};

$('form.new_field') .live('form:success', Field.onCreateSuccess);$('form.edit_field').live('form:success', Field.onUpdateSuccess);

{ 'html': { '#content': '<h1>Heading</h1><p>Yay!</p>' }, 'replaceWith': { '#post_12': '<div class="post">...</div>' }, 'remove': ['#post_11', '#comment_12']}

Rule #2The URL dictates everything

var Layout = { livePath: function(event, path, callback) { if (typeof(test) === 'string') { $(document).bind('path:' + event + ':' + path, callback); } else { Layout.live_path_regex[event].push([path, callback]); } }};

Layout.livePath('loading', /\/org\/([^\/]+)([0-9\/]+).*/, Groups.loading);

Layout.livePath('loading', /\/sections\/([0-9]+)$/, Sections.markCurrent);

Layout.livePath('success', '/org/groups', Groups.setup);

Layout.livePath('success', '/sections', Sections.makeSortable);

Layout.livePath('success', /\/admin\/assets(.*)/, Assets.init);

Layout.livePath('success', /\/admin\/items\/(\d+)/, ItemForm.init);

Layout.livePath('success', /\/admin\/items\/([0-9\/]+)/, Items.manageSidebar);

Rule #3

$('a.field_form_toggler') .live('click', Fields.toggleAddFieldForm);$('form.new_section') .live('form:success', SectionForm.onCreateSuccess);$('form.edit_section') .live('form:success', SectionForm.onUpdateSuccess);$('form.new_field') .live('form:success', Field.onCreateSuccess);$('form.edit_field') .live('form:success', Field.onUpdateSuccess);$('a.field_destroy') .live('destroy:success', Field.onDestroySuccess);

Easy

Ordered List

Thank you!john@orderedlist.com

John NunemakerjQueryConf San Francisco, CAApril 25, 2010

@jnunemaker