12
\$\begingroup\$

I have all these functions that work together to create functionality on a page. Is the structure of these functions OK? Can I do anything to speed this up or make my code better? I'm not exactly sure what namespaces are, but might they help me here?

This code is for a responsive/adaptive calendar website. On the homepage, there is a calendar with events listed by day. These functions filter the events using the ISOTOPE plugin, open and close dropdowns, etc.

If anyone is really interested. I can send you a link + login to the PW protected site.

var $eventwrap = $j('.tw-lists'),
        $daywrap = $j('.tw-day'),
        $dayfilter = $j('#tw-filter-days li a');
        $daywraphide = $j('tw-day.hide'),
        $catwrap = $j('.tw-list-filter'),
        $viewctrls = $j('.tw-view a'),
        $selectdays = $j('.select-days option'),
        $selectbarrio = $j('.select-barrio option'),
        $selectcats = $j('.select-cats option');

filters = {};

var opday = [],
    opcategory = [],
    opbarrio = [];

function optionArray(select,options) {
    $j(select).find('option').each(function() {
        options.push($j(this).attr('data-filter'));
    });

    return options;
}

// CHECK IF A GIVEN DAY HAS EVENTS
function filterToggle(element,x,y) {
    element.each(function(){
        var $me = $j(this),
            isli = $me.is('li');

        if(isli) {
            var myvalue = $me.find('a').attr('data-filter');
        } else {
            var myparent = $me.parent().attr('data-filter-group'),
                myvalue = $me.attr('data-filter');
        }

        if(!x) {x = ''}
        if(!y) {y = ''}

        var eventcount = $j('.tw-list'+ myvalue + x + y).length;

        if(eventcount == 0) {
            if(isli) {
                $me.addClass('empty tdn');
            } else {
                $me.attr('disabled','disabled');
            }
        } else {
            if(isli) {
                $me.removeClass('empty tdn');
            } else {
                $me.removeAttr('disabled');
            }
        }
    });
}

function filterAll() {
    var currentbarrio = $j('#tw-filter-barrio .current a').attr('data-filter'),
        currentcats = $j('#tw-filter-cats .current a').attr('data-filter'),
        currentday = $j('#tw-filter-days .current a').attr('data-filter'),
        selectbarrio = $j('.select-barrio :selected').attr('data-filter'),
        selectcats = $j('.select-cats :selected').attr('data-filter'),
        selectday = $j('.select-days :selected').attr('data-filter');

    filterToggle($j('#tw-filter-days li'),currentbarrio,currentcats);
    filterToggle($j('#tw-filter-barrio li'),currentcats,currentday);
    filterToggle($j('#tw-filter-cats li'),currentday,currentbarrio);

    filterToggle($selectdays,selectbarrio,selectcats);
    filterToggle($selectbarrio,selectcats,selectday);
    filterToggle($selectcats,selectday,selectbarrio);

    $j('#tw-filter-days .current a').click();

    return false;   
}

// SELECT FILTER ON CLICK
function filterDrop(dropdown) {
    dropdown.find('.tw-filter-drop-options a').on('click',function(){
        var $me = $j(this);

        if($me.parent().hasClass('empty')){ 
            //No events for that option
            return false; 
        } else {
            var $toggle = dropdown.find('.tw-filter-drop-toggle'),
                toggleText = $toggle.find('span').html(),
                myDropText = $me.html();

            dropdown.removeClass('open');

            if (myDropText == toggleText) {
                return false;
            } else {
                var $parent = $me.parents('.tw-filter-dropdown'),
                    parentId = $parent.attr('id');

                dropdown.find('.tw-filter-drop-options li').removeClass('current');

                $me.parent().addClass('current');

                dropdown.find('.tw-filter-drop-label').html(myDropText);

                filterAll();
            }
        }
    });
}

// FILTER CLICK
function filterClick($element) {
    $element.on('click',function(){
        var $me = $j(this),
            $parent = $me.parent(),
            myvalue = $me.attr('data-filter'),
            $mytwin = $j(".tw-filter-select").find("[data-filter='"+myvalue+"']");

        if($parent.hasClass('empty') || $parent.hasClass('current')) { 
            return false; 
        }   

        if($me.parents('#tw-filter-days').length !== 0) {
            $j('#tw-filter-days li').removeClass('current');
            $parent.addClass('current');
        }
        var $optionSet = $me.parents('ul'),
            group = $optionSet.attr('data-filter-group');

        filters[ group ] = myvalue;

        var isoFilters = [];
        for ( var prop in filters ) {
            isoFilters.push( filters[ prop ] )
        }
        var selector = isoFilters.join(''),
            currentday = $j('#tw-filter-days .current a').attr('data-filter'),
            $dayheader = $j('.tw-day-header');

        $dayheader.hide();
        $dayheader.filter(currentday).show();

        $j('.tw-list-filter').isotope({ filter: selector, animationEngine : 'css' });

        $mytwin.attr('selected','selected');

        filterAll();

        return false;
    });
}


