I've just released a jQuery plugin that conditionally displays elements based on form values. I would appreciate any suggestions on how to improve both the code and its usefulness.
- Would this be useful to you?
- What other functionality should I include?
- Any refactoring or structure changes that I should make?
- I am worried about exposing 4 different functions. Is this too many?
The Goal
The plugin helps in situations where an element should hide or show based on values in other elements. In particular, it excels when there are multiple conditions that need to be satisfied in order to hide or show.
Additional concepts to keep in mind
- Use as jQuery plugin
- Easy to chain rules together
- Understandable by reading a line of code
- Can build/add custom conditions
Example of business rules to solve
Let's say we have the following rules for elements:
Display a set of checkboxes if
- Zip is between 19000 and 20000
- Income is lower than 15000
Display another set of checkboxes if
- City is 'Philadelphia'
- Income is lower than 40000
Display city select box if
- Zip is between 19100 and 19400
Ideal look
$('.elements_to_display')
.reactIf( '#some_form_element', SatisfiesFirstCondition)
.reactIf( '#some_other_form_element', SatisfiesAnotherCondition)
.reactIf( '#some_other_form_element', SatisfiesAnotherCondition);
Page JS
var IS = $.extend({}, $.fn.reactor.helpers);
$('.cities')
.reactIf('#zip', IS.Between(19100, 19400))
.reactIf('#zip', IS.NotBlank);
$('.philly_middle_to_low_income')
.reactIf('#income_2011', IS.LessThan(40000))
.reactIf('#cities_select', IS.EqualTo('philadelphia'));
$('.low_income_select_zips')
.reactIf('#income_2011', IS.LessThan(15000))
.reactIf('#zip', IS.BetweenSameLength(19000, 20000))
.reactIf('#zip', IS.NotBlank);
$('.reactor').trigger('change.reactor');
Plugin react.js
(function($){
$.fn.reactTo = function(selector) {
var $elements = $(selector),
$reactor_element = $(this),
_proxy_event = function() {
$reactor_element.trigger('change.reactor');
};
$elements.filter('select').bind('change.reactor', _proxy_event);
$elements.filter('input').bind('keyup.reactor', _proxy_event);
return this;
};
$.fn.reactIf = function(sel, exp_func) {
var $sel = $(sel);
var _func = function() {
return exp_func.apply($sel);
};
this.each(function() {
if (!$(this).hasClass('reactor')) { $(this).reactor(); }
var conditions_arry = $(this).data('conditions.reactor');
if (!$.isArray(conditions_arry)) { conditions_arry = []};
conditions_arry.push(_func);
$(this).data('conditions.reactor', conditions_arry);
});
$(this).reactTo(sel);
return this;
};
$.fn.react = function() {
this.each(function() {
$(this).trigger('change.reactor')
});
return this;
};
$.fn.reactor = function(options) {
var settings = $.extend({}, $.fn.reactor.defaults, options);
this.each(function() {
// var opts = $.meta ? $.extend({}, settings, $this.data()) : settings;
var $element = $(this);
if (!$element.hasClass('reactor')) { $element.data('conditions.reactor', []).addClass('reactor'); }
var is_reactionary = function() {
var conditionalArray = $(this).data('conditions.reactor');
var r = true;
$.each(conditionalArray, function() {
r = (r && this.call());
});
return r;
}
var reaction = function(evt) {
evt.stopPropagation();
if (is_reactionary.apply(this)) {
settings.compliant.apply($element);
} else {
settings.uncompliant.apply($element);
}
}
$element.bind('change.reactor', reaction);
});
return this;
};
$.fn.reactor.defaults = {
compliant: function() {
$(this).show();
},
uncompliant: function() {
$(this).hide();
}
};
$.fn.reactor.helpers = {
NotBlank: function() {
return( $(this).val().toString() != "" )
},
Blank: function() {
return( $(this).val().toString() == "" )
},
EqualTo: function(matchStr) {
var _func = function() {
var v = $(this).val();
if (v) { return( v.toString() == matchStr ); }
else { return false; }
}
return _func;
},
LessThan: function(number) {
var _func = function() {
var v = $(this).val();
return(!(v && parseInt(v) > number));
}
return _func;
},
MoreThan: function(number) {
var _func = function() {
var v = $(this).val();
return(!(v && parseInt(v) < number));
}
return _func;
},
Between: function(min, max) {
var _func = function() {
var v = $(this).val();
return(!(v && (parseInt(v) > max || parseInt(v) < min)));
}
return _func;
},
BetweenSameLength: function(min, max) {
var len = min.toString().length;
var _func = function() {
var v = $(this).val();
return(!(v && v.length == len && (parseInt(v) > max || parseInt(v) < min)));
}
return _func;
}
};
})(jQuery);
HTML react.html
<form id="portfolio_form">
<fieldset>
<label>Zip</label>
<input id="zip" type="text" value="" /><br />
<label>2011 Income</label>
<input id="income_2011" name="income[2011]" />
</fieldset>
<p>Display cities only when zip is between 19100 and 19400</p>
<fieldset class="cities">
<label>Cities</label>
<select id="cities_select">
<option value=""></option>
<option value="philadelphia">Philadelphia</option>
<option value="media">Media</option>
<option value="doylestown">Doylestown</option>
</select>
</fieldset>
<p>Display checkboxes only for Philadelphia and income less than 40000</p>
<fieldset class="philly_middle_to_low_income">
<input type="checkbox" /> Check One<br />
<input type="checkbox" /> Check Two<br />
<input type="checkbox" /> Check Three<br />
<input type="checkbox" /> Check Four<br />
</fieldset>
<p>Display checkboxes when zip is between 19000 and 20000 and income is lower than 25000</p>
<fieldset class="low_income_select_zips">
<input type="checkbox" /> Check One<br />
<input type="checkbox" /> Check Two<br />
<input type="checkbox" /> Check Three<br />
<input type="checkbox" /> Check Four<br />
</fieldset>
</form>
-
\$\begingroup\$ looks nice! i would prefer to call it like this: $('.elements_to_display').reactIf( $("div") , SatisfiesFirstCondition) if you can provide a jquery element directly, you can be more flexible on the way to specify the element that you are dependent on. $('.elements_to_display').reactIf( $("div").parents(0) , SatisfiesFirstCondition) \$\endgroup\$meo– meo2011年06月28日 20:32:16 +00:00Commented Jun 28, 2011 at 20:32
-
\$\begingroup\$ @meo - yup that first argument can be a selector or a jquery object, so either will work. \$\endgroup\$natedavisolds– natedavisolds2011年06月28日 21:15:27 +00:00Commented Jun 28, 2011 at 21:15
1 Answer 1
The syntax is nice, but the necessity for the user to declare
IS
themselves is not ideal. You should look for a different solution. One possibility could be to supply the name of the conditional function as a string and its arguments as additional arguments ofreactIf
. That way the conditional functions would no longer need to be of higher-order (not that that is a bad thing). Example:$('.cities').reactIf('#zip', "Between", 19100, 19400); // ... $.fn.reactIf = function(sel, exp_func) { var $sel = $(sel); var args = arguments.slice(2); var _func = function() { return $.fn.reactor.helpers[exp_func].apply($sel, args); }; // ... } $.fn.reactor.helpers = { // ... Between: function(min, max) { var v = $(this).val(); return(!(v && (parseInt(v) > max || parseInt(v) < min))); }, // ... }
These is one more problem with the conditional functions: You supply a jQuery object as the
this
argument toapply
, so it's not needed to wrapthis
in another jQuery call inside the conditional functions. You should either change to apply call to:return exp_func.apply($sel[0]);
or in the conditional functions:
var v = this.val();
I'm not sure if it's a good idea to mark elements with a class. This can go wrong, for example, if a second JavaScript removes all classes from an element. Instead of
if (!$(this).hasClass('reactor')) { $(this).reactor(); } var conditions_arry = $(this).data('conditions.reactor'); if (!$.isArray(conditions_arry)) { conditions_arry = []};
I would use
var conditions_arry = $(this).data('conditions.reactor'); if (!$.isArray(conditions_arry)) { $(this).reactor(); conditions_arry = []; };
and similarly in
reactor()
.You should consider short-circuiting the
$.each()
loop calling the conditional functions (which also makes the&&
unnecessary):$.each(conditionalArray, function() { r = this.call(); return r; // Stops the `each` loop if r is `false` });
-
\$\begingroup\$ Thanks for your feedback. Very nice insights. I've incorporated all these into the plugin except for the first. I will probably check the type of the second argument. If it is string then do as you've suggested. If a function then I will just add it like I've done. The "IS" variable isn't required at all. I just put it in because it looks better. All of your suggestions made it in, can I give you credit in the plugin code? \$\endgroup\$natedavisolds– natedavisolds2011年06月28日 16:50:03 +00:00Commented Jun 28, 2011 at 16:50
-
\$\begingroup\$ You are welcome. I just saw one more thing: You can simplify
this.filter(...).length > 0
tothis.is(...)
. \$\endgroup\$RoToRa– RoToRa2011年06月29日 12:27:18 +00:00Commented Jun 29, 2011 at 12:27