Show/Hide form fields based on value of other fields

12,601

Solution 1

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)

Solution 2

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:

  1. page is loaded
  2. select element with id has changed
  3. radio button is clicked, and the radio button’s parent has an id

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>
Share:
12,601
Jashwant
Author by

Jashwant

Freelancer who loves to code :) Twitter Handle

Updated on June 14, 2022

Comments

  • Jashwant
    Jashwant almost 2 years

    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:

    Fiddle Demo

    $('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:

    Fiddle Demo

    $('[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.

  • Jashwant
    Jashwant over 9 years
    Question does not ask for another way to hide fields.
  • Jashwant
    Jashwant over 9 years
    Looks good :) However, I didn't get the logic entirely. I'll check again tomorrow.
  • jyrkim
    jyrkim over 9 years
    @Jashwant Hi, I'm glad you liked it :-) You're right I didn't provide enough detail to go with my answer. Now I have updated my answer with some logic on how to code works - I hope it helps. If you still have questions, then just let me know, and I'll try give some more insight how the code works.
  • jyrkim
    jyrkim over 9 years
    Very useful and excellent answer - I'd say almost text-book like :-)