// SWITCH VIEWS
function switchViews($element, $events) {
    $element.on('click',function(){
        var $me = $j(this),
            $parent = $me.parent();
            myclass = $me.attr('class').split('-');
            myview = myclass[0];

        if($me.hasClass('selected')) {
            return false;
        } 

        var selector = $j('#tw-filter-days .current a').attr('data-filter');

        if(myview == 'list') {
            $events.removeClass('grid-wrap');
            $j('.tw-list-filter').isotope({ animationEngine : 'css' });
        } else {
            $events.addClass('grid-wrap');
            $j('.tw-list-filter').isotope({ animationEngine : 'css' });
        }

        $parent.removeClass('list grid').addClass(myview);
        $element.removeClass('selected');
        $me.addClass('selected');
    });
}


// SETUP SCROLLABLE
function scrollableSetup($textslide, $imgslide, $homeslide) {
    $textslide.scrollable({easing: 'swing', keyboard: true, speed: 500}).navigator();
    $imgslide.scrollable({keyboard: false, speed: 950}).navigator();

    if($textslide.length) {
        var textscroll = $textslide.data("scrollable");

        textscroll.onSeek(function() {
            if(!$imgslide.is(':visible')) {
                var myindex = this.getIndex(),
                    $myslide = $textslide.find('.item:eq('+myindex+')'),
                    myheight = $myslide.find('.text-inner').height(),
                    homeslideHeight = $homeslide.css('height'),
                    homeslideHeight = homeslideHeight.replace('px','')

                if(homeslideHeight != myheight){
                    $homeslide.removeAttr('style').css('height',myheight);
                }
            }
            return false;
        });
    }

    $j(document).keydown(function(e){
        if (e.keyCode == 37) { 
            $textslide.scrollable().prev();
            $imgslide.scrollable().prev();
            return false;
        }

        if (e.keyCode == 39) { 
            $textslide.scrollable().next();
            $imgslide.scrollable().next();
            return false;
        }       
    });
}


// OPEN AND CLOSE DROPDOWN
function openDropDowns($droptoggle, $dropdown) {
    $droptoggle.on('click',function() {
        $j('.tw-filter-dropdown.open').removeClass('open');
        var $me = $j(this)
        var toggletext = $me.find('span').html();
        $me.parent().addClass('open');
    });

    //Hide drop downs on click off
    $j(document).bind('click', function (e) {
        $j('.tw-filter-dropdown').removeClass('open');
    });

    $dropdown.bind('click', function(e) {
        e.stopPropagation();
    });
}

// FOCUS FUNCTION FOR MOBILE SELECTS
function mobileFocus($select) {
    $select.focus(function(){
        var $me = $j(this),
            previousval = $me.value,
            $previous = $me.find(':selected');
    }).change(function(){
        var $me = $j(this),
            myvalue = $me.find(':selected').attr('data-filter'),
            $myoption = $me.find("[data-filter='"+myvalue+"']");
            mygroup = $me.attr('data-filter-group'),
            $mytwin = $j('.tw-filter a[data-filter="'+myvalue+'"]');

        if($myoption.attr('disabled') == 'disabled') {
            alert('No hay eventos en este barrio y día');
            return false;
        }

        filters[ mygroup ] = myvalue;

        var isoFilters = [];
        for ( var prop in filters ) {
            isoFilters.push( filters[ prop ] )
        }

        var selector = isoFilters.join(''),
            currentday = $j('.select-days :selected').attr('data-filter'),
            $dayheader = $j('.tw-day-header .white-bub, .tw-day-header');

        $dayheader.hide();
        $dayheader.filter(currentday).show();

        $j('.tw-list-filter').isotope({ filter: selector, animationEngine : 'css' });

        $mytwin.click();

        filterAll();
    });
}

$j(window).smartresize(function(){
    $j('.tw-list-filter').isotope({animationEngine : 'css'});
    $j('#tw-filter-barrio .current a').click();
    if($j(this).height() > 500) {
        $j('.navi .active').click();
        $j('.home-slide').removeAttr('style')
    }
});

Then in my HTML:

<script>
$j(document).ready(function($){

    var $textslide = $('.text-slide'),
        $imgslide = $('.img-slide'),
        $homeslide = $('.home-slide'),
        $filters = $('.tw-filter ul a'),
        $views = $('.tw-view a'),
        $droptoggle = $('.tw-filter-drop-toggle'),
        $dropdown = $('.tw-filter-dropdown'),
        $select = $('.tw-filter-select'),
        $filterdays = $('#tw-filter-days'),
        $filtercats = $('#tw-filter-cats'),
        $filterbarrio = $('#tw-filter-barrio'),
        $events = $('.tw-lists');

    scrollableSetup($textslide, $imgslide, $homeslide);
    filterClick($filters);
    switchViews($views, $events);
    mobileFocus($select)
    filterDrop($filterbarrio);
    filterDrop($filtercats);
    openDropDowns($droptoggle, $dropdown);

    $('.list-view').click();
    $dayfilter.not('.empty > a').first().click();
}); 

