Building
Single Page Applications




jQueryConf San Francisco, CA   John Nunemaker
April 25, 2010                       Ordered List
1. Why?
2. What?
3. How?
1. Why?
Because?
Because? NO!
Speed
Only retrieve what changes
Perceived Speed
Interactivity
Desktop in a browser
But the greatest of these is...

Experience
2. What?
http://harmonyapp.com/
3. How?
Goals
No reloads
No reloads
History/refresh
No reloads
History/refresh
Easy
#
No reloads
History/refresh
Easy
No reloads
History/refresh
Easy
No reloads
History/refresh
Easy
No reloads
History/refresh
Easy
The End
I 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 #1
Abuse 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 #2
The 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
Thank you!
john@orderedlist.com
@jnunemaker



jQueryConf San Francisco, CA   John Nunemaker
April 25, 2010                       Ordered List

Building Evented Single Page Applications

  • 1.
    Building Single Page Applications jQueryConfSan Francisco, CA John Nunemaker April 25, 2010 Ordered List
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
    But the greatestof these is... Experience
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
    $("a[href^='#/']").live('click', function (event){ window.location.hash = $(this).attr('href'); $(document).trigger('hashchange'); return false; });
  • 26.
  • 27.
    var Layout ={ reload: function() { Layout.load(window.location.hash); } };
  • 28.
    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(); } } }); } };
  • 29.
    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(); } } }); } };
  • 30.
    var Layout ={ onSuccess: function(json) { Layout.applyJSON(json); // trigger layout success }, };
  • 31.
  • 32.
  • 33.
    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);
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
    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(); } };
  • 42.
    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(); } };
  • 43.
    $(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(); }); });
  • 44.
  • 45.
  • 46.
    Rule #1 Abuse eventsfor better code separation and easier customization
  • 47.
    $('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; });
  • 48.
    $('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; });
  • 49.
    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);
  • 50.
    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);
  • 51.
    { 'html': { '#content': '<h1>Heading</h1><p>Yay!</p>' }, 'replaceWith': { '#post_12': '<div class="post">...</div>' }, 'remove': ['#post_11', '#comment_12'] }
  • 52.
    Rule #2 The URLdictates everything
  • 53.
    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]); } } };
  • 54.
    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);
  • 55.
  • 56.
    $('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);
  • 57.
  • 58.
    Thank you! john@orderedlist.com @jnunemaker jQueryConf SanFrancisco, CA John Nunemaker April 25, 2010 Ordered List