SlideShare une entreprise Scribd logo
1  sur  62
Télécharger pour lire hors ligne
sneaking structure into
                           your DOM-based
                              application


Sunday, April 17, 2011
ohai

                     ☛ Garann    Means
                     ☛ Vast

                     ☛ Austin   All-Girl Hack Night
                     ☛ @garannm     / garann.com




Sunday, April 17, 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);
                !       !       });


Sunday, April 17, 2011
you have a problem.

                     ☛ old   code
                     ☛ other   people’s code
                     ☛ written   too fast
                     ☛ scope   creep




Sunday, April 17, 2011
but..




                             http://www.flickr.com/photos/vintagechica/5597949576/




Sunday, April 17, 2011
your baby is ugly.




                                 http://www.flickr.com/photos/quinnanya/2885102816/in/photostream/




Sunday, April 17, 2011
your baby is ugly.
                     ☛ window.everything

                     ☛ data    in HTML
                     ☛ all   the plugins ever!!
                     ☛ $(...).click(manageState)

                     ☛ jQuery    1.oldAndBusted



Sunday, April 17, 2011
wontfix?

                     ☛ performance   gets worse
                     ☛ hacks   beget hacks
                     ☛ unmaintainable

                     ☛ un-upgradable




Sunday, April 17, 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?



Sunday, April 17, 2011
code written under
                         duress is probably what
                              got you here


Sunday, April 17, 2011
why ‘sneaky’

                     ☛ business   people see their features
                     ☛ users   see bugs fixed
                     ☛ you   see something other than the back of a
                         giant boulder




Sunday, April 17, 2011
it’s gonna get dirty




                                    http://www.flickr.com/photos/johnpaulgoguen/3359392738/




Sunday, April 17, 2011
it’s gonna get ridiculous




                                       http://www.flickr.com/photos/mhaithaca/447863503/




Sunday, April 17, 2011
where you want to be




Sunday, April 17, 2011
where you want to be
                     ☛ namespacing

                     ☛ data    separate from DOM
                     ☛ not   dependent on plugins
                     ☛ state   management separate from DOM
                     ☛ jQuery   1.newHotness



Sunday, April 17, 2011
(and then you can think
                                 about..)
                     ☛ MVC

                     ☛ AMD

                     ☛ unit   tests
                     ☛ event    delegation
                     ☛ deferreds

                     ☛ etc.



Sunday, April 17, 2011
keep it simple

                     ☛ one   release at a time
                     ☛ stick   to the plan
                     ☛ don’t   get scared




Sunday, April 17, 2011
let’s get cracking




Sunday, April 17, 2011
release 0

                     ☛ take   stock
                     ☛ add   TODOs
                     ☛ basic   reuse




Sunday, April 17, 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];

Sunday, April 17, 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"
       !       ! ! ! });
       !       ! ! });
       !       ! }
       !       ! ...
Sunday, April 17, 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"
       !       ! ! ! });
       !       ! ! });
Sunday, April 17, 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);
       ! });
       });

Sunday, April 17, 2011
avoid repetition
       $(document).ready(function() {
       !
       ! // r0: removed repeated code (+ link wireups)
       ! goToPage(0);

       });




Sunday, April 17, 2011
now you have..

                     ☛ tasks   defined
                     ☛ reuse

                     ☛ abstraction




Sunday, April 17, 2011
release 1

                     ☛ all   your code under one namespace
                     ☛ that’s   it.
                     ☛ find,   replace, test
                     ☛ memory         lane




Sunday, April 17, 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) {
       ! ...




Sunday, April 17, 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) {




Sunday, April 17, 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>




Sunday, April 17, 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;
       };




Sunday, April 17, 2011
now you have..

                     ☛ your   stuff is isolated
                     ☛ group   pieces of app
                     ☛ good   overview




Sunday, April 17, 2011
release 2

                     ☛ hidden   fields
                     ☛ attributes

                     ☛ add    new objects at the right level
                     ☛ stay   out of display code.. for now




