Sneaking structure into your DOM-based application

76f795cabbf80024b1024517c67f0bcf?s=47 Garann Means
November 16, 2011

Sneaking structure into your DOM-based application

You thought you were building a proof of concept, but then that proof of concept went live. Or you had two weeks to build what should have taken two months. Or the handful of progressive enhancements you threw onto a page to make the user experience a little nicer somehow evolved into an entire single-page app. Whatever the reason, you find yourself with a full-blown application built around click events and a staggering number of plugins you can't even remember downloading. If you could rewrite it, you'd use a framework built with your scenario in mind, but it gets 17 zillion hits a day and there's only one of you and starting from scratch isn't an option. No, what you need is the philosophy of a framework broken into discrete pieces that fit into a one- or two-week release cycle. This talk aims to provide bite-sized strategies you can implement in a short amount of time with minimal disruption to unchain your application from the DOM.

76f795cabbf80024b1024517c67f0bcf?s=128

Garann Means

November 16, 2011
Tweet

Transcript

  1. sneaking structure into your DOM-based application 1 Monday, November 14,

    2011
  2. ohai ☛ Garann Means ☛ Austin All-Girl Hack Night ☛

    Girl Develop It Austin ☛ @garannm / garann.com 2 Monday, November 14, 2011
  3. $(document).ready(function() { ! $.get("pages/title.html",function(r) { ! ! $(".page_text").html(r); ! },

    "html"); ! setInterval(function() { ! ! var currentpage = parseInt($("#currentpage").val()), ! ! ! num = window.location.hash.substring(1) || 0; ! ! if (currentpage != num) { ! ! ! goToPage(num); ! ! } ! }, 100); ! $(".continue a").click(function(e) { ! ! e.preventDefault(); ! ! goToPage(parseInt($("#currentpage").val())+1); ! });! ! $(".choose a").click(function(e) { ! ! e.preventDefault(); ! ! var newpage = $(this).attr("rel"); ! ! goToPage(newpage); ! });! ! $(document).bind("keydown",function(e) {! ! ! ! clearTimeout(typingPause);! ! ! ! var currentpage = parseInt($("#currentpage").val()); ! ! var k = keys[e.keyCode]; ! ! if (k == "left") { ! ! ! currentpage--; ! ! ! goToPage(currentpage); ! ! } else if (k == "right") { ! ! ! currentpage++; ! ! ! goToPage(currentpage); ! ! } else if (k) { ! ! ! newpage += "" + k; ! ! ! typingPause = setTimeout( ! ! ! ! function() { ! ! ! ! ! goToPage(newpage); ! ! ! ! }, 500); ! ! }! ! ! });! ! $('.pan-container').each(function(){ ! ! var $this=$(this).css({position:'relative', overflow:'hidden', cursor:'move'}) ! ! var $img=$this.children().eq(0) //image to pan ! ! var options={$pancontainer:$this, pos:$this.attr('data-orient'), curzoom:1, canzoom:$this.attr('data-canzoom'), wrappersize:[$this.width(), ! ! $img.imgmover(options) ! }) }); var newpage = ""; var typingPause; function goToPage(pagenum, pagetext, pageimg) {! ! $("#currentpage").val(pagenum); ! window.location.hash = pagenum;! ! newpage = ""; ! parseInt(pagenum) ? $("h3 span").text(pagenum) : $("h3 span").text("");! ! ! $.get("pages/" + pagetext,function(r) { ! ! $(".page_text").html(r);! ! ! ! pageimg ? $(".page_image").html('<img src="img/' + pageimg + '" />') : $(".page_image").html(""); ! ! if ($(".page_image").children().length) { ! ! ! $(".page_image").children().each(function() { ! ! ! ! $(this).zoomin({ ! ! ! ! ! bgcolor: "#999" ! ! ! ! }); ! ! ! }); ! ! }! ! ! ! $(".continue a").click(function(e) { ! ! ! e.preventDefault(); ! ! ! goToPage(parseInt($("#currentpage").val())+1); ! ! });! ! ! ! $(".choose a").click(function(e) { ! ! ! e.preventDefault(); ! ! ! var newpage = $(this).attr("rel"); ! ! ! goToPage(newpage); ! ! }); 3 Monday, November 14, 2011
  4. you have a problem. ☛ old code ☛ other people’s

    code ☛ written too fast ☛ scope creep 4 Monday, November 14, 2011
  5. but.. http://www.flickr.com/photos/vintagechica/5597949576/ 5 Monday, November 14, 2011

  6. your baby is ugly. http://www.flickr.com/photos/quinnanya/2885102816/in/photostream/ 6 Monday, November 14, 2011

  7. your baby is ugly. ☛ window.everything ☛ data in HTML

    ☛ all the plugins ever!! ☛ $(...).click(manageState) ☛ jQuery 1.oldAndBusted 7 Monday, November 14, 2011
  8. wontfix? ☛ performance gets worse ☛ hacks beget hacks ☛

    unmaintainable ☛ un-upgradable 8 Monday, November 14, 2011
  9. rewrite from scratch? ☛ undocumented business logic ☛ new bugs

    for old bugs ☛ business people will freak out ☛ users will be impatient ☛ how long you got? 9 Monday, November 14, 2011
  10. code written under duress is probably what got you here

    10 Monday, November 14, 2011
  11. why ‘sneaky’ ☛ business people see their features ☛ users

    see bugs fixed ☛ you see something other than the back of a giant boulder 11 Monday, November 14, 2011
  12. it’s gonna get dirty http://www.flickr.com/photos/johnpaulgoguen/3359392738/ 12 Monday, November 14, 2011

  13. it’s gonna get ridiculous http://www.flickr.com/photos/mhaithaca/447863503/ 13 Monday, November 14, 2011

  14. where you want to be 14 Monday, November 14, 2011

  15. where you want to be ☛ namespacing ☛ data separate

    from DOM ☛ not dependent on plugins ☛ state management separate from DOM ☛ jQuery 1.newHotness 14 Monday, November 14, 2011
  16. (and then you can think about..) ☛ MVC ☛ AMD

    ☛ unit tests ☛ event delegation ☛ deferreds ☛ etc. 15 Monday, November 14, 2011
  17. keep it simple ☛ one release at a time ☛

    stick to the plan ☛ don’t get scared 16 Monday, November 14, 2011
  18. let’s get cracking 17 Monday, November 14, 2011

  19. release 0 ☛ take stock ☛ add TODOs ☛ basic

    reuse 18 Monday, November 14, 2011
  20. TODO ! // TODO: find something less icky ! setInterval(function()

    { ! ! var currentpage = parseInt($("#currentpage").val()), ! ! ! num = window.location.hash.substring(1) || 0; ! ! if (currentpage != num) { ! ! ! goToPage(num); ! ! } ! }, 100); ! ! // TODO: rethink this whole thing ! $(document).bind("keydown",function(e) { ! ! ! ! clearTimeout(typingPause); ! ! ... ! ! var currentpage = parseInt($("#currentpage").val()); ! ! var k = keys[e.keyCode]; 19 Monday, November 14, 2011
  21. cache/improve selectors ! newpage = ""; ! parseInt(pagenum) ? !

    ! $("h3 span").text(pagenum) : ! ! $("h3 span").text("");! ! $.get("pages/" + pagetext,function(r) { ! ! $(".page_text").html(r);! ! ! ! pageimg ? ! ! ! $(".page_image").html('<img src="img/' + pageimg + '" ! ! ! $(".page_image").html(""); ! ! if ($(".page_image").children().length) { ! ! ! $(".page_image").children().each(function() { ! ! ! ! $(this).zoomin({ ! ! ! ! ! bgcolor: "#999" ! ! ! ! }); ! ! ! }); ! ! } ! ! ... 20 Monday, November 14, 2011
  22. cache/improve selectors ! // r0: saved .page_image selector ! var

    $h = $("h3 span"), ! ! $p = $("div.page_image"); ! newpage = ""; ! parseInt(pagenum) ? $h.text(pagenum) : $h.text("");! ! $.get("pages/" + pagetext,function(r) { ! ! $(".page_text").html(r);! ! ! ! pageimg ? ! ! ! $p.html('<img src="img/' + pageimg + '" />') : ! ! ! $p.html(""); ! ! var imgs = $p.children(); ! ! if (imgs.length) { ! ! ! $.each(imgs,function() { ! ! ! ! $(this).zoomin({ ! ! ! ! ! bgcolor: "#999" ! ! ! ! }); ! ! ! }); 21 Monday, November 14, 2011
  23. avoid repetition $(document).ready(function() { ! $.get("pages/title.html",function(r) { ! ! $(".page_text").html(r);

    ! }, "html"); ! ! $(".continue a").click(function(e) { ! ! e.preventDefault(); ! ! goToPage(parseInt($("#currentpage").val())+1); ! }); ! ! $(".choose a").click(function(e) { ! ! e.preventDefault(); ! ! var newpage = $(this).attr("rel"); ! ! goToPage(newpage); ! }); }); 22 Monday, November 14, 2011
  24. avoid repetition $(document).ready(function() { ! ! // r0: removed repeated

    code (+ link wireups) ! goToPage(0); }); 23 Monday, November 14, 2011
  25. now you have.. ☛ tasks defined ☛ reuse ☛ abstraction

    24 Monday, November 14, 2011
  26. release 1 ☛ all your code under one namespace ☛

    that’s it. ☛ find, replace, test ☛ memory lane 25 Monday, November 14, 2011
  27. leave window alone var newpage = ""; // TODO: is

    this necessary? var keys = { ! "37": "left", ! "39": "right" }; var typingPause; // TODO: fewer arguments function goToPage(pagenum, pagetext, pageimg) { ! ... 26 Monday, November 14, 2011
  28. leave window alone // r1: added this namespace var cyoa

    = cyoa || { ! ! newpage: "", ! ! // TODO: is this necessary? ! ! keys: { ! ! ! "37": "left", ! ! ! "39": "right" ! ! }, ! ! typingPause: null ! }; // TODO: fewer arguments cyoa.goToPage = function(pagenum, pagetext, pageimg) { 27 Monday, November 14, 2011
  29. check for inline JS <a href=”javascript:goToPage(9)”>click here!!1</a> <script type=”text/javascript”> if

    (newpage == “”) document.write(“no new page to load”); </script> 28 Monday, November 14, 2011
  30. check external code // r1: added this namespace var cyoa

    = cyoa || { ! ! newpage: "", ! ! ... ! ! typingPause: null ! }; // r1: well it would have been cool, anyway.. var newpage = function() { ! return cyoa.newpage; }; 29 Monday, November 14, 2011
  31. now you have.. ☛ your stuff is isolated ☛ group

    pieces of app ☛ good overview 30 Monday, November 14, 2011
  32. release 2 ☛ hidden fields ☛ attributes ☛ add new

    objects at the right level ☛ stay out of display code.. for now 31 Monday, November 14, 2011
  33. val() // TODO: fewer arguments cyoa.goToPage = function(pagenum, pagetext, pageimg)

    { ! ... ! $("#currentpage").val(pagenum); ! window.location.hash = pagenum; 32 Monday, November 14, 2011
  34. val() // TODO: fewer arguments cyoa.goToPage = function(pagenum, pagetext, pageimg)

    { ! ... ! cyoa.currentPage = pagenum; ! window.location.hash = pagenum; 33 Monday, November 14, 2011
  35. attr() <a rel="4">If you decide to refactor, turn to page

    4.</a> $("div.choose a").click(function(e) { ! e.preventDefault(); ! var newpage = $(this).attr("rel"); ! cyoa.goToPage(newpage); }); 34 Monday, November 14, 2011
  36. attr() <a href="#4">If you decide to refactor, turn to page

    4.</a> (or) // TODO: add to state object $("div.choose a").click(function(e) { ! e.preventDefault(); ! var newpage = $(this).attr("rel"); ! cyoa.goToPage(newpage); }); 35 Monday, November 14, 2011
  37. now you have.. ☛ DOM data vs. non-DOM data ☛

    access data more quickly ☛ change HTML without breaking app 36 Monday, November 14, 2011
  38. release 3 ☛ isolate existing plugins ☛ formalize widgets ☛

    my.plugin = function($t) or $.fn.plugin 37 Monday, November 14, 2011
  39. wrapping plugins ! ! var imgs = $p.children(); ! !

    if (imgs.length) { ! ! ! $.each(imgs,function() { ! ! ! ! $(this).zoomin({ ! ! ! ! ! bgcolor: "#999" ! ! ! ! }); ! ! ! }); ! ! } 38 Monday, November 14, 2011
  40. wrapping plugins ! ! // r3: removed plugin setup code

    ! ! cyoa.setUpZoom($p.children()); ... cyoa = { ! ! setUpZoom: function($t) { ! ! ! if ($t.length) { ! ! ! ! $t.each(function() { ! ! ! ! ! cyoa.zoomin($(this), { ! ! ! ! ! ! bgcolor: "#999" ! ! ! ! ! }); ! ! ! ! }); ! ! ! } ! ! } } 39 Monday, November 14, 2011
  41. not $.fn.everything ☛ known element type? ☛ known class? ☛

    known properties? ☛ known length? ☛ that’s a widget, y’all 40 Monday, November 14, 2011
  42. now you have.. ☛ easy plugin swap/upgrade ☛ app-specific stock

    of widgets ☛ your junk out of $.fn.* ☛ widgets within relevant state in... 41 Monday, November 14, 2011
  43. release 4 ☛ state objects ☛ steps in arrays ☛

    non-linear states: myApp.states[“thisState”] ☛ state functions ☛ init ☛ change state ☛ error 42 Monday, November 14, 2011
  44. confine state info // TODO: fewer arguments cyoa.goToPage = function(pagenum,

    pagetext, pageimg) { ! // TODO: put this logic someplace else ! if (!pagetext) { ! ! switch (parseInt(pagenum)) { ! ! ! case 0: ! ! ! ! cyoa.goToPage0(); ! ! ! ! break; ! ! ! case 1: ! ! ! ! cyoa.goToPage1(); ! ! ! ! break; ! ! ! case 2: ! ! ! ! cyoa.goToPage2(); ! ! ! ! break; ! ! ! ... 43 Monday, November 14, 2011
  45. confine state info // r4: added more structured state management

    cyoa.state = { ! init: function() { ! ! var num = window.location.hash.substring(1) || 0; ! ! cyoa.state.goToPage(num); ! }, ! ! goToPage: function(pagenum) { ! ! ! ! // r4: check that page is valid ! ! pagenum = parseInt(pagenum); ! ! if (pagenum < 0 || pagenum >= cyoa.states.length) ! ! ! return; ! ! ! ! cyoa.currentPage = pagenum; ! ! window.location.hash = pagenum; 44 Monday, November 14, 2011
  46. confine state info // TODO: NOT THIS. cyoa.goToPage0 = function()

    { ! cyoa.goToPage(0,"title.html"); } cyoa.goToPage1 = function() { ! cyoa.goToPage(1,"whatToDo.html"); } cyoa.goToPage2 = function() { ! cyoa.goToPage(2,"youHaveDied.html","explosion.jpg"); } cyoa.goToPage3 = function() { ! cyoa.goToPage(3,"youHaveDied.html","sisyphus.jpg"); } cyoa.goToPage4 = function() { ! cyoa.goToPage(4,"sellIt.html"); } 45 Monday, November 14, 2011
  47. confine state info // r4: created array with page/state info

    cyoa.states = [ ! {page: "title.html"},! ! {page: "whatToDo.html"},! ! {page: "youHaveDied.html", image: "explosion.jpg"},! ! {page: "youHaveDied.html", image: "sisyphus.jpg"},! ! {page: "sellIt.html"} ]; // extra credit: routing info 46 Monday, November 14, 2011
  48. now you have.. ☛ state differences encapsulated ☛ single place

    to switch state ☛ clear definitions ☛ add states cleanly ☛ add in routing with less risk 47 Monday, November 14, 2011
  49. release 5 ☛ pub/sub instead of event handlers ☛ reduce

    anonymous functions ☛ event handlers manage $(this) ☛ onreadystatechange publishes 48 Monday, November 14, 2011
  50. publish/subscribe ! ! $.get("pages/" + state.page,function(r) { ! ! !

    $("div.page_text").html(r); ! ! ! ! ! ! img ? ! ! ! ! $p.html('<img src="img/' + img + '" />') : ! ! ! ! $p.html(""); ! ! ! // r3: removed plugin setup code ! ! ! cyoa.setUpZoom($p.children()); ! ! ! ! ! ! $("div.continue a").click(function(e) { ! ! ! ! e.preventDefault(); ! ! ! ! cyoa.state.goToPage(cyoa.currentPage+1); ! ! ! }); ! ! ! ! ! ! ...! ! ! ! ! ! }, "html"); 49 Monday, November 14, 2011
  51. publish/subscribe ! ! $.get("pages/" + state.page,function(r) {! ! ! !

    ! ! cyoa.event.publish("pageLoaded", [r]);! ! ! }, "html"); ... ! cyoa.event.subscribe("pageLoaded", function(r) {! ! ! ! ! $("div.page_text").html(r); ! ! ! ! $("div.continue a").click(function(e) { ! ! ! e.preventDefault(); ! ! ! cyoa.currentPage += 1; ! ! }); ! ! ! ! }); 50 Monday, November 14, 2011
  52. pub/sub and get/set cyoa = { ! ! _currentPage: 0,

    ! ! // r5: getter and setter for currentPage ! ! get currentPage() { return this._currentPage; }, ! ! set currentPage(n) { ! ! ! if (n < 0 || n >= cyoa.states.length) return; ! ! ! this._currentPage = n; ! ! ! cyoa.event.publish("pageChanged"); ! ! } } ... cyoa.event.subscribe("pageChanged", function(r) {!! ! cyoa.state.goToPage(); }); 51 Monday, November 14, 2011
  53. now you have.. ☛ application events vs. DOM events ☛

    no manual callback chains ☛ easily add features that observe events ☛ control state through properties 52 Monday, November 14, 2011
  54. release 6 ☛ upgrade ☛ regression test ☛ swap out

    non-forward-compatible plugins ☛ regression test ☛ maybe roll back (sorries :( ) 53 Monday, November 14, 2011
  55. upgrade and test ! cyoa.event.subscribe("pageLoaded", function(r) {! ! ! !

    ! $("div.page_text").html(r); ! ! ! ! $("div.continue a").click(function(e) { ! ! ! e.preventDefault(); ! ! ! cyoa.currentPage += 1; ! ! }); ! ! ! ! }); 54 Monday, November 14, 2011
  56. upgrade and test ! // r6: don't keep binding this

    every time ! $("div.page_text") ! ! .delegate("div.continue a","click",function(e) { ! ! e.preventDefault(); ! ! cyoa.currentPage += 1; ! }); 55 Monday, November 14, 2011
  57. when you can’t upgrade ☛ bugs should be easier to

    find ☛ fix them ☛ keep trying ☛ ala carte features 56 Monday, November 14, 2011
  58. but once you do.. !! ☛ dependency management ☛ event

    delegation ☛ deferreds ☛ unit tests ☛ documentation ☛ ..framework? maybe? 57 Monday, November 14, 2011
  59. nobody saw a thing ☛ business people: “Oh I thought

    you finished that five releases ago?” ☛ users: less “It’s too slow,” more “Why can’t I make this have polka dots?” 58 Monday, November 14, 2011
  60. you: ☛ “:D” ☛ can develop faster ☛ can fix

    easier ☛ can get hit by all the buses you want 59 Monday, November 14, 2011
  61. hey, since you did such a super job on that

    refactor... 60 Monday, November 14, 2011
  62. thanks! ☛ who’s got questions? ☛ shy questions: ☛ @garannm

    ☛ garann@gmail.com 61 Monday, November 14, 2011