Share
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
Transcript
62 slides
-
ohai ☛ Garann Means ☛ Austin All-Girl Hack Night ☛
Girl Develop It Austin ☛ @garannm / garann.com 2 Monday, November 14, 2011 -
$(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 -
you have a problem. ☛ old code ☛ other people’s
code ☛ written too fast ☛ scope creep 4 Monday, November 14, 2011 -
your baby is ugly. ☛ window.everything ☛ data in HTML
☛ all the plugins ever!! ☛ $(...).click(manageState) ☛ jQuery 1.oldAndBusted 7 Monday, November 14, 2011 -
wontfix? ☛ performance gets worse ☛ hacks beget hacks ☛
unmaintainable ☛ un-upgradable 8 Monday, November 14, 2011 -
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 -
code written under duress is probably what got you here
10 Monday, November 14, 2011 -
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 -
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 -
(and then you can think about..) ☛ MVC ☛ AMD
☛ unit tests ☛ event delegation ☛ deferreds ☛ etc. 15 Monday, November 14, 2011 -
keep it simple ☛ one release at a time ☛
stick to the plan ☛ don’t get scared 16 Monday, November 14, 2011 -
release 0 ☛ take stock ☛ add TODOs ☛ basic
reuse 18 Monday, November 14, 2011 -
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 -
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 -
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 -
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 -
avoid repetition $(document).ready(function() { ! ! // r0: removed repeated
code (+ link wireups) ! goToPage(0); }); 23 Monday, November 14, 2011 -
now you have.. ☛ tasks defined ☛ reuse ☛ abstraction
24 Monday, November 14, 2011 -
release 1 ☛ all your code under one namespace ☛
that’s it. ☛ find, replace, test ☛ memory lane 25 Monday, November 14, 2011 -
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 -
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 -
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 -
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 -
now you have.. ☛ your stuff is isolated ☛ group
pieces of app ☛ good overview 30 Monday, November 14, 2011 -
release 2 ☛ hidden fields ☛ attributes ☛ add new
objects at the right level ☛ stay out of display code.. for now 31 Monday, November 14, 2011 -
val() // TODO: fewer arguments cyoa.goToPage = function(pagenum, pagetext, pageimg)
{ ! ... ! $("#currentpage").val(pagenum); ! window.location.hash = pagenum; 32 Monday, November 14, 2011 -
val() // TODO: fewer arguments cyoa.goToPage = function(pagenum, pagetext, pageimg)
{ ! ... ! cyoa.currentPage = pagenum; ! window.location.hash = pagenum; 33 Monday, November 14, 2011 -
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 -
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 -
now you have.. ☛ DOM data vs. non-DOM data ☛
access data more quickly ☛ change HTML without breaking app 36 Monday, November 14, 2011 -
release 3 ☛ isolate existing plugins ☛ formalize widgets ☛
my.plugin = function($t) or $.fn.plugin 37 Monday, November 14, 2011 -
wrapping plugins ! ! var imgs = $p.children(); ! !
if (imgs.length) { ! ! ! $.each(imgs,function() { ! ! ! ! $(this).zoomin({ ! ! ! ! ! bgcolor: "#999" ! ! ! ! }); ! ! ! }); ! ! } 38 Monday, November 14, 2011 -
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 -
not $.fn.everything ☛ known element type? ☛ known class? ☛
known properties? ☛ known length? ☛ that’s a widget, y’all 40 Monday, November 14, 2011 -
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 -
release 4 ☛ state objects ☛ steps in arrays ☛
non-linear states: myApp.states[“thisState”] ☛ state functions ☛ init ☛ change state ☛ error 42 Monday, November 14, 2011 -
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 -
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 -
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 -
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 -
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 -
release 5 ☛ pub/sub instead of event handlers ☛ reduce
anonymous functions ☛ event handlers manage $(this) ☛ onreadystatechange publishes 48 Monday, November 14, 2011 -
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 -
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 -
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 -
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 -
release 6 ☛ upgrade ☛ regression test ☛ swap out
non-forward-compatible plugins ☛ regression test ☛ maybe roll back (sorries :( ) 53 Monday, November 14, 2011 -
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 -
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 -
when you can’t upgrade ☛ bugs should be easier to
find ☛ fix them ☛ keep trying ☛ ala carte features 56 Monday, November 14, 2011 -
but once you do.. !! ☛ dependency management ☛ event
delegation ☛ deferreds ☛ unit tests ☛ documentation ☛ ..framework? maybe? 57 Monday, November 14, 2011 -
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 -
you: ☛ “:D” ☛ can develop faster ☛ can fix
easier ☛ can get hit by all the buses you want 59 Monday, November 14, 2011 -
hey, since you did such a super job on that
refactor... 60 Monday, November 14, 2011 -
thanks! ☛ who’s got questions? ☛ shy questions: ☛ @garannm
☛ [email protected] 61 Monday, November 14, 2011