Sunday, April 17, 2011
val()
       // TODO: fewer arguments
       cyoa.goToPage = function(pagenum, pagetext, pageimg) {

       !       ...

       !       $("#currentpage").val(pagenum);
       !       window.location.hash = pagenum;




Sunday, April 17, 2011
val()
       // TODO: fewer arguments
       cyoa.goToPage = function(pagenum, pagetext, pageimg) {

       !       ...

       !       cyoa.currentPage = pagenum;
       !       window.location.hash = pagenum;




Sunday, April 17, 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);
       });




Sunday, April 17, 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);
       });




Sunday, April 17, 2011
now you have..

                     ☛ DOM   data vs. non-DOM data
                     ☛ access   data more quickly
                     ☛ change   HTML without breaking app




Sunday, April 17, 2011
release 3

                     ☛ isolate   existing plugins
                     ☛ formalize   widgets
                     ☛ my.plugin   = function($t) or $.fn.plugin




Sunday, April 17, 2011
wrapping plugins
       !       !         var imgs = $p.children();
       !       !         if (imgs.length) {
       !       !         ! $.each(imgs,function() {
       !       !         ! ! $(this).zoomin({
       !       !         ! ! ! bgcolor: "#999"
       !       !         ! ! });
       !       !         ! });
       !       !         }




Sunday, April 17, 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"
       ! ! ! ! ! });
       ! ! ! ! });
       ! ! ! }
       ! ! }
       }




Sunday, April 17, 2011
not $.fn.everything
                     ☛ known     element type?
                     ☛ known     class?
                     ☛ known     properties?
                     ☛ known     length?
                     ☛ that’s   a widget, y’all



Sunday, April 17, 2011
now you have..

                     ☛ easy   plugin swap/upgrade
                     ☛ app-specific   stock of widgets
                     ☛ your   junk out of $.fn.*
                     ☛ widgets   within relevant state in...



Sunday, April 17, 2011
release 4
                     ☛ state      objects
                         ☛ steps    in arrays
                         ☛ non-linear    states: myApp.states[“thisState”]
                     ☛ state      functions
                         ☛ init

                         ☛ change     state
                         ☛ error

Sunday, April 17, 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;
       !       !         ! ...

Sunday, April 17, 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;

Sunday, April 17, 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");
       }

Sunday, April 17, 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




Sunday, April 17, 2011
now you have..
                     ☛ state   differences encapsulated
                     ☛ single   place to switch state
                     ☛ clear   definitions
                     ☛ add   states cleanly
                     ☛ add   in routing with less risk



Sunday, April 17, 2011
release 5

                     ☛ pub/sub     instead of event handlers
                     ☛ reduce   anonymous functions
                         ☛ event   handlers manage $(this)
                         ☛ onreadystatechange    publishes



Sunday, April 17, 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");
Sunday, April 17, 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;
       !       ! });
       !       ! !
       !       });

Sunday, April 17, 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();
       });




Sunday, April 17, 2011
now you have..

                     ☛ application   events vs. DOM events
                     ☛ no   manual callback chains
                     ☛ easily   add features that observe events
                     ☛ control   state through properties




Sunday, April 17, 2011
release 6
                     ☛ upgrade

                     ☛ regression   test
                     ☛ swap   out non-forward-compatible plugins
                     ☛ regression   test
                     ☛ maybe   roll back (sorries :( )



Sunday, April 17, 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;
       !       ! });
       !       ! !
       !       });




Sunday, April 17, 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;

       !       });




Sunday, April 17, 2011
when you can’t upgrade

                     ☛ bugs   should be easier to find
                     ☛ fix   them
                     ☛ keep    trying
                     ☛ ala   carte features




Sunday, April 17, 2011
but once you do.. !!
                     ☛ dependency     management
                     ☛ event    delegation
                     ☛ deferreds

                     ☛ unit   tests
                     ☛ documentation

                     ☛ ..framework?   maybe?


Sunday, April 17, 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?”




