Dependent dropdowns
Introduction
On a regular basis, people ask on the forums for a solution implementing dependent dropdowns.
- dependent dropdowns
- values in a dropdown are depending on a selection made in an earlier dropdown field.
Since selecting a value in a dropdown is done client-side, at least some part of the solution will include (client-side) javascript coding.
Possible solutions may exist out of these:
- use hidden frames to call a custom made HTMPL file containing the values for the next dropdown, then use this data to populate the dropdown in the main window
- an AJAX implementation
- find a way to call the CA Service Desk web services using javascript, get the necessary values from the server and populate the dropdown
- pre-load all data when your form gets loaded, then use javascript to display values when making selections in the dropdowns
All these solutions will have advantages and disadvantages (e.g. calling webservices, using AJAX and even the 'hidden frame solution' may work a little slower compared to pre-loading all data. On the other hand, pre-loading data may take a long time if you have hundreds of values)
Let's take a look at an implementation that pre-loads all data.
Components
Server side
When a web server gets a request to serve a page it will first parse all server side code (HTMPL) and replace that code with valid HTML code before passing that HTML back to the requester of the page.
To start, you will want to have a place in the database where you store the values of the drowndowns and their dependencies. This can be done in many different ways (having 3 custom tables, then using 2 more to store the LREL between them), but let's keep it simple for now: as you may know, the request area structure is much like dependent dropdowns: once you select a node in the tree, you get access to the underlying nodes.
As a sidenote, you could simply use one field similar to the request area to implement this functionality instead of using all dropdown fields. But in case you still want to go ahead with the dropdowns, read on...
For this example, we will (re)use the rootcause field and the structure it provides (using . as a separator), but you could of course create your own table for this.
The values in my rootcause table are: <source lang="text"> Hardware.Server.Error Hardware.Server.Component Failure Hardware.Network.Firewall Hardware.Network.Router Hardware.Network.Cable Software.Browser.Settings Software.Browser.Version </source>
This code comes as is, and has been tested for these values only. Further finetuning may be necessary when deviating from a 3 level deep structure.
To get these values from the back end and pass them onto our front end (javascript) we will use a PDM_LIST tag.
Client side
As you can see in the previous section, we will be using 3 dropdown fields (list1, list2 and list3), each containing a level of the tree shown above. These fields will be added to the detail_cr.htmpl form. These fields will be dropdowns when in edit mode
and plain text when in viewing mode.
An additional field (SET.rootcause) will be necessary to make sure that any selection made in the dropdowns is stored into the database when you save the form.
Next to the fields, we will need (javascript) logic to do 4 things:
- build the relationships
- populate a dropwdown based on values from a previous one (fillDependantDropdown)
- save the values to the database once a selection is made in the last dropdown (setrootcause)
- display values back from the database when the form is initially loaded (readrootcause)
All together now
This solution will display 3 dependent dropdowns on the detail_cr.htmpl form and allow you to view, select and store values into the rootcause field using these dropdowns.
- At the top of your page, right after all the javascript include files, create your own javascript tag containing this:
<source lang="javascript"> var allRootCauses = new Array(); var thevalues = new Array();
var somevalue; <PDM_LIST PREFIX=rootcauses WHERE="delete_flag = 0" FACTORY=rc> somevalue = "$rootcauses.id" + "|" + "$rootcauses.sym"; allRootCauses[allRootCauses.length] = somevalue; </PDM_LIST>
var tempArray; var somecounter; var temp; var tempArr1; var tempArr2; var prevlevel1; prevlevel1 = ""; var prevlevel2; prevlevel2 = ""; var len1; var len2; var len3; len1 = -1; len2 = -1; len3 = -1;
for (somecounter=0; somecounter < allRootCauses.length; somecounter++) {
tempArr1 = new Array(); tempArr1 = allRootCauses[somecounter].split("|");
tempArr2 = new Array(); tempArr2 = tempArr1[1].split(".");
if (tempArr2[0] != prevlevel1) { len1+= 1; thevalues[len1] = new Array(); prevlevel1 = tempArr2[0]; len2 = -1; len3 = -1; }
if (tempArr2[1] != prevlevel2) {
len2 += 1;
thevalues[len1][len2] = new Array();
prevlevel2 = tempArr2[1];
len3 = -1;
}
len3 += 1; thevalues[len1][len2][len3] = allRootCauses[somecounter];
}
var thelevel1;
var thelevel2;
var thelevel3;
function readrootcause() {
var readcounter;
var readid;
var tempsplit;
for (readcounter=0; readcounter < allRootCauses.length; readcounter++) {
readid = allRootCauses[readcounter].split("|")[0];
if (readid == "$args.rootcause") { tempsplit = allRootCauses[readcounter].split("|")[1].split("."); thelevel1 = tempsplit[0]; thelevel2 = tempsplit[1]; thelevel3 = tempsplit[2]; return; } } }
function fillDependantDropdown(indicator) {
if (_dtl.edit) {
var list1index; var list2index;
list1index = document.main_form.list1.selectedIndex-1; list2index = document.main_form.list2.selectedIndex-1;
var i; var somevalue; var sometext;
if (indicator == 1) { for (i=document.main_form.list1.options.length; i>0; i--) { document.main_form.list1.options[i] = null; } for (i=document.main_form.list2.options.length; i>0; i--) { document.main_form.list2.options[i] = null; } for (i=document.main_form.list3.options.length; i>0; i--) { document.main_form.list3.options[i] = null; } document.main_form.list1.options[0] = new Option("-- select --", ""); for (i=0; i<thevalues.length; i++) { somevalue = new Array(); somevalue = thevalues[i][0][0].split("|"); sometext = new Array(); sometext = somevalue[1].split("."); document.main_form.list1.options[i+1] = new Option(sometext[0], somevalue[0]); if (sometext[0] == thelevel1) { document.main_form.list1.options[i+1].selected = true; } } return; }
if (indicator == 2) { for (i=document.main_form.list2.options.length; i>0; i--) { document.main_form.list2.options[i] = null; } for (i=document.main_form.list3.options.length; i>0; i--) { document.main_form.list3.options[i] = null; } document.main_form.list2.options[0] = new Option("-- select --", ""); for (i=0; i<thevalues[list1index].length; i++) { somevalue = new Array(); somevalue = thevalues[list1index][i][0].split("|"); sometext = new Array(); sometext = somevalue[1].split("."); document.main_form.list2.options[i+1] = new Option(sometext[1], somevalue[0]); if (sometext[1] == thelevel2) { document.main_form.list2.options[i+1].selected = true; } } return; } if (indicator == 3) { for (i=document.main_form.list3.options.length; i>0; i--) { document.main_form.list3.options[i] = null; } document.main_form.list3.options[0] = new Option("-- select --", ""); for (i=0; i<thevalues[list1index][list2index].length; i++) { somevalue = new Array(); somevalue = thevalues[list1index][list2index][i].split("|"); sometext = new Array(); sometext = somevalue[1].split("."); document.main_form.list3.options[i+1] = new Option(sometext[2], somevalue[0]); if (sometext[2] == thelevel3) { document.main_form.list3.options[i+1].selected = true; } } } }
}
function setrootcause(thenewvalue) { document.main_form.elements["SET.rootcause"].value = thenewvalue; } </source>
This code uses a pdm_list tag to get all rootcauses from the server and populate the allRootCauses javascript array.
After all code is parsed by the server, it should look like so:
<source lang="javascript"> allRootCauses[0] = "40001|Hardware.Server.Error"; allRootCauses[1] = "40002|Hardware.Server.Component Failure"; allRootCauses[2] = "40003|Hardware.Network.Firewall"; allRootCauses[3] = "40004|Hardware.Network.Router"; allRootCauses[4] = "40005|Hardware.Network.Cable"; allRootCauses[5] = "40006|Software.Browser.Settings"; allRootCauses[6] = "40007|Software.Browser.Version"; </source>
Then, the code loops through these root causes and converts them into an array of arrays much like so:
<source lang="javascript"> thevalues[0][0][0] = "40001|Hardware.Server.Error" thevalues[0][0][1] = "40002|Hardware.Server.Component Failure" thevalues[0][1][0] = "40003|Hardware.Network.Firewall" thevalues[0][1][1] = "40004|Hardware.Network.Router" thevalues[0][1][2] = "40005|Hardware.Network.Cable" thevalues[1][0][0] = "40006|Software.Browser.Settings" thevalues[1][0][1] = "40007|Software.Browser.Version" </source>
this will be easier to parse later on.
The readrootcause function fills 3 variables: thelevel1, thelevel2 and thelevel3, these are the text values that will be displayed in the read only version of the fields.
The fillDependantDropdown function is capable of filling the dropdown of list1, list2 or list3. While doing so, it takes a variable (indicator) to indicate the list you want to populate. It will take into account the selection made in a previous dropdown, as well as the values from thelevel1, thelevel2 and thelevel3, making sure that when you go to edit mode the value set from a previous save is displayed as the selected one in each dropdown.
The setrootcause function will trigger when you make a selection on the list3 dropdown. It will then set the hidden field (SET.rootcause) to the id of your selected rootcause (remember that the allRootCauses looks like id|level1;level2.level3, so this function will set that id into the SET.rootcause field so that it is saved into the back-end)
- In your HTMPL, before the dtlEndTable tag, add this:
<source lang="html4strict"> <PDM_MACRO NAME=dtlStartRow>
readrootcause();
detailRowHdr("LIST 1", 1, 0); if (_dtl.edit) { var htmlText; htmlText = "<select id=list1 name=list1 onchange=\"fillDependantDropdown(2)\"><option>-- select --</option></select>"; detailSetRowData(htmlText); } else { detailSetRowData(""+thelevel1); }
detailRowHdr("LIST 2", 1, 0); if (_dtl.edit) { var htmlText2; htmlText2 = "<select id=list2 name=list2 onchange=\"fillDependantDropdown(3)\"><option>-- select --</option></select>"; detailSetRowData(htmlText2); } else { detailSetRowData(""+thelevel2); }
detailRowHdr("LIST 3", 1, 0); if (_dtl.edit) { var htmlText3; htmlText3 = "<select id=list3 name=list3 onchange=\"setrootcause(this.options[this.options.selectedIndex].value)\"><option>-- select --</option></select>"; detailSetRowData(htmlText3); } else { detailSetRowData(""+thelevel3); }
</source>
This section adds the 3 dropdown fields to your form and makes sure that the saved rootcause values are set (by calling the readrootcause).
- on the BODY tag, make sure you add the setting of your dropdowns (for edit mode):
<source lang="html4strict"> <BODY class=detailro onLoad="loadActions();fillDependantDropdown(1);fillDependantDropdown(2);fillDependantDropdown(3)" onUnload="unloadActions()"> </source>
This will populate the dropdowns when in edit mode and make sure that the previously saved values are selected.
- also add the SET.rootcause field to you form (after <PDM_IF "$prop.form_name_3" == "edit">):
<source lang="html4strict"> </script> <input type=hidden NAME=SET.rootcause value="$args.rootcause"> </source>