Display element when specific option is selected

Hi there,

I have the following code:

    <select id="location" name="drop">
        <option value="loc1">Location 1</option>
        <option value="loc2">Location 2</option>
        <option value="loc3">Location 3</option>
        <option value="loc4">Location 4</option>
        <option value="loc5">Location 5</option>
        <option value="loc6">Location 6</option>
        <option value="loc7">Location 7</option>
      </select>


    <select id="second" name="term">
        <option value="OPT1">Option 1</option>
        <option value="OPT2">Option 2</option>
        <option value="OPT3">Option 3</option>
        <option value="OPT4">Option 4</option>
      </select>

What I would like to do is to show the second select field only when “Location 5” is selected in the first drop down.

How would I do this?

Thanks

1 Like

You would:

  • hide the second one (CSS is good for that),
  • use JavaScript to attach an onchange event to the first one
  • In that onchange event, you would:
    • check to see if Location 5 is currently selected.
    • If it is, set a class name on the second select, so that it becomes visible
    • If it isn’t, remove the class name from the second select, returning it to being hidden

That’s how you would do it.

If you need it, here’s some sample code showing how it’s done.

#second {
  display: none;
}

#second.show {
  display: block;
}
const source = document.querySelector("#location");
const target = document.querySelector("#second");

const displayWhenSelected = (source, value, target) => {
    const selectedIndex = source.selectedIndex;
    const isSelected = source[selectedIndex].value === value;
    target.classList[isSelected
        ? "add"
        : "remove"
    ]("show");
};
source.addEventListener("change", (evt) =>
    displayWhenSelected(source, "loc5", target)
);
3 Likes

Thanks. I managed to come up with this:

$("#location").change(function(){
   if($(this).val()=="loc5")
   {    
       $("div#container").show();
   }
    else
    {
        $("div#container").hide();
    }
});

However, what I have found is that if I select an option from the second drop when visible, it keeps the value when hidden if switched back to the other option.

Is there a way I can set the value of the second drop down when it is hidden to over-ride the changed value when visible? So basically revert to a different value when the other options are selected. Not sure if that makes sense?

Do you mean you want the second select to always open with option 1 rather than the one that was selected before it was hidden again?

If so then you could try this:

$("#location").change(function(){
   if($(this).val()=="loc5")
   {    
       $("div#container").show();
	   $("#second").val($("#second option:first").val());
   }
    else
    {
        $("div#container").hide();
    }
});

Paul will give the correct answer though :slight_smile:

1 Like

Hi there toolman,

not being a bona fide javascript coder my little effort does
look rather old fashioned compared to that of @Paul_Wilkins. :cold_sweat:

Nevertheless, it does meet your requirements, assuming, of
course, that I have understood them correctly. :sunglasses:

Also note that there is no “JQuery” overload and I have
allowed for those who have javascript disabled. :mask:

<!DOCTYPE html>
<html lang="en">
<head>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1">

<title>untitled document</title>

<style media="screen">
.hide  {
    display: none;
 }
</style>
</head>
<body> 
<form id="myform" action="#">
<select id="location" name="drop">
 <option value="">drop selection</option>
 <option value="loc1">Location 1</option>
 <option value="loc2">Location 2</option>
 <option value="loc3">Location 3</option>
 <option value="loc4">Location 4</option>
 <option value="loc5">Location 5</option>
 <option value="loc6">Location 6</option>
 <option value="loc7">Location 7</option>
</select>
<select id="second" name="term">
 <option value="">term selection</option>
 <option value="OPT1">Option 1</option>
 <option value="OPT2">Option 2</option>
 <option value="OPT3">Option 3</option>
 <option value="OPT4">Option 4</option>
</select>
</form>

<script>
(function() {
   'use strict';
   /* jshint browser: true */

   var d=document;
   var mf=d.getElementById('myform');
   var se=d.getElementById('second');
   var lo=d.getElementById('location')
   var temp;

   mf.reset();
   se.className='hide';
   lo.onchange=function() {
if(this.value==='loc5') {
   se.className=se.className.replace('hide','');
 }
else {
   temp=this.value;
   se.className='hide';
   mf.reset();
   lo.value=temp;
  }
 };
}());
</script>
</body>
</html>

coothead

3 Likes

The best solution here is one that requires no JavaScript at all.

All you need to do is to tell the backend that’s receiving the form to ignore the second select until location 5 has been chosen.

2 Likes

Thanks, I figured that it was a good time to flex some ES6 skills. The trouble is, that it’s not as easy to understand as simpler code.

Here’s what I mean. The ES6 code is:

const displayWhenSelected = (source, value, target) => {
    const selectedIndex = source.selectedIndex;
    const isSelected = source[selectedIndex].value === value;
    target.classList[isSelected
        ? "add"
        : "remove"
    ]("show");
};
source.addEventListener("change", (evt) =>
    displayWhenSelected(source, "loc5", target)
);

Why would you want to code that way? After all, the following ES5 code is so much easier to understand:

document.getElementById("location").onchange = function () {
    if (this[this.selectedIndex].value === "loc5") {
        document.getElementById("second").className = "show";
    } else {
        document.getElementById("second").className = "";
    }
};

There are many reasons to change the above code, all that apply at different stages of the code lifecycle.

When comparing document.getElementById to document.querySelector, the second is shorter and provides greater flexibility should you ever need to change it.

document.querySelector("#location").onchange = function () {
    if (this[this.selectedIndex].value === "loc5") {
        document.querySelector("#second").className = "show";
    } else {
        document.querySelector("#second").className = "";
    }
};

We are also referring to the second select element twice, which can be pulled out to a variable:

document.querySelector("#location").onchange = function () {
    var target = document.querySelector("#second");
    if (this[this.selectedIndex].value === "loc5") {
        target.className = "show";
    } else {
        target.className = "";
    }
};

We can also pull out a matching source variable too, and place the source and target together for easier maintenance.

var source = document.querySelector("#location");
var target = document.querySelector("#second");

source.onchange = function () {
    if (this[this.selectedIndex].value === "loc5") {
        target.className = "show";
    } else {
        target.className = "";
    }
};

If we ever end up styling that second select, the className part will end up ruining things because it completely replaces the class attribute. So instead, we can use classList.add and classList.remove:

var source = document.querySelector("#location");
var target = document.querySelector("#second");