Sunday, April 17, 2011
you:
                     ☛ “:D”




                     ☛ can    develop faster
                     ☛ can    fix easier
                     ☛ can    get hit by all the buses you want



Sunday, April 17, 2011
hey, since you did such a
                      super job on that
                          refactor...


Sunday, April 17, 2011
thanks!

                     ☛ who’s   got questions?
                     ☛ shy   questions:
                         ☛ @garannm

                         ☛ garann@gmail.com




Sunday, April 17, 2011

Contenu connexe

Dernier

Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Victor Rentea
 
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Safe Software
 
Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
WSO2
 

Dernier (20)

Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
 
Rising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdf
Rising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdfRising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdf
Rising Above_ Dubai Floods and the Fortitude of Dubai International Airport.pdf
 
Boost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfBoost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdf
 
MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024
 
Spring Boot vs Quarkus the ultimate battle - DevoxxUK
Spring Boot vs Quarkus the ultimate battle - DevoxxUKSpring Boot vs Quarkus the ultimate battle - DevoxxUK
Spring Boot vs Quarkus the ultimate battle - DevoxxUK
 
Corporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxCorporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptx
 
Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...
 
Apidays New York 2024 - APIs in 2030: The Risk of Technological Sleepwalk by ...
Apidays New York 2024 - APIs in 2030: The Risk of Technological Sleepwalk by ...Apidays New York 2024 - APIs in 2030: The Risk of Technological Sleepwalk by ...
Apidays New York 2024 - APIs in 2030: The Risk of Technological Sleepwalk by ...
 
Navigating the Deluge_ Dubai Floods and the Resilience of Dubai International...
Navigating the Deluge_ Dubai Floods and the Resilience of Dubai International...Navigating the Deluge_ Dubai Floods and the Resilience of Dubai International...
Navigating the Deluge_ Dubai Floods and the Resilience of Dubai International...
 
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
 
Exploring Multimodal Embeddings with Milvus
Exploring Multimodal Embeddings with MilvusExploring Multimodal Embeddings with Milvus
Exploring Multimodal Embeddings with Milvus
 
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
 
Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
 
Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024
 
Ransomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdfRansomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdf
 
DEV meet-up UiPath Document Understanding May 7 2024 Amsterdam
DEV meet-up UiPath Document Understanding May 7 2024 AmsterdamDEV meet-up UiPath Document Understanding May 7 2024 Amsterdam
DEV meet-up UiPath Document Understanding May 7 2024 Amsterdam
 
Cyberprint. Dark Pink Apt Group [EN].pdf
Cyberprint. Dark Pink Apt Group [EN].pdfCyberprint. Dark Pink Apt Group [EN].pdf
Cyberprint. Dark Pink Apt Group [EN].pdf
 
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
 
Artificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : UncertaintyArtificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : Uncertainty
 
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, AdobeApidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
 

En vedette

How Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthHow Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental Health
ThinkNow
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie Insights
Kurio // The Social Media Age(ncy)
 

En vedette (20)

2024 State of Marketing Report – by Hubspot
2024 State of Marketing Report – by Hubspot2024 State of Marketing Report – by Hubspot
2024 State of Marketing Report – by Hubspot
 
Everything You Need To Know About ChatGPT
Everything You Need To Know About ChatGPTEverything You Need To Know About ChatGPT
Everything You Need To Know About ChatGPT
 
Product Design Trends in 2024 | Teenage Engineerings
Product Design Trends in 2024 | Teenage EngineeringsProduct Design Trends in 2024 | Teenage Engineerings
Product Design Trends in 2024 | Teenage Engineerings
 
How Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthHow Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental Health
 
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdfAI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
 
Skeleton Culture Code
Skeleton Culture CodeSkeleton Culture Code
Skeleton Culture Code
 
PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024
 
Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)
 
How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie Insights
 
Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024
 
5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary
 
ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd
 
Getting into the tech field. what next
Getting into the tech field. what next Getting into the tech field. what next
Getting into the tech field. what next
 
