Reputation: 28995
I am dynamically generating a form with different fields ( textbox, textarea, select, radio / checkbox ).
I want to show / hide some fields, depending on what was selected in some other fields.
A simple case can be:
$('select').change(function () {
if($(this).val() === '1') {
$('p').show();
}
else {
$('p').hide();
}
});
p {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<select>
<option value="0">Bike</option>
<option value="1">Car</option>
</select>
<p>Left / right hand drive ?
Left: <input type="radio" value="left" name="dir" />
Right: <input type="radio" value="right" name="dir" />
</p>
I am generating the fields dynamically and there are multiple situations, where I need to show / hide different fields, based on value of some other fields. So, I do not want to write the same code again & again. To adhere with the DRY principle, I want to create some sort of generalized function ( a plugin may be ), to handle all these scenarios.
My basic (working) idea is:
$('[data-dependent]').each(function () {
var $ele = $(this);
var dependsOn = $ele.data('dependent');
$.each(dependsOn, function (target, value) {
$(target).on('change', function () {
if ($(this).val() === value) {
$ele.show();
} else {
$ele.hide();
}
});
});
});
[data-dependent] {
display: none;
}
<select id="vehicle">
<option value="0">Bike</option>
<option value="1">Car</option>
</select>
<p data-dependent='{"#vehicle": "1" }'>Left / right hand drive ? Left:
<input type="radio" value="left" name="dir" />Right:
<input type="radio" value="right" name="dir" />
</p>
This code works, but it attaches a lot of event handlers. Also, it does not handle the situation where a field can be shown / hidden, based on values of more than 1 field. I couldn't figure out a nice way to handle this.
I am looking for a clean & flexible solution. If there's already a plugin for this, that will work too.
Upvotes: 3
Views: 16329
Reputation: 75
$('select').change(function () {
if($(this).val() === '1') {
$('p').show();
}
else {
$('p').hide();
}
});
p {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<select>
<option value="0">Bike</option>
<option value="1">Car</option>
</select>
<p>Left / right hand drive ?
Left: <input type="radio" value="left" name="dir" />
Right: <input type="radio" value="right" name="dir" />
</p>
Upvotes: 0
Reputation: 1153
Have you heard about The Observer Pattern ? If not, link here or just serach it in a js design pattern book. The thing about the observer pattern is that it is very useful and modular (and efficient) in these cases, where an object listens to changes on another object.
As i see in your question, it is not neccesarily needed the observer pattern, becouse an object is shown only when just one object is clicked or selected. But if you want for example a to be shown only when two radio buttons are selected, well, then you really need it.
Becouse you said you want the answer in a clean/flexible solution, a design pattern you need ;) Here is my try: (skip the code and see the codepen i made for you)
the HTML:
<!--
1.what is data-publish?
if data-publish gets clicked or changed(depends wheter is input or select option), it notifies every element in DOM with data-observe with the same value as our clicked data-publish.
2.what is data-observe?
data-observe="4 5" observes the DOM elements with data-publish="4" & data-publish="5" for a change. data-observe="4 5" gets display:block only when data-publish="4" & data-publish="5" are selected(it can be the case where data-publish="4 5", and it works the same)
3. what is data-name?
data-name is a unique name of a DOM element which as data-observe attribute. this is set at page load, js insert in DOM a random string to data-name
-->
<p>combinations:(note, at page load, nothing is selected)</p>
<p>1 & 1</p>
<p>2 & 2</p>
<p>1 & 1 -> & 7 </p>
<p>1 & 1 & 10 & 10 -> & 7</p>
<select>
<option selected></option>
<option data-publish="1">pub 1</option>
<option data-publish="2">pub 2</option>
<option data-publish="3">pub 3</option>
</select>
<select>
<option selected></option>
<option data-publish="4">pub 4</option>
<option data-publish="1">pub 1</option>
<option data-publish="5">pub 5</option>
</select>
<select>
<option selected></option>
<option data-publish="10">pub 10</option>
<option data-publish="11">pub 11</option>
<option data-publish="12">pub 12</option>
</select>
<select>
<option selected></option>
<option data-publish="10 2">pub 10 & 2</option>
<option data-publish="13">pub 13</option>
<option data-publish="14">pub 14</option>
</select>
<p data-observe="1">(triggered by two 1)
pub 7<input type="checkbox" data-publish="7">
pub 8<input type="checkbox" data-publish="8">
pub 9<input type="checkbox" data-publish="9">
</p>
<p data-observe="2">triggered by two 2</p>
<p data-observe="1 7">i am triggered by two 1, one 7</p>
<p data-observe="1 7 10">i am triggered by two 1, one 7, two 10</p>
CSS :
[data-observe] {
display: none;
}
Javascript :
/*
* @author: Ali Cerrahoglu
* @thing: awesome thing
* @use: no defaults...so version 1.0.0
it's a simple pub-sub which shows divs based on select options and radio/checkbox inputs
*/
// revealing module pattern
var ObserverPlugin = (function(){
// here will be stored every DOM object which has
// data-observe attr and data-name attr (data-name will be served
// as a key , which will store another object with a reference to the DOM object
// how many object does it observe)
var observers = {};
var publishers = [];
// observer pattern & revealing module pattern
var observer = (function(){
var topics = {};
var publish = function(topic, reference) {
// if there is no topic on the publish call, well get out !
if (!topics[topic]) {
return false;
}
// self invoked funciton, which calls the function passed when
// the topic was subscribed (if more then one function was published on the same topic
// then call each one of them)
(function(){
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func(topic, reference);
}
})();
};
var subscribe = function(topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}
topics[topic].push({
func: func
});
};
return {
subscribe: subscribe,
publish: publish,
topics: topics
}
})();
// creates random string, used to make data-name random for observers
var _makeRandomString = function() {
var text = "";
var possible = "abcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 5; i++ ) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
// verifies if eleme existis in array, if not, returns false
var _isInside = function( elem, array ) {
return array.indexOf(elem) > -1;
}
// topic is the topic
// reference is a reference to the DOM object clicked
var _observerFunction = function(topic, reference) {
var number = reference.attr('data-value');
var topics = topic.toString().split(' ');
var length = topics.length;
for( var key in observers ) {
for( var i = 0; i < length; i +=1 ) {
if( _isInside( topics[i], observers[key].topicsObserved ) ) {
// it exists
observers[key].sum += Number(number);
// 'number' is a string, so we have to convert it back to number
}
}
if( observers[key].sum == 0 ) {
// it is 0, so show that goddam DOM obj ! :))
// again, put here 'var' for clarity
// does not affect the code
var display = 'block';
}
else {
// it is not 0, so hide it
var display = 'none';
}
observers[key].reference.css('display', display);
}
// change value to -1 or 1
if( number == '-1' ) {
reference.attr('data-value', '1');
}
else {
reference.attr('data-value', '-1');
}
}
/*
* lets say we have 3 DOM objects with data-publish="1"
and 2 DOM objects with data-publish="2"
and one with data-observe="1 2";
so data-observe has to be called 5 times in order for him to be shown on the page;
each DOM object with data-publish will be added at runtime a data-value attribute
which will be -1 or 1. each time it is clicked or changed, it changes to the opposite.
this serves as data-observes will have a property named sum, which will be in the previous case 5
5 gets calculated with -1, or 1 when clicked data-publish DOM object.
So if i click first at data-publish="1" , 5 becomes 4. if i click again the same data-publish, becomes 5.
when sum property becomes 0, the data-observe is shown.
this function calculates how many data-publish="1" exists and so on
(it also does the other stuff needed for publishers)
*/
var _managePublishers = function() {
$('[data-publish]').each(function(){
var el = $(this);
// adds that value data, remember it? :D
el.attr('data-value', '-1');
// trim in case data-publish = "1 2 3" and store in an array
var publisher = el.data('publish').toString();
// we subscripe 'publisher' topic, but we check each string in topic
// here is tricky. if one publishers has more than one topic inside data-publish
// then we subscribe topic, but we check for topic's substring in publishers
var topics = publisher.split(' ');
if( !observer.topics[publisher] ) {
// we subscribe data-publish topic, becouse when we click it we want to fire something, no?
observer.subscribe( publisher, _observerFunction );
}
// but here in publishers we add only the substrings
for( var key in topics ) {
if( publishers[topics[key]] ) {
// the publisher exists
publishers[topics[key]] += 1;
}
else {
// the publisher doesn't exist
publishers[topics[key]] = 1;
}
}
});
}
// gets the observers, calculates sum, caches their reference
var _manageObservers = function() {
$('[data-observe]').each(function(){
var el = $(this);
// create random data-name
el.attr('data-name', _makeRandomString());
var datas = el.data('observe').toString().split(' '); // make an array again if we have multiple attachments
observers[el.data('name')] = (function(){
var sum = (function(){
var sum2 = 0;
// if datas[key] is found in publishers array, add it to sum
for( var key in datas ) {
var temp = publishers[datas[key]];
if( temp ) {
sum2 += temp;
}
}
return sum2;
})();
var reference = el; // caching, so it is faster !
var topicsObserved = datas;
// we need this when a user clicks data-publish, we need to see which DOM obj. are observing this.
// i really like revealing module pattern...i got used to it
return {
sum: sum,
reference: reference,
topicsObserved: topicsObserved
}
})();
})
}
var init = function() {
_managePublishers();
_manageObservers();
$('[data-publish]').on( 'click', function(){
observer.publish( $(this).data('publish'), $(this) );
});
$('select').on('change', function(){
var cache = $(this);
// if in this select there is an option which has value 1(there is chance that it triggered a succesfull publish) we publish that too
observer.publish( cache.find('[data-value="1"]').data('publish'), cache.find('[data-value="1"]') );
var el = cache.find('[data-publish]:selected');
observer.publish( el.data('publish'), el );
})
}
return {
init: init,
// in case you want to add after page load
// warning: i didn't test these methods. maybe it can be a bug somewhere
// it is not in my scope to test these, as i won't need for this example any adding after page load
publish: observer.publish,
subscribe: observer.subscribe
}
})();
ObserverPlugin.init();
So yeah, well this is it. you can see my codepen again (here) I made it in form of a plugin. You can attach to an observer multiple publishers, you can attach multiple publishers with same value to one observer, or you can simply attach only one pub to one observer. I tried to make it as efficient as possible. I hope this helps you Jashwant :)
(edited. now it supports multiple strings in data-publish, have fun!) (edited. you don't have to add random data-name string to HTML observers)
Upvotes: 4
Reputation: 2869
I liked your idea of making dynamic handling for element visibility based on few controls. I tried making some, and currently you can set visibility to other elements based on radiobuttons and select items. The solution below makes an assumption that the elements that control other elements' visibility have an id
-attribute. Perhaps, for performance reasons you could change that to a new attribute sth like: data-has-dependents
, so id
-attribute would only used for its value. If there are a lot of elements with id’s then this could be practical.
Also I allowed some elements to be visible if there are two different values chosen. vehicle
has comma separated list for values 2
and 3
below:
<p data-dependent='{"vehicle": "2,3" }'>Rocket engine type ? Gas turbine
<input type="radio" value="gasturbine" name="enginetype" />Aerojet Rocketdyne
<input type="radio" value="aerojet" name="enginetype" />
</p>
So if user chooses Plane or Space shuttle, then the radio buttons above are visible.
Also Type of Ice Cream fields are visible if either vehicle Ice Cream Truck or desert type Ice Cream is chosen:
<p data-dependent='{"vehicle": "4", "dessert": "icecream" }'>Type of Ice Cream? Vanilla:
<input type="radio" value="vanilla" name="icecream" />Strawberry Ice cream:
<input type="radio" value="strawberry" name="icecream" />Coffee Ice cream:
<input type="radio" value="coffee" name="icecream" />
</p>
Updated, in the code below the checkDependencies
function is used after one of the following instances takes places:
In the checkDependencies
function the first loop goes through each dependent data element, and data-dependent element’s attribute value is obtained. In the second loop each element with an id is obtained for its value. Finally, in the third and fourth loop the previously found dependent data element value is used to find a matching select (2.) or radio button’s parent element (3.). More accurately, the third loop is used to allow more than one key & value for dependent data elements, for example: data-dependent='{"vehicle": "4", "dessert": "icecream" }'
. The fourth loop allows dependent element to have two values for one key, for example. data-dependent='{"vehicle": "2,3" }'
. Therefore, the third and the fourth loops are for flexibility.
I think there could be more sophisticated answers than this + I think a MVC based JavaScript framework like AngularJS could be quite practical in this situation.
checkDependencies();
$("select[id]").on('change', function() {
checkDependencies();
});
$("[id] > :radio").on('click', function() {
checkDependencies();
});
function checkDependencies() {
$("[data-dependent]").each(function() {
$dependent = $(this);
var data = $(this).data("dependent");
var keyCount = Object.keys(data).length;
var checkedCount = 0;
var setVisible = false;
var dependentValues = $.map(data, function(value, index) {
return value;
});
$("[id]").each(function() {
var hasRadioButtons = $(this).find(":radio").length;
var elementId = $(this).attr("id");
var elementValue;
if (hasRadioButtons) {
elementValue = $(this).find(":checked").val()
} else {
elementValue = $(this).val();
}
for (i = 0; i < keyCount; i++) {
var dependentId = Object.keys(data)[i];
//if multiple values for one key
var values = dependentValues[i].split(",");
for (j = 0; j < values.length; j++) {
var dependentValue = values[j];
if (elementId === dependentId) {
//check if value selected
if (elementValue === dependentValue) {
checkedCount += 1;
setVisible = true;
$dependent.show();
//found element, exit inner loop
break;
} else {
//hide if not previously set visible
if (!setVisible)
$dependent.hide();
//if all element dependencies found exit inner loop
if (keyCount === checkedCount)
break;
}
}
}
}
});
});
}
[data-dependent] {
display: none;
}
#dessert {
margin-left: 20px;
display: inline
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<select id="vehicle">
<option value="0">Bike</option>
<option value="1">Car</option>
<option value="2">Plane</option>
<option value="3">Space shuttle</option>
<option value="4">Ice Cream Truck</option>
</select>
<p id="dessert">Dessert: Sweets
<input type="radio" value="sweets" name="dessert" />Ice cream
<input type="radio" value="icecream" name="dessert" />Cake
<input type="radio" value="cake" name="dessert" />
</p>
<p data-dependent='{"vehicle": "0" }'>Bike brand ? Trek
<input type="radio" value="trek" name="bike" />Giant
<input type="radio" value="gt" name="bike" />Cannondale
<input type="radio" value="cannondale" name="bike" />
</p>
<p data-dependent='{"vehicle": "1" }'>Car's fuel type ? Petrol
<input type="radio" value="petrol" name="fueltype" />Diesel
<input type="radio" value="diesel" name="fueltype" />Biodiesel
<input type="radio" value="biodiesel" name="fueltype" />Hybrid
<input type="radio" value="hybrid" name="fueltype" />
</p>
<p data-dependent='{"vehicle": "2,3" }'>Rocket engine type ? Gas turbine
<input type="radio" value="gasturbine" name="enginetype" />Aerojet Rocketdyne
<input type="radio" value="aerojet" name="enginetype" />
</p>
<p data-dependent='{"vehicle": "1" }'>Left / right hand drive? Left:
<input type="radio" value="left" name="dir" />Right:
<input type="radio" value="right" name="dir" />
</p>
<select data-dependent='{"dessert": "sweets" }'>
<option value="0">Jelly beans</option>
<option value="1">Haribo gummy bears</option>
<option value="2">Fruit candy</option>
</select>
<p data-dependent='{"vehicle": "4", "dessert": "icecream" }'>Type of Ice Cream? Vanilla:
<input type="radio" value="vanilla" name="icecream" />Strawberry Ice cream:
<input type="radio" value="strawberry" name="icecream" />Coffee Ice cream:
<input type="radio" value="coffee" name="icecream" />
</p>
<p data-dependent='{"dessert": "cake" }'>Type of cake? Chocolate Cake:
<input type="radio" value="chokocake" name="cake" />Cheesecake:
<input type="radio" value="cheesecake" name="cake" />Carrot Cake:
<input type="radio" value="carrotcake" name="cake" />
</p>
Upvotes: 2
Reputation: 1
to change the style you can use
$('p').css("display","");
or
$('p').css("display","none");
Upvotes: -1