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>