Google's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentGoogle's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search Intent
 
How to have difficult conversations
How to have difficult conversations How to have difficult conversations
How to have difficult conversations
 
Introduction to Data Science
Introduction to Data ScienceIntroduction to Data Science
Introduction to Data Science
 
Time Management & Productivity - Best Practices
Time Management & Productivity -  Best PracticesTime Management & Productivity -  Best Practices
Time Management & Productivity - Best Practices
 
The six step guide to practical project management
The six step guide to practical project managementThe six step guide to practical project management
The six step guide to practical project management
 
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
 

Sneaking structure into your DOM-based application

  • 1. sneaking structure into your DOM-based application Sunday, April 17, 2011
  • 2. ohai ☛ Garann Means ☛ Vast ☛ Austin All-Girl Hack Night ☛ @garannm / garann.com Sunday, April 17, 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); ! ! }); Sunday, April 17, 2011
  • 4. you have a problem. ☛ old code ☛ other people’s code ☛ written too fast ☛ scope creep Sunday, April 17, 2011
  • 5. but.. http://www.flickr.com/photos/vintagechica/5597949576/ Sunday, April 17, 2011
  • 6. your baby is ugly. http://www.flickr.com/photos/quinnanya/2885102816/in/photostream/ Sunday, April 17, 2011
  • 7. your baby is ugly. ☛ window.everything ☛ data in HTML ☛ all the plugins ever!! ☛ $(...).click(manageState) ☛ jQuery 1.oldAndBusted Sunday, April 17, 2011
  • 8. wontfix? ☛ performance gets worse ☛ hacks beget hacks ☛ unmaintainable ☛ un-upgradable Sunday, April 17, 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? Sunday, April 17, 2011
  • 10. code written under duress is probably what got you here Sunday, April 17, 2011
  • 11. why ‘sneaky’ ☛ business people see their features ☛ users see bugs fixed ☛ you see something other than the back of a giant boulder Sunday, April 17, 2011
  • 12. it’s gonna get dirty http://www.flickr.com/photos/johnpaulgoguen/3359392738/ Sunday, April 17, 2011
  • 13. it’s gonna get ridiculous http://www.flickr.com/photos/mhaithaca/447863503/ Sunday, April 17, 2011
  • 14. where you want to be Sunday, April 17, 2011
  • 15. where you want to be ☛ namespacing ☛ data separate from DOM ☛ not dependent on plugins ☛ state management separate from DOM ☛ jQuery 1.newHotness Sunday, April 17, 2011
  • 16. (and then you can think about..) ☛ MVC ☛ AMD ☛ unit tests ☛ event delegation ☛ deferreds ☛ etc. Sunday, April 17, 2011
  • 17. keep it simple ☛ one release at a time ☛ stick to the plan ☛ don’t get scared Sunday, April 17, 2011
  • 19. release 0 ☛ take stock ☛ add TODOs ☛ basic reuse Sunday, April 17, 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]; Sunday, April 17, 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" ! ! ! ! }); ! ! ! }); ! ! } ! ! ... Sunday, April 17, 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" ! ! ! ! }); ! ! ! }); Sunday, April 17, 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); ! }); }); Sunday, April 17, 2011
  • 24. avoid repetition $(document).ready(function() { ! ! // r0: removed repeated code (+ link wireups) ! goToPage(0); }); Sunday, April 17, 2011
  • 25. now you have.. ☛ tasks defined ☛ reuse ☛ abstraction Sunday, April 17, 2011
  • 26. release 1 ☛ all your code under one namespace ☛ that’s it. ☛ find, replace, test ☛ memory lane Sunday, April 17, 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) { ! ... Sunday, April 17, 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) { Sunday, April 17, 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> Sunday, April 17, 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; }; Sunday, April 17, 2011
  • 31. now you have.. ☛ your stuff is isolated ☛ group pieces of app ☛ good overview Sunday, April 17, 2011
  • 32. release 2 ☛ hidden fields ☛ attributes ☛ add new objects at the right level ☛ stay out of display code.. for now Sunday, April 17, 2011
  • 33. val() // TODO: fewer arguments cyoa.goToPage = function(pagenum, pagetext, pageimg) { ! ... ! $("#currentpage").val(pagenum); ! window.location.hash = pagenum; Sunday, April 17, 2011
  • 34. val() // TODO: fewer arguments cyoa.goToPage = function(pagenum, pagetext, pageimg) { ! ... ! cyoa.currentPage = pagenum; ! window.location.hash = pagenum; Sunday, April 17, 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); }); Sunday, April 17, 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); }); Sunday, April 17, 2011
  • 37. now you have.. ☛ DOM data vs. non-DOM data ☛ access data more quickly ☛ change HTML without breaking app Sunday, April 17, 2011
  • 38. release 3 ☛ isolate existing plugins ☛ formalize widgets ☛ my.plugin = function($t) or $.fn.plugin Sunday, April 17, 2011
  • 39. wrapping plugins ! ! var imgs = $p.children(); ! ! if (imgs.length) { ! ! ! $.each(imgs,function() { ! ! ! ! $(this).zoomin({ ! ! ! ! ! bgcolor: "#999" ! ! ! ! }); ! ! ! }); ! ! } Sunday, April 17, 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" ! ! ! ! ! }); ! ! ! ! }); ! ! ! } ! ! } } Sunday, April 17, 2011
  • 41. not $.fn.everything ☛ known element type? ☛ known class? ☛ known properties? ☛ known length? ☛ that’s a widget, y’all Sunday, April 17, 2011
  • 42. now you have.. ☛ easy plugin swap/upgrade ☛ app-specific stock of widgets ☛ your junk out of $.fn.* ☛ widgets within relevant state in... Sunday, April 17, 2011
  • 43. release 4 ☛ state objects ☛ steps in arrays ☛ non-linear states: myApp.states[“thisState”] ☛ state functions ☛ init ☛ change state ☛ error Sunday, April 17, 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; ! ! ! ... Sunday, April 17, 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; Sunday, April 17, 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"); } Sunday, April 17, 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 Sunday, April 17, 2011
  • 48. now you have.. ☛ state differences encapsulated ☛ single place to switch state ☛ clear definitions ☛ add states cleanly ☛ add in routing with less risk Sunday, April 17, 2011
  • 49. release 5 ☛ pub/sub instead of event handlers ☛ reduce anonymous functions ☛ event handlers manage $(this) ☛ onreadystatechange publishes Sunday, April 17, 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"); Sunday, April 17, 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; ! ! }); ! ! ! ! }); Sunday, April 17, 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(); }); Sunday, April 17, 2011
  • 53. now you have.. ☛ application events vs. DOM events ☛ no manual callback chains ☛ easily add features that observe events ☛ control state through properties Sunday, April 17, 2011
  • 54. release 6 ☛ upgrade ☛ regression test ☛ swap out non-forward-compatible plugins ☛ regression test ☛ maybe roll back (sorries :( ) Sunday, April 17, 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; ! ! }); ! ! ! ! }); Sunday, April 17, 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; ! }); Sunday, April 17, 2011
  • 57. when you can’t upgrade ☛ bugs should be easier to find ☛ fix them ☛ keep trying ☛ ala carte features Sunday, April 17, 2011
  • 58. but once you do.. !! ☛ dependency management ☛ event delegation ☛ deferreds ☛ unit tests ☛ documentation ☛ ..framework? maybe? Sunday, April 17, 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?” Sunday, April 17, 2011
  • 60. you: ☛ “:D” ☛ can develop faster ☛ can fix easier ☛ can get hit by all the buses you want Sunday, April 17, 2011
  • 61. hey, since you did such a super job on that refactor... Sunday, April 17, 2011
  • 62. thanks! ☛ who’s got questions? ☛ shy questions: ☛ @garannm ☛ garann@gmail.com Sunday, April 17, 2011