Home : Webmonkey : Code : Dropdown Lists In Javascript

I originally submitted this article to Evolt on 15 Feb 2003

introduction

Have you ever hit a dropdown box that was just too long? There were so many items that finding the one you wanted was a hassle.

You hit the first letter of the option you were looking for, only to find that there are at least 50 options starting with that letter - or worse, the list wasn't sorted alphabetically and you had to type the letter 20 times to get to your option.

Maybe the option you wanted was phrased differently than you expected. It didn't even start with the letter you were typing!

Sound familliar? Well, there are a couple of solutions. One way around this problem is to stage your dropdown boxes; what you choose in box 1 determines your options in box 2, etc.

This is often a great way to reduce cumbersome lists into managable chunks. However this technique is based on the assumption that your visitors understand the way you've categorised the options.

Take for example a simple dropdown list of countries. In the past, I've found my beloved Australia under categories such as "Asia", "Pacific", "South Pacific", and, "Oceania". Searching for options this way can be a bit hit and miss.

Another solution to the "long list" problem is to allow your visitor to make their option magically rise to the top of the list...

example

Search:

the code

<html>
<head>
	<title>Dropdown Filter</title>
</head>

<body>
<script language="JavaScript" type="text/javascript">
<!--
/*
  - Give Credit Where Its Due -
  Please acknowledge this article and its author, at
  least in code comments, when using this code.


  Thank you.
  Justin Whitford
*/

