Ruby-ying Javascript: Avoiding jQuery Spaghetti

40
RUBYFYING JAVASCRIPT: RUBYFYING JAVASCRIPT: AVOIDING JQUERY SPAGHETTI AVOIDING JQUERY SPAGHETTI FORREST CHANG FORREST CHANG [email protected] [email protected]

Transcript of Ruby-ying Javascript: Avoiding jQuery Spaghetti

RUBYFYING JAVASCRIPT:RUBYFYING JAVASCRIPT:AVOIDING JQUERY SPAGHETTIAVOIDING JQUERY SPAGHETTI

FORREST CHANGFORREST [email protected]@YAHOO.COM

A JQUERY STORYA JQUERY STORYHave a problemAdd a little jQueryFixed, yay!

AS TIME GOES ONAS TIME GOES ONAdd function, add function, nest functionInsert event handlers in DOMAdd business logicGet input, dialogsAjaxEffectsUpdate DOMAssign handlers to idetc.

RESULTRESULT

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); } });});

THE CODETHE CODESimpleDoes what I want for the left sidebar

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?

A BETTER APPROACHA BETTER APPROACH

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

BETTERBETTERIntent revealedObjects from the get go

We'll see why this matters later

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 VALUES FOR :LEFTSETTING VALUES FOR :LEFTSet values and use them

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

DONE FOR NOWDONE FOR NOWDoes what I needExceeds original implementation

Reusable

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