source.onchange = function () {
    if (this[this.selectedIndex].value === "loc5") {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
};

Also, the onchange event will easily be lost if any other code adds an onchange event to that element, so we should use addEventListener instead:

var source = document.querySelector("#location");
var target = document.querySelector("#second");

source.addEventListener("change", function () {
    if (this[this.selectedIndex].value === "loc5") {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}, false);

The this keyword tends to be frowned on because it hides what you’re referring to. Normally we would assign it to a separate variable inside of the function:

var source = document.querySelector("#location");
var target = document.querySelector("#second");

source.addEventListener("change", function () {
    var source = this;
    if (source[source.selectedIndex].value === "loc5") {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}, false);

But because we already have a reference to the source variable so close by, and we are already using the target variable within, we can easily go without assigning the this keyword too.

var source = document.querySelector("#location");
var target = document.querySelector("#second");

source.addEventListener("change", function () {
    if (source[source.selectedIndex].value === "loc5") {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}, false);

The source[source.selectedIndex] part is better understood by us as source[selectedIndex], so we can put the selectedIndex in to a separate variable too.

var source = document.querySelector("#location");
var target = document.querySelector("#second");

source.addEventListener("change", function () {
    var selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === "loc5") {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}, false);

This though is still a lot of code for an event handler. We can move things out to a separate function called displayWhenSelected so that the event handler does just one thing, while the separate function handles the rest.

var source = document.querySelector("#location");
var target = document.querySelector("#second");

function displayWhenSelected() {
    var selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === "loc5") {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}
source.addEventListener("change", function () {
    displayWhenSelected();
}, false);

The source and target variables, along with the select value, all now look like they are better suited as function parameters.

var source = document.querySelector("#location");
var target = document.querySelector("#second");

function displayWhenSelected(source, displayValue, target) {
    var selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === displayValue) {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}
source.addEventListener("change", function () {
    displayWhenSelected(source, "loc5", target)
}, false);

We can even bring the “loc5” out to a displayValue for easy configuration too:

var source = document.querySelector("#location");
var displayValue = "loc5";
var target = document.querySelector("#second");

function displayWhenSelected(source, displayValue, target) {
    var selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === displayValue) {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}
source.addEventListener("change", function () {
    displayWhenSelected(source, displayValue, target)
}, false);

The target.classList duplication, along with the if statement can be avoided by using a ternary to choose whether to add or remove:

var source = document.querySelector("#location");
var displayValue = "loc5";
var target = document.querySelector("#second");

function displayWhenSelected(source, displayValue, target) {
    var selectedIndex = source.selectedIndex;
    var action = (source[selectedIndex].value === displayValue) ? "add" : "remove";
    target.classList[action]("show");
}
source.addEventListener("change", function () {
    displayWhenSelected(source, displayValue, target)
}, false);

But now, the action condition is hard to read, so let’s pull that out to a selectedValue variable:

var source = document.querySelector("#location");
var displayValue = "loc5";
var target = document.querySelector("#second");

function displayWhenSelected(source, displayValue, target) {
    var selectedIndex = source.selectedIndex;
    var selectedValue = source[selectedIndex].value;
    var action = (selectedValue === displayValue) ? "add" : "remove";
    target.classList[action]("show");
}
source.addEventListener("change", function () {
    displayWhenSelected(source, displayValue, target)
}, false);

Is that better? I don’t think so. It was a mistake to remove the if condition part of the code. Let’s go back to the if condition:

var source = document.querySelector("#location");
var displayValue = "loc5";
var target = document.querySelector("#second");

function displayWhenSelected(source, displayValue, target) {
    var selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === displayValue) {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}
source.addEventListener("change", function () {
    displayWhenSelected(source, displayValue, target)
}, false);

That’s a large improvement from the ES5 code that we started with. The code is easier to manage when it comes to future changes, and is more resilient to potential problems that may come along.

Converting the above code to ES6 doesn’t need to be done, but can help to simplify and streamline more of the code.

For comparison though, here’s the original ES5 code with comments on why certain parts deserve improvement, as compared with the improved ES5 code seen above.

// hard to find all references to elements, if they need to be updated
// getElementById is too restrictive, incapable of handling future changes to the HTML design
// onchange event can easily be clobbered by other code
document.getElementById("location").onchange = function () {
    // too much code within the onchange function
    // the this keyword can easily be confused
    // it's better understand as obj[index] without other references within
    // "loc5" is deeply hidden within the code
    if (this[this.selectedIndex].value === "loc5") {
        // className can easily be clobbered by other code
        document.getElementById("second").className = "show";
    } else {
        // duplicate reference to the same element
        document.getElementById("second").className = "";
    }
};
4 Likes

Hi there Paul,

so it’s back to the drawing board then. :cold_sweat:

Or perhaps I should just stick to CSS? :sunglasses:

As a septuagenarian I am also fully aware that
the grim reaper is breathing down my neck and
he has cut a swathe through many of us recently. :scream:

Still I have C&P’d your ES6 lesson just in case the
bugger misses me out. :mask:

coothead

I always get excited when @Paul_Wilkins gets on a tutorial roll. Beautiful work, as usual, Paul. You really should write a book or create a course. Your logical approach is so interesting and helpful. :slight_smile:

2 Likes

[quote=“coothead, post:8, topic:226616, full:true”]
so it’s back to the drawing board then. :cold_sweat:[/quote]

Not at all. All of the above code achieves the same objective. It’s just a matter of how long it will be before we have to come back to it to make further changes. Ideally we do not ever want to come back to make changes to the code. But if we do, it should be as easy as possible to make the desired changes.

It’s far better to use CSS instead of using JavaScript to make the CSS changes. Mostly for management reasons again. When we want to change how something looks, it’s the CSS that we want to look at first.

What is not wanted is to search through the CSS, then through the HTML, then through the JS, only to realize that you had the PHP code saying how to style something. That’s a huge waste of time. It’s best to keep content in HTML, the styling in CSS, and the behaviour in JavaScript.

Thanks, that’s much appreciated. It’s ES5 though for my most recent large post.

Here’s the difference between ES5 and ES6 code, starting with the recommended ES5 code from before:

var source = document.querySelector("#location");
var displayValue = "loc5";
var target = document.querySelector("#second");

function displayWhenSelected(source, displayValue, target) {
    var selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === displayValue) {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}
source.addEventListener("change", function () {
    displayWhenSelected(source, displayValue, target)
}, false);

With ES6, we can change the var assignments to let or const, where let is used for variables that you intend to change later, and const is used for what will not be changing.

const source = document.querySelector("#location");
const displayValue = "loc5";
const target = document.querySelector("#second");

function displayWhenSelected(source, displayValue, target) {
    const selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === displayValue) {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
}
source.addEventListener("change", function () {
    displayWhenSelected(source, displayValue, target)
}, false);

Next up, we can use fat-arrow notation for functions. This is where function () { … } is replaced with () => { … }
The fat-arrow notation has some different implications for the this keyword too (where it doesn’t change when going to a new fat-arrow function), but that’s of no consequence here.

const source = document.querySelector("#location");
const displayValue = "loc5";
const target = document.querySelector("#second");

const displayWhenSelected = (source, displayValue, target) => {
    const selectedIndex = source.selectedIndex;
    if (source[selectedIndex].value === displayValue) {
        target.classList.add("show");
    } else {
        target.classList.remove("show");
    }
};
source.addEventListener("change", () => {
    displayWhenSelected(source, displayValue, target)
}, false);

Other benefits of ES6 techniques aren’t all that relevant to the code that we’re using here, but we can certainly enclose all of the code within an immediately invoked function expression (called an IIFE), so that the variables and functions do not affect any other code that is outside of the IIFE.

Here are what the ES5 and ES6 IIFE’s look like:

// IIFE for ES5
var moduleName = (function () {
    // rest of code in here
}());

// IIFE for ES6
const moduleName = (() => {
    // rest of code in here
})();

In the ES5 code sample the enclosing parenthesis are not strictly necessary, but serve as an important indicator to other programmers about what is going on there. When they’re not being assigned to a module name, ES5 code requires the IIFE to be wrapped in parenthesis in order to allow the IIFE function to be invoked, so consistency here with the parentheses is handy.

The resulting ES6 code below ends up being not that much different from the ES5 code that we started with in this post.

(() => {
    const source = document.querySelector("#location");
    const displayValue = "loc5";
    const target = document.querySelector("#second");

    const displayWhenSelected = (source, displayValue, target) => {
        const selectedIndex = source.selectedIndex;
        if (source[selectedIndex].value === displayValue) {
            target.classList.add("show");
        } else {
            target.classList.remove("show");
        }
    };
    source.addEventListener("change", () => {
        displayWhenSelected(source, displayValue, target)
    }, false);
})();
3 Likes

Hi there Paul,

so, is the use of…

   'use strict';

…now dispensed with in ES6?

coothead

The use strict directive is still required. It’s only when using modules that it’s not required, as those are strict by default.

1 Like

Well, that is really comforting to this bald headed old fart. :sunglasses:

I will, at least, be able to start my code with something familiar. :ok_hand:

coothead

2 Likes

With the refactoring of the code in post #7, it came to light that it was all achieved thanks mainly to one simple technique.

Here’s the list of changes that were made to improve the code:

1 getElementById to querySelector
2 create variable to make multiple references more generic
3 moved variables to the top of the code: group configurable variables together
4 className to classList.add and classList.remove
5 replace onchange with addEventListener
6 replace this keyword with source variable
7 create variable to change source[source.selectedIndex] to source[selectedIndex]
8 extract event handler code to a separate function
9 provide parameters for the function
10 extract the “loc5” variable

Most of these follow one basic rule to make the specific more generic. The only other type of change was to improve the readability of the code.

Make specific more generic

When improving your code you will invariably find that as the tests become more specific, the code becomes more generic. That just so happens to be one of the foundational rules of test-driven development.

Here are all of the changes that were made, that resulted in the code becoming more generic.

  • getElementById to querySelector
    • as querySelector is more capable of handling both class and id selectors, whichever is needed at the time
  • create variable for multiple references
    • allowing us to edit only the one variable which will be referred to from multiple locations
  • className to classList.add and classList.remove
    • instead of the class being changed to the exact string removing everything else that was there, this applies a more generic technique allowing us to add and remove from what may already be there
  • replace onchange with addEventListener
    • instead of having only one specific onchange handler on the element, this can now be one of several different onchange events on that element
  • create variable to change source[source.selectedIndex] to source[selectedIndex]
    • replace a specific reference with a more generic one, improving the readability of the code
  • extract event handler code to a separate function
    • ensure a separation of concerns by having the event handler do just one thing
  • provide parameters for the function
    • allowing the code within to more generically handle different source and target elements
  • extract the “loc5” variable
    • replacing the specific reference with a more generic variable, allowing us to have all the configurable information in just one area at the top

Improve readability

As other people are almost guaranteed to be seeing this code, and you yourself won’t know all the details of how your code works even some months from now, it’s important that the code is as easy to understand as can be reasonably achieved.

  • move variables to the top of the code
    • group configurable variables together
  • replace this keyword with source variable
    • replacing the generic this keyword with a named variable to help give an improved understanding of the code

Further details

Techniques like this and more are covered in books such as Clean Code, and test driven development using Jasmine can easily hone those techniques.

Code katas are easy programming tasks that are designed for you to repeat daily, not only as warm-up tasks, but the process of repeatedly solving the problem allows you to think more deeply about how you approach it.

Corey Haines provides a wonderful demonstration of test-driven development techniques, with his Roman Numerals Kata with Commentary kata (video). The explanations of his thought process are a sight to behold.

3 Likes

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.