/*
  filtery(pattern, list)
  pattern: a string of zero or more characters by which to filter the list
  list: reference to a form object of type, select

  Example:
  <form name="yourForm">
    <input type="text" name="yourTextField"
       onchange="filtery(this.value,this.form.yourSelect)">
    <select name="yourSelect">
      <option></option>
      <option value="Australia">Australia</option>
       .......
*/
function filtery(pattern, list){
  /*
  if the dropdown list passed in hasn't
  already been backed up, we'll do that now
  */
  if (!list.bak){
    /*
    We're going to attach an array to the select object
    where we'll keep a backup of the original dropdown list
    */
    list.bak = new Array();
    for (n=0;n<list.length;n++){
      list.bak[list.bak.length] = new Array(list[n].value, list[n].text);
    }
  }

  /*
  We're going to iterate through the backed up dropdown
  list. If an item matches, it is added to the list of
  matches. If not, then it is added to the list of non matches.
  */
  match = new Array();
  nomatch = new Array();
  for (n=0;n<list.bak.length;n++){
    if(list.bak[n][1].toLowerCase().indexOf(pattern.toLowerCase())!=-1){
      match[match.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }else{
      nomatch[nomatch.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }
  }

  /*
  Now we completely rewrite the dropdown list.
  First we write in the matches, then we write
  in the non matches
  */
  for (n=0;n<match.length;n++){
    list[n].value = match[n][0];
    list[n].text = match[n][1];
  }
  for (n=0;n<nomatch.length;n++){
    list[n+match.length].value = nomatch[n][0];
    list[n+match.length].text = nomatch[n][1];
  }

  /*
  Finally, we make the 1st item selected - this
  makes sure that the matching options are
  immediately apparent
  */
  list.selectedIndex=0;
}
// -->
</script>

<form name="yourForm">
  Search <input type="text" name="yourTextField"
       onkeyup="filtery(this.value,this.form.yourSelect)"
       onchange="filtery(this.value,this.form.yourSelect)">
  <select name="yourSelect">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>
</form>


</body>
</html>

Discussion

If the visitor does several searches, the dropdown list could get rather ugly from the constant shuffling. To combat this, we make a copy of the dropdown list and use that as the master copy. We don't want to do this every time the list is filtered - just the first time.

The first thing the function does is check whether a master copy exists. If one does not, then it attaches and populates a master copy array to the form's select object

The function then creates two arrays: one for matches and one for non-matches. Now, as it iterates through the master copy, it appends items to either of the two arrays.

Once each item in the dropdown has been assessed, the matches and non-matches arrays are written back to the select object.

Finally, the function forces the first item in the list to be selected - this makes sure that the matching options are immediately apparent to the visitor

variation #1

If you're not fussed about supporting version three browsers, you can enhance the function by having it match by regular expression.

Simply add this near the top of the function:

pattern = new RegExp(pattern,"i");

... and replace...

    if(list.bak[n][1].toLowerCase().indexOf(pattern.toLowerCase())!=-1){
... with...
    if(pattern.test(list.bak[n][1])){

Your visitors will now be able to type:

  • "^au" to get all items starting with au.
  • "ia$" to get all items ending with ia.
  • "9\d\d" to get all items containing numbers between 900 and 999.
  • etc, etc.

variation #1 code

/*
  - Give Credit Where Its Due -
  Please acknowledge this article and its author, at
  least in code comments, when using this code.


  Thank you.
  Justin Whitford
*/
function filtery(pattern, list){
  if (!list.bak){
    list.bak = new Array();
    for (n=0;n<list.length;n++){
      list.bak[list.bak.length] = new Array(list[n].value, list[n].text);
    }
  }

  pattern = new RegExp(pattern,"i");
  match = new Array();
  nomatch = new Array();
  for (n=0;n<list.bak.length;n++){
    if(pattern.test(list.bak[n][1])){
      match[match.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }else{
      nomatch[nomatch.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }
  }

  for (n=0;n<match.length;n++){
    list[n].value = match[n][0];
    list[n].text = match[n][1];
  }
  for (n=0;n<nomatch.length;n++){
    list[n+match.length].value = nomatch[n][0];
    list[n+match.length].text = nomatch[n][1];
  }

  list.selectedIndex=0;
}

variation #2

Perhaps you'd like something a bit funkier

(Find in list - click me!)

This time we're using a bit of dynamic HTML to make the search box appear and disappear. I've added an extra function to handle this

variation #2 code

<script language="JavaScript" type="text/javascript">
<!--
/*
  - Give Credit Where Its Due -
  Please acknowledge this article and its author, at
  least in code comments, when using this code.


  Thank you.
  Justin Whitford
*/
findBak='';
function listSearch(obj,formName){
  listName=obj.id.substring(7,obj.id.length);
  if(obj.innerHTML.indexOf(listName)==-1){
    var listSource;
    if(formName==null){
      for(f in document.forms){
        if(document.forms[f].elements && document.forms[f].elements[listName]){
          listSource=document.forms[f].elements[listName];
          formName=f;
        }
      }
    }
    findBak=obj.innerHTML;
    obj.innerHTML="<input size='4' name='find_"+listName+"' type='text'"
      +" onkeyup=\"filtery(this.value,document.forms['"
	  +formName
	  +"']."+listName+")\""
      +" onblur=\"document.getElementById('"
	  +obj.id
	  +"').innerHTML=findBak\""
      +"/>";
    document.forms[formName].elements['find_'+listName].focus();
  }
}
function filtery(pattern, list){
  if (!list.bak){
    list.bak = new Array();
    for (n=0;n<list.length;n++){
      list.bak[list.bak.length] = new Array(list[n].value, list[n].text);
    }
  }

  match = new Array();
  nomatch = new Array();
  for (n=0;n<list.bak.length;n++){
    if(list.bak[n][1].toLowerCase().indexOf(pattern.toLowerCase())!=-1){
      match[match.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }else{
      nomatch[nomatch.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }
  }

  for (n=0;n<match.length;n++){
    list[n].value = match[n][0];
    list[n].text = match[n][1];
  }
  for (n=0;n<nomatch.length;n++){
    list[n+match.length].value = nomatch[n][0];
    list[n+match.length].text = nomatch[n][1];
  }

  list.selectedIndex=0;
}
// -->
</script>

<form name="yourForm">
<p>
  <select name="yourSelect">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>
  <span id='search_yourSelect' 
  onclick='listSearch(this)'>(Find in list - click me!)&t;/span>
</p>
</form>

variation #3

I received a question asking how to have one search box and multiple dropdowns, using radio buttons to target the search

Search
1
2
3

First we'll change the HTML

Search <input type="text" name="yourTextField"
  onkeyup="filtery(this.value,this.form,'searchSelect')"
  onchange="filtery(this.value,this.form.yourSelect)"><br/>

<input type="radio" name="searchSelect" value="list1" checked="true">1<br/>
<input type="radio" name="searchSelect" value="list2">2<br/>
<input type="radio" name="searchSelect" value="list3">3<br/>

  <select name="list1">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>

  <select name="list2">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>

  <select name="list3">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>
Notice that I've changed the JavaScript function signature to take three arguments.

Now we just update the 1st couple of lines of the function:

function filtery(pattern, formObj, radioName){
  /*
  First we have to find out which list to work with
  */
  var radioGroup=formObj[radioName];
  var list;
  for(n=0;n<radioGroup.length;n++){
    list=(radioGroup[n].checked)?formObj[radioGroup[n].value]:list;
  }
  if (!list.bak){

variation #3 code

<html>
<head>
	<title>Dropdown Filter</title>
</head>

<body>
<script language="JavaScript" type="text/javascript">
<!--
/*
  - Give Credit Where Its Due -
  Please acknowledge this article and its author, at
  least in code comments, when using this code.


  Thank you.
  Justin Whitford
*/
function filtery(pattern, formObj, radioName){
  var radioGroup=formObj[radioName];
  var list;
  for(n=0;n<radioGroup.length;n++){
    list=(radioGroup[n].checked)?formObj[radioGroup[n].value]:list;
  }
  if (!list.bak){
    list.bak = new Array();
    for (n=0;n<list.length;n++){
      list.bak[list.bak.length] = new Array(list[n].value, list[n].text);
    }
  }

  match = new Array();
  nomatch = new Array();
  for (n=0;n<list.bak.length;n++){
    if(list.bak[n][1].toLowerCase().indexOf(pattern.toLowerCase())!=-1){
      match[match.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }else{
      nomatch[nomatch.length] = new Array(list.bak[n][0], list.bak[n][1]);
    }
  }

  for (n=0;n<match.length;n++){
    list[n].value = match[n][0];
    list[n].text = match[n][1];
  }
  for (n=0;n<nomatch.length;n++){
    list[n+match.length].value = nomatch[n][0];
    list[n+match.length].text = nomatch[n][1];
  }

  list.selectedIndex=0;
}
// -->
</script>

<form name="yourForm">
  Search <input type="text" name="yourTextField"
  onkeyup="filtery(this.value,this.form,'searchSelect')"
  onchange="filtery(this.value,this.form,'searchSelect')"><br/>

  <input type="radio" name="searchSelect" value="list1" checked="true">1<br/>
  <input type="radio" name="searchSelect" value="list2">2<br/>
  <input type="radio" name="searchSelect" value="list3">3<br/>

  <select name="list1">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>

  <select name="list2">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>

  <select name="list3">
    <option></option>
    <option value="Australia">Australia</option>
    <option value="China">China</option>
    <option value="England">England</option>
    <option value="New Zealand">New Zealand</option>
  </select>
</form>


</body>
</html>