Ruby-ying Javascript: Avoiding jQuery Spaghetti
-
Upload
forrest-chang -
Category
Software
-
view
457 -
download
0
Transcript of Ruby-ying Javascript: Avoiding jQuery Spaghetti
RUBYFYING JAVASCRIPT:RUBYFYING JAVASCRIPT:AVOIDING JQUERY SPAGHETTIAVOIDING JQUERY SPAGHETTI
FORREST CHANGFORREST [email protected]@YAHOO.COM
AS TIME GOES ONAS TIME GOES ONAdd function, add function, nest functionInsert event handlers in DOMAdd business logicGet input, dialogsAjaxEffectsUpdate DOMAssign handlers to idetc.
JQUERY SPAGHETTIJQUERY SPAGHETTIby Steve O'Brien in http://steve-obrien.com/javascript-jquery-spaghetti/
Without a strong framework or architecture you end upwith jQuery spaghetti. This is usually because you start
with a small piece of jquery shoved somewhere in the domas you add features it all grows out of proportion and
becomes a tangled mess. The most challenging thing ismaintaining state. Relying heavily on jQuery means your
application state information is stores in the dom, thisworks well for small features and isolated components
here and there, but in a complex app it quickly becomesvery difficult to manage.
JQUERY SPAGHETTI IS JSAPABO #1JQUERY SPAGHETTI IS JSAPABO #1JavaScript AntiPatterns Addressed by Opal (JSAPABO)
There are anti patterns addressed by Opal - trying to codify why Opalmakes browser code better, starting w/naming what's wrongVery much a Work In Progress, apologies
http://funkworks.blogspot.com/2015/04/javascript-antipatterns-addressed-by.html
OPAL CAN HELPOPAL CAN HELPWhat is Opal? TLDR; Ruby in the browser
How can it help?Ruby > JSCulture, ConventionsOOPand More!
See here for more info
REAL LIFE STORYREAL LIFE STORYWant slide out barNeed both Left and right slide barsWould like simple, low cruftDemo of finished product
ORIGINAL CODE ORIGINAL CODE HTTP://JSFIDDLE.NET/DMYTR/37/HTTP://JSFIDDLE.NET/DMYTR/37/$.asm = {};$.asm.panels = 1;
function sidebar(panels) { $.asm.panels = panels; if (panels === 1) { $('#sidebar').animate({ left: -180, }); } else if (panels === 2) { $('#sidebar').animate({ left: 20, }); $('#sidebar').height($(window).height() - 50); }};
$(function() { $('#toggleSidebar').click(function() { if ($.asm.panels === 1) { $('#toggleSidebar i').addClass('glyphicon-chevron-left'); $('#toggleSidebar i').removeClass('glyphicon-chevron-right'); return sidebar(2); } else { $('#toggleSidebar i').removeClass('glyphicon-chevron-left'); $('#toggleSidebar i').addClass('glyphicon-chevron-right'); return sidebar(1); } });});
HOW ABOUT THE RIGHT SIDEBAR? MY SPIKEHOW ABOUT THE RIGHT SIDEBAR? MY SPIKE$.asm2 = {};$.asm2.panels = 1;
function sidebar2(panels) { $.asm2.panels = panels; if (panels === 1) { $('#sidebar-right').animate({ right: -780, }); } else if (panels === 2) { $('#sidebar-right').animate({ right: 20, }); $('#mapCanvas').width($('#mapCanvas').parent().width()); $('#mapCanvas').height($(window).height() - 50); $('#sidebar-right').height($(window).height() - 50); }};
$(function() { $('#toggleSidebar-right').click(function() { if ($.asm2.panels === 1) { $('#toggleSidebar-right i').removeClass('glyphicon-chevron-left'); $('#toggleSidebar-right i').addClass('glyphicon-chevron-right'); return sidebar2(2); } else { $('#toggleSidebar-right i').addClass('glyphicon-chevron-left'); $('#toggleSidebar-right i').removeClass('glyphicon-chevron-right'); return sidebar2(1); } });
});
KINDA UGLY CODEKINDA UGLY CODEOriginal code for a jsfiddle - don't expect a lotTypical for jQuery examples
by itself not bad. NOT good OO codeNow that concept has been proven, time to make the code "real"
HOW TO CONVERT? HOW TO CONVERT? JUST TRANSLATE?JUST TRANSLATE?Didn't like it from the beginning
Document.ready? { Element.find('#toggleSidebar').on :click { }}
MIRED IN THE DETAILSMIRED IN THE DETAILSJSAPABO #6 Stuck in the weedsWhat's the big pictureWhat's my intent?
SEGUESEGUEReasons Opal Makes your Browser Code Better #1 (ROMYBCB) - a futureblog seriesIn Ruby, we Think in ObjectsSo Start w/objects
HOW I WANT TO USE IT?HOW I WANT TO USE IT?# Create w/intentleft_sidebar = Sidebar.new('#toggleSidebar', 'left')# elsewhere manipulateleft_sidebar.hide
CONVERTING THE JS CLICK HANDLERCONVERTING THE JS CLICK HANDLER// original code$(function() { $('#toggleSidebar').click(function() { if ($.asm.panels === 1) { $('#toggleSidebar i').addClass('glyphicon-chevron-left'); $('#toggleSidebar i').removeClass('glyphicon-chevron-right'); return sidebar(2); } else { $('#toggleSidebar i').removeClass('glyphicon-chevron-left'); $('#toggleSidebar i').addClass('glyphicon-chevron-right'); return sidebar(1); } });});
WHAT DOES IT DO?WHAT DOES IT DO?
if $.asm.panels = 1 // magic number 1 = closed statemake sidebar handle left facing leftsidebar(2) // set sidebar state to 2 (open state) - slide out
elsemake sidebar handle face rightsidebar(1) // set sidebar state to 2 (closed state) - slide in
WHAT DOES IT DO AT A HIGHER LEVELWHAT DOES IT DO AT A HIGHER LEVELstep away from the details (JSAPABO #6)If the slider is open close itelse open it
OPAL CLICK HANDLER WITH INTENTION REVEALEDOPAL CLICK HANDLER WITH INTENTION REVEALEDPut it in #initialize(), so it happens for each instance
class Sidebar def initialize(element_id, side) @state = :closed Element.find(element_id).on :click { if @state == :open close else open end } endend
WHERE TO HANG THE STATE?WHERE TO HANG THE STATE?JSAPABO #5
Where do you put state?Coz not using objects, where put state? Global?In jQuery, can hang off of jQuery
$.asm.panels // hung off of jQueryWhere would you hang data in Ruby/Opal
instance variable, because you use objects from the get goeasy
@state = :closed
IMPLEMENT OPEN AND CLOSE, ROUND 1IMPLEMENT OPEN AND CLOSE, ROUND 1def open icon = Element.find("#{element_id} i") icon.add_class('glyphicon-chevron-left') icon.remove_class('glyphicon-chevron-right') Element.find('#sidebar').animate left: 20 @state = :openend
def close icon = Element.find("#{element_id} i") icon.remove_class('glyphicon-chevron-left') icon.add_class('glyphicon-chevron-right') Element.find('#sidebar').animate left: -180 @state = :closeend
ROUND 2: REMOVE DUPLICATIONROUND 2: REMOVE DUPLICATIONdef open set_icon('glyphicon-chevron-left', 'glyphicon-chevron-right') Element.find('#sidebar').animate left: 20 @state = :openend
def set_icon(class_to_add, class_to_remove) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove)end
def close set_icon('glyphicon-chevron-right', 'glyphicon-chevron-left') Element.find('#sidebar').animate left: -180 @state = :closedend
ROUND 3: REFACTOR MORE DUPLICATIONROUND 3: REFACTOR MORE DUPLICATIONdef open set_icon('glyphicon-chevron-left', 'glyphicon-chevron-right', 20) @state = :openend
def set_icon(class_to_add, class_to_remove, new_position) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove) Element.find('#sidebar').animate left: new_positionend
def close set_icon('glyphicon-chevron-right', 'glyphicon-chevron-left', -180) @state = :closedend
YET ANOTHER PATTERNYET ANOTHER PATTERNThere's another pattern- the state change, so we move that functionality into
set_icondef open set_icon('glyphicon-chevron-left', 'glyphicon-chevron-right', 20, :open)end
def set_icon(class_to_add, class_to_remove, new_position, new_state) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove) Element.find('#sidebar').animate left: new_position @state = new_stateend
def close set_icon('glyphicon-chevron-right', 'glyphicon-chevron-left', -180, :closed)end
NEED A NEW NAMENEED A NEW NAMEset_icon() no longer describes what it's doing
def open new_state('glyphicon-chevron-left', 'glyphicon-chevron-right', 20, :open)end
def new_state(class_to_add, class_to_remove, new_position, new_state) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove) Element.find('#sidebar').animate left: new_position @state = new_stateend
def close new_state('glyphicon-chevron-right', 'glyphicon-chevron-left', -180, :closed)end
OPAL CODE THAT MATCHES THE JSFIDDLEOPAL CODE THAT MATCHES THE JSFIDDLE# Sidebar abstractionclass Sidebar attr_reader :element_id def initialize(element_id, side) @element_id = element_id @state = :closed Element.find("#{element_id} .toggles").on :click do if @state == :open close else open end end end def open new_state('glyphicon-chevron-left', 'glyphicon-chevron-right', 20, :open) end def new_state(class_to_add, class_to_remove, new_position, new_state) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove) Element.find("#{element_id}").animate left: new_position @state = new_state end def close new_state('glyphicon-chevron-right', 'glyphicon-chevron-left', -180, :closed) endendDocument.ready? { left_sidebar = Sidebar.new('#sidebar', 'left')}
CODE IS BETTERCODE IS BETTERAbout same lines of code (LOC)More intention revealingCode can be reused/repurposed
Can programmaticaly open or close sidebar easily, i.e. left_sidebar.openCouldn't do that w/original code WITHOUT refactoring
STILL NEED A RIGHT SIDEBARSTILL NEED A RIGHT SIDEBARBegin w/the end in mind
Document.ready? { left_sidebar = Sidebar.new('#sidebar', 'left') right_sidebar = Sidebar.new('#sidebar-right', 'right)}
ORIGINAL EVIL CUT AND PASTE CODEORIGINAL EVIL CUT AND PASTE CODE$.asm2 = {};$.asm2.panels = 1;
function sidebar2(panels) { $.asm2.panels = panels; if (panels === 1) { $('#sidebar-right').animate({ right: -780, }); } else if (panels === 2) { $('#sidebar-right').animate({ right: 20, }); $('#sidebar-right').height($(window).height() - 50); }};
$(function() { $('#toggleSidebar-right').click(function() { if ($.asm2.panels === 1) { $('#toggleSidebar-right i').removeClass('glyphicon-chevron-left'); $('#toggleSidebar-right i').addClass('glyphicon-chevron-right'); return sidebar2(2); } else { $('#toggleSidebar-right i').addClass('glyphicon-chevron-left'); $('#toggleSidebar-right i').removeClass('glyphicon-chevron-right'); return sidebar2(1); } });
});
NOTESNOTESBecause of JSAPABO #5, needed to store right panel state
Can't use $.asm.panels, cut and paste $.asm2What if I want a dropdown, $.asm3 ?
Not a problem if dealing with objects from the get go
PARAMETRIZEPARAMETRIZEInstead of converting the copy pasted code, we could parametrize by sideAdd below to #initializeset_params_for_side(side)
SETTING LEFTSETTING LEFTattr_reader :closed_icon_class, :opened_icon_class, :opened_x_position, :closed_x_positiondef set_params_for_side(side) if side == :left @closed_icon_class = 'glyphicon-chevron-right' @opened_icon_class = 'glyphicon-chevron-left' @opened_x_position = 20 @closed_x_position = -180 endend
def open new_state(opened_icon_class, closed_icon_class, opened_x_position, :open)end
def new_state(class_to_add, class_to_remove, new_position, new_state) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove) Element.find("#{element_id}").animate left: new_position @state = new_stateend
def close new_state(closed_icon_class, opened_icon_class, closed_x_position, :closed)end
HANDLE NON LEFT PARAMETERHANDLE NON LEFT PARAMETERattr_reader :closed_icon_class, :opened_icon_class, :opened_x_position, :closed_x_position, :x_position_sidedef set_params_for_side(side) if side == :left @closed_icon_class = 'glyphicon-chevron-right' @opened_icon_class = 'glyphicon-chevron-left' @opened_x_position = 20 @closed_x_position = -180 @x_position_side = 'left' else @closed_icon_class = 'glyphicon-chevron-left' @opened_icon_class = 'glyphicon-chevron-right' @opened_x_position = 20 @closed_x_position = -780 @x_position_side = 'right' endend
def open new_state(opened_icon_class, closed_icon_class, opened_x_position, :open)end
def new_state(class_to_add, class_to_remove, new_position, new_state) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove) Element.find("#{element_id}").animate x_position_side => new_position @state = new_stateend
RESULTING CODERESULTING CODEclass Sidebar attr_reader :element_id def initialize(element_id, side) @element_id = element_id @state = :closed set_params_for_side(side) Element.find("#{element_id} .toggles").on :click do if @state == :open close else open end end end attr_reader :closed_icon_class, :opened_icon_class, :opened_x_position, :closed_x_position, :x_position_side def set_params_for_side(side) if side == :left @closed_icon_class = 'glyphicon-chevron-right' @opened_icon_class = 'glyphicon-chevron-left' @opened_x_position = 20 @closed_x_position = -180 @x_position_side = 'left' else @closed_icon_class = 'glyphicon-chevron-left' @opened_icon_class = 'glyphicon-chevron-right' @opened_x_position = 20 @closed_x_position = -780 @x_position_side = 'right' end end
PAGE 2PAGE 2 def open new_state(opened_icon_class, closed_icon_class, opened_x_position, :open) end
def new_state(class_to_add, class_to_remove, new_position, new_state) icon = Element.find("#{element_id} i") icon.add_class(class_to_add) icon.remove_class(class_to_remove) Element.find("#{element_id}").animate x_position_side => new_position @state = new_state end
def close new_state(closed_icon_class, opened_icon_class, closed_x_position, :closed) end
end
Document.ready? { left_sidebar = Sidebar.new('#sidebar', 'left') right_sidebar = Sidebar.new('#sidebar-right', 'right')}
CONCLUSIONCONCLUSIONOpal gives you
Better CodeBetter functionalityHappinessBlogged here