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

More Related Content

PPTX
Elixir flow: Building and tuning concurrent workflows
PDF
introduction to Django in five slides
KEY
Advanced jQuery
PDF
jQuery: out with the old, in with the new
PDF
DOM Scripting Toolkit - jQuery
PDF
20 modules i haven't yet talked about
PDF
How Kris Writes Symfony Apps
Elixir flow: Building and tuning concurrent workflows
introduction to Django in five slides
Advanced jQuery
jQuery: out with the old, in with the new
DOM Scripting Toolkit - jQuery
20 modules i haven't yet talked about
How Kris Writes Symfony Apps

What's hot (20)

PDF
Beyond the DOM: Sane Structure for JS Apps
PDF
How kris-writes-symfony-apps-london
PDF
Matters of State
PDF
PDF
WordPressでIoTをはじめよう
PDF
jQuery: Events, Animation, Ajax
PDF
Road to Async Nirvana
PDF
Angular Promises and Advanced Routing
ZIP
Web+GISという視点から見たGISの方向性
KEY
Jquery Fundamentals
PDF
Jqeury ajax plugins
PDF
Node.js - Demnächst auf einem Server in Ihrer Nähe
PDF
An Introduction to Jquery
PDF
jQuery and Rails, Sitting in a Tree
PDF
Decoupling the Ulabox.com monolith. From CRUD to DDD
PDF
Delivering a Responsive UI
PPT
Perl调用微博API实现自动查询应答
PDF
Love and Loss: A Symfony Security Play
PDF
How Kris Writes Symfony Apps
ODT
linieaire regressie
Beyond the DOM: Sane Structure for JS Apps
How kris-writes-symfony-apps-london
Matters of State
WordPressでIoTをはじめよう
jQuery: Events, Animation, Ajax
Road to Async Nirvana
Angular Promises and Advanced Routing
Web+GISという視点から見たGISの方向性
Jquery Fundamentals
Jqeury ajax plugins
Node.js - Demnächst auf einem Server in Ihrer Nähe
An Introduction to Jquery
jQuery and Rails, Sitting in a Tree
Decoupling the Ulabox.com monolith. From CRUD to DDD
Delivering a Responsive UI
Perl调用微博API实现自动查询应答
Love and Loss: A Symfony Security Play
How Kris Writes Symfony Apps
linieaire regressie
Ad

Viewers also liked (12)

PPTX
Reactive web applications
PDF
Reactive programming
PDF
Can Single Page Applications Deliver a World-Class Web UX?
PDF
Modern app programming with RxJava and Eclipse Vert.x
PDF
Securing Single-Page Applications with OAuth 2.0
PDF
Voxxed Days Vienna - The Why and How of Reactive Web-Applications on the JVM
PDF
Reactive Web Applications
PPTX
Vert.x for Microservices Architecture
PDF
Reactive Programming in Spring 5
PPTX
Single Page Applications with AngularJS 2.0
PPT
Reactive programming with examples
PDF
Intro to Reactive Programming
Reactive web applications
Reactive programming
Can Single Page Applications Deliver a World-Class Web UX?
Modern app programming with RxJava and Eclipse Vert.x
Securing Single-Page Applications with OAuth 2.0
Voxxed Days Vienna - The Why and How of Reactive Web-Applications on the JVM
Reactive Web Applications
Vert.x for Microservices Architecture
Reactive Programming in Spring 5
Single Page Applications with AngularJS 2.0
Reactive programming with examples
Intro to Reactive Programming
Ad

Similar to Building Evented Single Page Applications (20)

PDF
Building Large jQuery Applications
PPT
Enhance Web Performance
PDF
Building a Mobile App with Sencha Touch
KEY
jQuery: Tips, tricks and hints for better development and Performance
PPT
jQuery Performance Rules
PDF
HTML5 and the dawn of rich mobile web applications pt 2
PDF
Rails is not just Ruby
PPT
Javascript Experiment
PPTX
Taming that client side mess with Backbone.js
PDF
Organizing Code with JavascriptMVC
PDF
Create a mobile web app with Sencha Touch
PDF
A mobile web app for Android in 75 minutes
PPTX
Design Patterns for JavaScript Web Apps - JavaScript Conference 2012 - OPITZ ...
PPTX
Design Summit - UI Roadmap - Dan Clarizio, Martin Povolny
PPTX
AppForum 2014 Boost Hybrid App Performance
PPTX
PDF
Writing Maintainable JavaScript
PPT
Jquery presentation
PDF
Refactor Large applications with Backbone
PDF
Refactor Large apps with Backbone
Building Large jQuery Applications
Enhance Web Performance
Building a Mobile App with Sencha Touch
jQuery: Tips, tricks and hints for better development and Performance
jQuery Performance Rules
HTML5 and the dawn of rich mobile web applications pt 2
Rails is not just Ruby
Javascript Experiment
Taming that client side mess with Backbone.js
Organizing Code with JavascriptMVC
Create a mobile web app with Sencha Touch
A mobile web app for Android in 75 minutes
Design Patterns for JavaScript Web Apps - JavaScript Conference 2012 - OPITZ ...
Design Summit - UI Roadmap - Dan Clarizio, Martin Povolny
AppForum 2014 Boost Hybrid App Performance
Writing Maintainable JavaScript
Jquery presentation
Refactor Large applications with Backbone
Refactor Large apps with Backbone

Building Evented Single Page Applications

  • 1. Building Single Page Applications jQueryConf San Francisco, CA John Nunemaker April 25, 2010 Ordered List
  • 9. But the greatest of these is... Experience
  • 13. Goals
  • 17. #
  • 25. $("a[href^='#/']").live('click', function (event) { window.location.hash = $(this).attr('href'); $(document).trigger('hashchange'); return false; });
  • 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 }, };
  • 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);
  • 35. org groups 12 45 new
  • 36. org groups 12 45 new
  • 37. org groups 12 45 new
  • 38. org groups 12 45 new
  • 39. org groups 12 45 new
  • 40. org groups 12 45 new
  • 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(); }); });
  • 45. Easy
  • 46. Rule #1 Abuse events for 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 URL dictates 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);
  • 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. Easy
  • 58. Thank you! john@orderedlist.com @jnunemaker jQueryConf San Francisco, CA John Nunemaker April 25, 2010 Ordered List