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

Sneaking structure into your DOM-based application

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.

Garann Means

November 16, 2011
Tweet

More Decks by Garann Means

Other Decks in Technology

Transcript

  1. ohai ☛ Garann Means ☛ Austin All-Girl Hack Night ☛

    Girl Develop It Austin ☛ @garannm / garann.com 2 Monday, November 14, 2011
  2. $(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
  3. you have a problem. ☛ old code ☛ other people’s

    code ☛ written too fast ☛ scope creep 4 Monday, November 14, 2011
  4. your baby is ugly. ☛ window.everything ☛ data in HTML

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

    unmaintainable ☛ un-upgradable 8 Monday, November 14, 2011
  6. 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
  7. 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
  8. 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
  9. (and then you can think about..) ☛ MVC ☛ AMD

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

    stick to the plan ☛ don’t get scared 16 Monday, November 14, 2011
  11. release 0 ☛ take stock ☛ add TODOs ☛ basic

    reuse 18 Monday, November 14, 2011
  12. 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
  13. 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
  14. 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
  15. 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
  16. avoid repetition $(document).ready(function() { ! ! // r0: removed repeated

    code (+ link wireups) ! goToPage(0); }); 23 Monday, November 14, 2011
  17. release 1 ☛ all your code under one namespace ☛

    that’s it. ☛ find, replace, test ☛ memory lane 25 Monday, November 14, 2011
  18. 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
  19. 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
  20. 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
  21. 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
  22. now you have.. ☛ your stuff is isolated ☛ group

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

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

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

    { ! ... ! cyoa.currentPage = pagenum; ! window.location.hash = pagenum; 33 Monday, November 14, 2011
  26. 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
  27. 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
  28. now you have.. ☛ DOM data vs. non-DOM data ☛

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

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

    if (imgs.length) { ! ! ! $.each(imgs,function() { ! ! ! ! $(this).zoomin({ ! ! ! ! ! bgcolor: "#999" ! ! ! ! }); ! ! ! }); ! ! } 38 Monday, November 14, 2011
  31. 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
  32. not $.fn.everything ☛ known element type? ☛ known class? ☛

    known properties? ☛ known length? ☛ that’s a widget, y’all 40 Monday, November 14, 2011
  33. 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
  34. release 4 ☛ state objects ☛ steps in arrays ☛

    non-linear states: myApp.states[“thisState”] ☛ state functions ☛ init ☛ change state ☛ error 42 Monday, November 14, 2011
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. release 5 ☛ pub/sub instead of event handlers ☛ reduce

    anonymous functions ☛ event handlers manage $(this) ☛ onreadystatechange publishes 48 Monday, November 14, 2011
  41. 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
  42. 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
  43. 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
  44. 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
  45. release 6 ☛ upgrade ☛ regression test ☛ swap out

    non-forward-compatible plugins ☛ regression test ☛ maybe roll back (sorries :( ) 53 Monday, November 14, 2011
  46. 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
  47. 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
  48. when you can’t upgrade ☛ bugs should be easier to

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

    delegation ☛ deferreds ☛ unit tests ☛ documentation ☛ ..framework? maybe? 57 Monday, November 14, 2011
  50. 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
  51. you: ☛ “:D” ☛ can develop faster ☛ can fix

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

    refactor... 60 Monday, November 14, 2011