Upgrade to Pro — share decks privately, control downloads, hide ads and more …

jQuery Conference SF 2012: Beyond the DOM: Sane Structure for JS Apps

jQuery Conference SF 2012: Beyond the DOM: Sane Structure for JS Apps

In the bad old days of JavaScript, our days were spent fighting through a thicket of DOM differences. These days, we've pretty much conquered those challenges thanks to tools such as jQuery, but our new challenge is figuring out how to adapt as more and more of our application logic moves from the server to the client. We have great tools like Backbone, Ember, Knockout, and more, but in order to use them effectively, we need to do more than learn their APIs -- we need to learn to think about our code beyond the DOM-centric ways of the past. In this talk, I'll look at useful patterns for thinking about client-side application development that will help you avoid creating a tangled mess of code.

Rebecca Murphey

June 28, 2012
Tweet

More Decks by Rebecca Murphey

Other Decks in Technology

Transcript

  1. Beyond the DOM: Sane Structure for JS Apps Rebecca Murphey

    • jQuery Conference 2012 • San Francisco Thursday, June 28, 12
  2. function ObjInlineUp(e) { if (is.ie) e = event if (is.ie

    && ! is.ieMac && e.button != 1 && e.button != 2) return if (is.ieMac && e.button != 0) return if (is.ns && ! is.ns4 && ! is.nsMac && e.button != 0 && e.button != 2) return if (is.ns4 && e.which != 1 && e.which != 3) return if ((!is.ns4 && e.button == 2) || (is.ns4 && e.which == 3)) { if (this.hasOnRUp) { document.oncontextmenu = ocmNone this.onRUp() setTimeout("document.oncontextmenu = ocmOrig", 100) } } else if (this.hasOnUp) this.onUp() } Thursday, June 28, 12
  3. <div id="searchForm"> <form class="form-inline"> <input type="text" placeholder="Enter your search term">

    <button type="submit">Search</button> </form> <ul id="searchResults"></ul> </div> Thursday, June 28, 12
  4. $("#searchForm form").submit(function(e) { alert('submit'); e.preventDefault(); var term = $('#searchForm input').val(),

    req = $.getJSON('http://search.twitter.com/search.json?callback=?&q=' + encodeURIComponent(term)); req.then(function(resp) { var resultsHTML = $.map(resp.results, function(r) { return '<li>' + '<p class="tweet">' + r.text + '</p>' + '<p class="username">' + r.from_user + '</p>' + '</li>'; }).join(''); $('#searchResults').html(resultsHTML); }); }); Thursday, June 28, 12
  5. a').hasClass('md_fullpage')) { // alert('clicked section is current section AND fullpage

    mode is active; teaser should load'); // Minimize jQuery('#md_tabs_navigation a').removeClass('md_fullpage'); jQuery('.md_body').hide(); jQuery('#md_feature').slideDown('normal',function(){ var bodyContent = jQuery('#md_body_'+ section); bodyContent.fadeOut('normal',function(){ jQuery('#md_tabs_navigation a').each(function(){ var thisSection = jQuery(this).html().replace('<span<','').replace('<\/span<',''); var thisSection_comp = thisSection.toLowerCase().replace(' ','_'); jQuery('#md_body_'+ thisSection_comp).load( '/app/modules/info/loadTeaser.php?sect='+ thisSection_comp, function(){ tb_init('.md_body a.thickbox, .md_body area.thickbox, .md_body input.thickbox'); bodyContent.animate({ height: 'toggle', opacity: 'toggle' },"slow"); } ); }); }); }); jQuery('#expandtabs span').empty().append('Expand Tabs'); } else { // if the clicked section is NOT the current section OR we're NOT in full page mode // then let's go to full page mode and show the whole tab // Maximize // alert('clicked section is not the current section OR full page mode is not active; full section should load'); jQuery('#md_tabs_navigation li').removeClass('current'); jQuery('#md_tab_'+ section).addClass('current'); jQuery('#md_tabs_navigation a').addClass('md_fullpage'); jQuery('.md_body').hide(); jQuery('#md_feature').slideUp('normal',function(){ var bodyContent = jQuery('#md_body_'+ section); bodyContent.fadeOut('normal',function(){ bodyContent.empty(); var pageLoader = 'info/loadSection.php?sect='+ section; if (section == 'contact_us') { pageLoader = 'contact/loadContactForm.php?form_id=1'; } bodyContent.load('/app/modules/'+ pageLoader,function(){ // ADD THICKBOXES tb_init('.md_body a.thickbox, .md_body area.thickbox, .md_body input.thickbox'); $recent_news_links = jQuery('ul.md_news li a.recent_news_link'); $recent_news_links .unbind('click') .each(function(){ var hrefMod = this.href; hrefMod = hrefMod.replace(/article/,'loadNews').replace(/storyid/,'id'); this.href = hrefMod; }) .click(function(){ var t = this.title || this.name || null; Thursday, June 28, 12
  6. search data search input search results $("#searchForm form").submit(function(e) { alert('submit');

    e.preventDefault(); var term = $('#searchForm input').val(), req = $.getJSON('http://search.twitter.com encodeURIComponent(term)); req.then(function(resp) { var resultsHTML = $.map(resp.results, functi return '<li>' + '<p class="tweet">' + r.text + '</p>' + '<p class="username">' + r.from_user + ' '</li>'; }).join(''); $('#searchResults').html(resultsHTML); }); }); Thursday, June 28, 12
  7. define([ 'jquery', 'text!template.html' ], function($, html) { return function() {

    $('body').append(html); }; }); Thursday, June 28, 12
  8. require.config({ deps : [ 'main' ], paths : { //

    JavaScript folders lib : '../lib', plugins : '../lib/plugins', tests : '../tests', app : '.', // Libraries jquery : '../lib/jquery', text : '../lib/plugins/text' } }); Thursday, June 28, 12
  9. require([ 'use!backbone', 'jquery', 'router', 'models/app' ], function(B, $, Router, app)

    { $(function() { app.router = new Router(); B.history.start(); }); }); app/main Thursday, June 28, 12
  10. user interface display data, announce user interaction, and await further

    instruction managing state manage application state and communicate with the server brokering communication transport messages between user interface and state management Thursday, June 28, 12
  11. searches collection keeps track of recent search terms search data

    collection fetches results from the server for a given search term app model keeps track of general application state, including the current search search model for representing individual searches Thursday, June 28, 12
  12. $("#searchForm form").submit(function(e) { alert('submit'); e.preventDefault(); var term = $('#searchForm input').val(),

    req = $.getJSON('http://search.twitter.com/search.json?callba encodeURIComponent(term)); req.then(function(resp) { var resultsHTML = $.map(resp.results, function(r) { return '<li>' + '<p class="tweet">' + r.text + '</p>' + '<p class="username">' + r.from_user + '</p>' + '</li>'; }).join(''); $('#searchResults').html(resultsHTML); }); }); Thursday, June 28, 12
  13. a').hasClass('md_fullpage')) { // alert('clicked section is current section AND fullpage

    mode is active; teaser should load'); // Minimize jQuery('#md_tabs_navigation a').removeClass('md_fullpage'); jQuery('.md_body').hide(); jQuery('#md_feature').slideDown('normal',function(){ var bodyContent = jQuery('#md_body_'+ section); bodyContent.fadeOut('normal',function(){ jQuery('#md_tabs_navigation a').each(function(){ var thisSection = jQuery(this).html().replace('<span<','').replace('<\/span<',''); var thisSection_comp = thisSection.toLowerCase().replace(' ','_'); jQuery('#md_body_'+ thisSection_comp).load( '/app/modules/info/loadTeaser.php?sect='+ thisSection_comp, function(){ tb_init('.md_body a.thickbox, .md_body area.thickbox, .md_body input.thickbox'); bodyContent.animate({ height: 'toggle', opacity: 'toggle' },"slow"); } ); }); }); }); jQuery('#expandtabs span').empty().append('Expand Tabs'); } else { // if the clicked section is NOT the current section OR we're NOT in full page mode // then let's go to full page mode and show the whole tab // Maximize // alert('clicked section is not the current section OR full page mode is not active; full section should load'); jQuery('#md_tabs_navigation li').removeClass('current'); jQuery('#md_tab_'+ section).addClass('current'); jQuery('#md_tabs_navigation a').addClass('md_fullpage'); jQuery('.md_body').hide(); jQuery('#md_feature').slideUp('normal',function(){ var bodyContent = jQuery('#md_body_'+ section); bodyContent.fadeOut('normal',function(){ bodyContent.empty(); var pageLoader = 'info/loadSection.php?sect='+ section; if (section == 'contact_us') { pageLoader = 'contact/loadContactForm.php?form_id=1'; } bodyContent.load('/app/modules/'+ pageLoader,function(){ // ADD THICKBOXES tb_init('.md_body a.thickbox, .md_body area.thickbox, .md_body input.thickbox'); $recent_news_links = jQuery('ul.md_news li a.recent_news_link'); $recent_news_links .unbind('click') .each(function(){ var hrefMod = this.href; hrefMod = hrefMod.replace(/article/,'loadNews').replace(/storyid/,'id'); this.href = hrefMod; }) .click(function(){ var t = this.title || this.name || null; Thursday, June 28, 12
  14. events : { 'submit .search-form' : '_onSearch' }, _onSearch :

    function(e) { e.preventDefault(); if (this.disabled) { return; } var term = $.trim(this.$('.js-input').val()); if (!term) { return; } this._disable(); this.trigger('search', term); } app/views/searchForm Thursday, June 28, 12
  15. searchForm.on('search', update); function update(t) { var term = $.trim(t), existing

    = searches.where({ term : term }), dfd = $.Deferred(), search; app.set('currentSearch', term); if (term) { if (existing.length) { search = existing[0]; search.update(); } else { search = new Search({ term : term }); searches.add(search); } searchData.fetch({ data : { term : term } }) .then(dfd.resolve, dfd.reject) .always(searchForm.release); app/controllers/search Thursday, June 28, 12
  16. searchForm.on('search', update); function update(t) { var term = $.trim(t), existing

    = searches.where({ term : term }), dfd = $.Deferred(), search; app.set('currentSearch', term); if (term) { if (existing.length) { search = existing[0]; search.update(); } else { search = new Search({ term : term }); searches.add(search); } searchData.fetch({ data : { term : term } }) .then(dfd.resolve, dfd.reject) .always(searchForm.release); app/controllers/search Thursday, June 28, 12
  17. function update(t) { var term = $.trim(t), existing = searches.where({

    term : term }), search; app.set('currentSearch', term); if (existing.length) { search = existing[0]; search.update(); } else { search = new Search({ term : term }); searches.add(search); } searchData.fetch({ data : { term : term } }) .always(searchForm.release); app.router.navigate('search/' + term); } Thursday, June 28, 12
  18. describe("#update", function() { it("should update the time", function(done) { var

    search = new Search(), oldTime = search.get('time'); setTimeout(function() { search.update(); expect(search.get('time')).to.be.greaterThan(oldTime); done(); }, 1000); }); }); Thursday, June 28, 12
  19. it("should update when there is a new search", function() {

    expect(el.html()).not.to.contain('baz'); rs.currentSearch = function() { return 'baz'; }; rs.searches.add({ term : 'baz' }); expect(el.html()).to.contain('baz'); expect(el.find('.active').html()).to.contain('baz'); }); Thursday, June 28, 12