3
\$\begingroup\$

I started working on a jQuery port for this Form Dependency Manager script. It works fine, and I added a few extra options too.

The problem is speed. If you create dependencies between 20+ form elements on a page you get the PC to run really slow. Firefox is throwing that "script is too slow" message. This is probably because of the heavy FOR loops.

Do you guys have any suggestions on how could this be improved, or maybe new ideas on how to implement this kind of functionality?

(function($){
$.fn.setupDependencies = function(options){

  var defaults = {
        attribute              : 'rules',           // the field attribute which contains the rules (use 'rel' for w3c valid code)
        disable_only           : true,              // if true it will disable fields + label, otherwise it will also hide them
        clear_inactive         : false,             // clears input values from hidden/disabled fields
        identify_by            : 'name',            // attribute used to identify dependencies (ie. DEPENDS ON [identify_by] BEING ...)

        condition_separator    : ' AND ',           // rules...
        possibility_separator  : ' OR ',
        name_value_separator   : ' BEING ',
        depends                : 'DEPENDS ON ',
        conflicts              : 'CONFLICTS WITH ',
        empty                  : 'EMPTY'
      },

      settings = $.extend({}, defaults, options),
      matches = this,

      valueMatches = function(e, v){
        return (e.val() == v || (e.is(':radio') && e.filter(':checked').val() == v));
      },

      // show or enable
      show = function(e){
        $('label[for="' + e.attr('id') + '"]').removeClass('disabled');
        e.removeAttr('disabled');
        if(!settings.disable_only){
          e.show();
          $('label[for="' + e.attr('id') + '"]').show();
        }
        return true;
      },

      // hide or disable
      hide = function(e){
        $('label[for="' + e.attr('id') + '"]').addClass('disabled');
        e.attr('disabled', 'disabled');
        if(!settings.disable_only){
          e.hide();
          $('label[for="' + e.attr('id') + '"]').hide();
        }
        if(settings.clear_inactive == true && !e.is(':submit')) // ignore submit buttons
          if(e.is(':checkbox,:radio')) e.removeAttr('checked'); else e.val('');
        return true;
      };


  return this.bind('change input', function(){  // note: input event not working in IE <= 8, obviously
    var j, k, f, n, isHidden, dep;

    matches.each(function(){

      isHidden = false;
      dep = $(this).attr(settings.attribute);

      if(typeof dep !== 'undefined' && dep !== false)
        for(j = 0, f = dep.split(settings.condition_separator); j < f.length; ++j)
          if(f[j].indexOf(settings.depends) === 0){
            for(k = 0, g = f[j].substr(settings.depends.length).split(settings.possibility_separator); k < g.length; ++k)
              if(g[k].indexOf(settings.name_value_separator) === -1){
                if(matches.filter('[' + settings.identify_by + '="' + g[k] + '"]').is(':checked')) break; else if(k + 1 == g.length) isHidden = hide($(this));

              }else{
                n = g[k].split(settings.name_value_separator);
                if(valueMatches(matches.filter('[' + settings.identify_by+'="' + n[0] + '"]'), n[1])) break; else if(k + 1 == g.length) isHidden = hide($(this));
              }
          }else if(f[j].indexOf(settings.conflicts) === 0){
            if(f[j].indexOf(settings.name_value_separator) === -1){
              if(matches.filter('[' + settings.identify_by + '="' + f[j].substr(settings.conflicts.length) + '"]').is(':checked')){
                isHidden = hide($(this));
                break;
              }
            }else{
              n = f[j].substr(settings.conflicts.length).split(settings.name_value_separator);
              if(valueMatches(matches.filter('[' + settings.identify_by + '="' + n[0] + '"]'), n[1])){
                isHidden = hide($(this));
                break;
              }
            }
          };

      if(!isHidden) show($(this));

    });

    return true;
  }).change();       

};

})(jQuery);

Note that my version uses a non-standard attribute for the rules by default (rules) instead of class like in the original script...

\$\endgroup\$
2
  • \$\begingroup\$ Personally I don't like non-standard attributes. I'd prefer using class. At least you should use a HTML5 data- attribute. \$\endgroup\$
    – RoToRa
    Jun 23, 2011 at 10:35
  • \$\begingroup\$ well if anyone else wants to use this, they can change it. I'm using it in a site's administration interface, where the w3c validator is not welcome :) \$\endgroup\$ Jun 24, 2011 at 22:35

2 Answers 2

1
\$\begingroup\$

All the points below I started writing while looking over the code for the performance issue, but aren't related to it. To be honest after the first read over I can't see how one would do it differently to be faster. One would probably have to do some proper profiling.

My best guess however would be, that the performance is lost while re-parsing the rules every time and the only idea I have concerning that, is somehow to parse them once and maybe generate functions out of them, which you only need to call later on.

Thinking of that, another solution would be to use a completely different approach and avoid parsing altogether. Instead of assigning a the rules via string in a property, you could create jQuery methods. Something like: $("#some_form_element").dependsOn("[name='mycheckbox']");

Now the performance unrelated points:

  • The main code block is very difficult to read due to the deeply nested ifs and fors. It would help to split it down into more sub routines, especially as a believe to see some code duplication.
  • Additionally the one letter variables makes reading the code a bit more difficult, too.
  • Setting custom properties on DOM objects (isHidden) is usually not recommended - that's why you had problems as it was called hidden. Why are you using a property here at all? It doesn't seem that you are using the value outside the loop anyway, so a simple variable would suffice.
  • if(typeof dep !== "undefined" && dep !== false) could be simplified to if (dep).
  • I would move the default object and all the "private" methods out of the main function
  • Real nitpick: You have unnecessary semicolons after block statements
\$\endgroup\$
2
  • \$\begingroup\$ I've taken a function, jQuery approach like you've suggested. I posted it at codereview.stackexchange.com/questions/3104/… . Is this similiar to what you are suggesting? \$\endgroup\$ Jun 24, 2011 at 22:10
  • \$\begingroup\$ About your solution - I think natedavisolds here did exactly that. Anyway in my my case I'm trying to cut down the javascript as much as possible on a CMS page with lots of such form elements that depend on each other. This is why I'd like to avoid explicitly calling jQuery methods for each input. Regarding the speed I mentioned, I think I have to looks some place else because I tried testing this in a HTML page with 100+ elements and it seems to run pretty fast. Thanks for the input! \$\endgroup\$ Jun 24, 2011 at 22:29
1
\$\begingroup\$

I just posted a similar code review request: Plugin that conditionally displays elements based on form values .

I think that we are tackling the same issue here. I wish that we could merge the discussion to find the best solution, but at least we can use this great forum to bounce ideas off each other. :)

It looks like you've taken a String approach to the problem. While I've taken a function based approach.

I don't have any speed benchmarks yet, but I will be performing those on Monday and can share them then.

Do you have an example of how to extend this to more rules? Is it easy?

\$\endgroup\$
1
  • \$\begingroup\$ I'm not sure what other rules could be added, because these ones cover pretty much all situations. I'm also not so sure about the performance hit anymore, because outside my CMS it doesn't seem to be noticeable; will have to do some more tests about this \$\endgroup\$ Jun 24, 2011 at 22:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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