\$\endgroup\$
  • \$\begingroup\$ Look into object oriented programming, DRY, modules, separation of concerns. As it is now, anyone trying to understand this code needs to be intimately familiar with the whole thing in its entirety instead of nice digestible chunks. \$\endgroup\$ – Esailija Apr 26 '12 at 18:07
  • 1
    \$\begingroup\$ I agree with Esailija. Please, explain the functionality at least. \$\endgroup\$ – Michael Apr 26 '12 at 18:11
  • \$\begingroup\$ I don't have the rep to re-tag, but functional-programming seems to not apply here. \$\endgroup\$ – steveax Apr 29 '12 at 1:55
24
\$\begingroup\$

Here are the first steps I would take (not necessarily in order):

  1. Surround everything with an anonymous self calling function

    (function () {
        ...
    }());
    

    This both helps ensure you don't accidentally add/overwrite global variables and aids in the future minification of your script.

  2. Define all variables at the start of their respective functions

    if(isli) {
        var myvalue = $me.find('a').attr('data-filter');
    } else {
        var myparent = $me.parent().attr('data-filter-group'),
            myvalue = $me.attr('data-filter');
    }
    

    This kind of code is error-prone and makes future refactorings harder (and you will change it later).

  3. Explicitly export any globals you wish to define at the very end of your script

    $.extend(window, {
        'scrollableSetup': scrollableSetup,
        'filterClick': filterClick,
        'switchViews': switchViews,
        'mobileFocus': mobileFocus,
        'filterDrop': filterDrop,
        'openDropDowns': openDropDowns
    });
    

    This also helps the minification process (internally the function names can be munged). As a general rule it is better to be explicit where you can be. The quotes are important to certain minifiers.

  4. Explicitly declare/pass in any global objects to the anonymous function

    (function (window, document, $, ...) {
        ...
    }(window, document, jQuery, ...));
    

    Again this helps minification (now globals can be munged). Also, still better to be explicit.

  5. Run JSLint on your code

    http://www.jslint.com/

    The only errors I tolerate in JSLint are:

    • messy whitespace (sometimes)
    • ++ usage (only in counter part of for loops)
    • "'variable' was used before it was defined." (only in the parameter list to the anonymous method)
  6. Remove unused variables

    $select.focus(function(){
        var $me = $j(this),
            previousval = $me.value,
            $previous = $me.find(':selected');
    }).change(function(){
    

    Unused variables are dead weight. Every line of code you don't need to write is a line of code that the person looking at this 6 months from now doesn't need to look at and figure out what you were thinking. Unused variables come in several forms:

    1. variables in the beginning of your functions (see step 2) that don't get used within the function
    2. variables that get exported to an outer scope but do not serve a purpose
    3. functions that do not get used
    4. event handlers that don't do anything
  7. Code that does the same thing should look the same

    JQuery has a half dozen ways to bind events (.bind, .on, .click, .live, .delegate, .one). The only one you need is .on. Replace:

    .bind('click', function () {
    

    with

    .on('click', function () {
    

    and so on.

    Similarly within a method, avoid switching between if statements and switch statements and back:

    if (x === y) {
        return 'x';
    }
    switch (x) {
    case z:
        ...
    }
    if (x ...
    

    This is better written with pure if statements.

    Continuing this thought process, don't switch between method short circuiting and "The One Return" (with one exception: arg checks):

    if (x) { return a; }
    if (y) { return b; }
    if (z) { r = ... }
    if (!r && t) { r = ... }
    ...
    return r || ...;
    }
    

    In general, logic that changes in style is much harder to read than logic that remains stylistically constant.

  8. Pull constants out

    If a variable doesn't change within a function or between runs of a function, then it is not a variable with respect to that function. Thus it doesn't belong there (except at the root level).

  9. Use the right method for the job

    Instead of                                   Use 
    .attr('data-x')                              .data('x')
    if (x) { $a.show() } else { $a.hide() }      $a.toggle(x) 
    x ? $a.addClass('a') : $a.removeClass('a')   $a.toggleClass('a', x)
    
\$\endgroup\$
  • \$\begingroup\$ Great! Thanks for the pointers. The one thing I'm having trouble with is wrapping everything with the anonymous function and exporting the variables-- after doing so the script doesn't seem to load. Is there anything I need to change in my document.ready function in my footer where I call those functions? \$\endgroup\$ – j-man86 Apr 26 '12 at 21:59
  • \$\begingroup\$ I'd try using firebug (or equivalent for other browsers, but fb is the best imo). I'd need the error in the console to help farther. \$\endgroup\$ – Bill Barry Apr 26 '12 at 22:56
  • \$\begingroup\$ that's the frustrating part... it doesn't throw an error \$\endgroup\$ – j-man86 Apr 26 '12 at 23:22
  • \$\begingroup\$ ok try this: start with the empty anonymous function over at JSLint, and add code to it one function at a time until you get everything over there. \$\endgroup\$ – Bill Barry Apr 27 '12 at 0:22
  • \$\begingroup\$ While I think it would be best if you went through that process yourself, here is a link with all of this done: jsfiddle.net/qcbng (there is still a lot more that can be done to make this code better, but your code should look in jslint just like that and I may have fixed some variables you were expecting to be broken due to misplaced semicolons) \$\endgroup\$ – Bill Barry Apr 27 '12 at 0:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.