OpenClinica User Manual/LongLists

From Wikibooks, open books for an open world
Jump to: navigation, search

For response types such as the single-select, the RESPONSE_VALUES_OR_CALCULATIONS field in the Excel template contains a description of the options available to a user. The maximum number of characters allowed in this field is 4000. Whereas generally this will suffice, in some cases, when dealing with very long lists, it will not. On this page three methods are provided that explain how you can circumvent this problem. The first method can be applied when the long-list in the CRF is part of a non-repeating group. In this approach a non-OC single-select is added to the CRF and employed to fill a read-only OC text field. The second method describes how to deal with a repeating group. It involves finding the dynamically created ID, opening a new window with the long list and changing the value of a read-only text in the CRF based on the selected value. The final method is a slightly nicer version of the second method and applies a tooltip for the single-select.

The example used throughout this page is the Anatomical Therapeutic Chemical (ATC) classification, a list containing a code and a description of the medication.

Method 1: Long List in Non-repeating Group[edit]

In this approach the general idea is as follows:

  • Create an external XML file which describes your long list
  • Create an CRF which contains an OC text field and a non-OC single-select
  • Add some script to your CRF which:
    • Sets the text field to read-only
    • Reads the XML file and fills the non-OC single-select
    • Copies the selected value from the single-select to the text field

External XML[edit]

The external XML file should describe your list. To give an idea of what the XML file should look like, here's an example:

<?xml version="1.0" encoding="utf-8" ?>
<ATCList>
	<ATCCode Code="A02BC01">
		<Description>A02BC01 - Omeprazole</Description>
	</ATCCode>
	<ATCCode Code="A02BC02">
		<Description>A02BC02 - Pantoprazole</Description>
	</ATCCode>
   ...
</ATCList>

The example file was named "ATC_Codes.xml". The file has to be stored at your OC server, for example in the includes directory, to be able access it from your CRF.

How it works[edit]

XML has to contain a root element, which is just the name of the list, <ATCList>; it won't be used anywhere. Next, the elements have to be defined. ATCCode is the name of the element, which is used in JavaScript later on to find the element. Code is the actual code, which is basically one of the RESPONSE_OPTIONS_TEXT entries of your Excel. Description is the description visible to the user for this Code, similar to the RESPONSE_VALUES_OR_CALCULATIONS of your Excel.

The CRF[edit]

The CRF requires two items:

  • A text field, in the example called textOut
  • A non-OC single-select, in the example called myList

textOut is just a regular OC text field. To be able to find it in the script, it has to be surrounded by a span tag and given an identifier. The LEFT_ITEM_TEXT thus becomes: <span id=myOutput>textOut</span>

A non-OC single-select can be created by applying the HTML "select" tag and options added via the "option" tag:

<select id="myList">
   <option val="None"/>None
</select>

This creates a single-select with the identifier "myList" and it currently has one option called "None". A nice position for this drop down is to the right side of textOut, which can be done by pasting the HTML code in the RIGHT_ITEM_TEXT. So far, the CRF looks like this:

Method 1: initial CRF


The Script[edit]

All that remains to be done is add some script, which sets the textOut field to read only, reads the xml file, fills the single-select and copies the single-select value to the textOut field.

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script lang="Javascript">
   $.noConflict();
   jQuery(document).ready(function($) {
      var myField = $("#myList")
      var myOutputField = $("#myOutput").parent().parent().find("input");
      myOutputField.attr("readonly",true); 
 
      $.ajax({
         type: "GET",
         url: "includes/ATC_Codes.xml",
         dataType: "xml",
         success: parseXML
      });
 
      function parseXML(xml){
         $(xml).find("ATCCode").each(function(){
            myField.append($("<option />").val($(this).attr("Code")).text($(this).find("Description").text()));
         });
         myField.val(myOutputField.val());
      }
 
      myField.change(function(){
         myOutputField.val(myField.val());
         myOutputField.change();
      });
   });
</script>

One place to paste this is the RIGHT_ITEM_TEXT. Four items have to be changed to suit your specific case:

  • url: "includes/ATC_Codes.xml" has to contain the link to the location of your xml file
  • In the function parseXML, the "ATCCode", "Code" and "Description" have to match the tags you defined in your xml file

How it works[edit]

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script lang="Javascript">
   $.noConflict();

The first thing we do, is include jQuery 1.9.1. You could also choose to include OpenClinica's jQuery version. Furthermore, we prevent a possible conflict with OpenClinica's code by calling noConflict.

   var myField = $("#myList")
   var myOutputField = $("#myOutput").parent().parent().find("input");
   myOutputField.attr("readonly",true);

To get access to the non-OC single-select and the OC text field, two variables are defined, myField and myOutputField, using the previously defined identifiers. We then set myOutoutField to read-only, to prevent users from manually entering values.

      myField.change(function(){
         myOutputField.val(myField.val());
         myOutputField.change();
      });

When myField (the single-select list) changes, the value of the output field is set to the value of the single-select list. OC is then notified that the field was changed, by calling the field's change function. This ensures that the data is saved to the database when save is pressed.

   $.ajax({
      type: "GET",
      url: "includes/ATC_Codes.xml",
      dataType: "xml",
      success: parseXML
   });

Here, ajax is employed to read the xml file. The url points to the location of the file. When the file has been successfully read, the parseXML function is called.

   function parseXML(xml){
      //find every ATCCode and Description
      $(xml).find("ATCCode").each(function(){
         myField.append($("<option />").val($(this).attr("Code")).text($(this).find("Description").text()));
      });
      myField.val(myOutputField.val());
   }

In our XML file we defined "ATCCode". parseXML tries to find every instance of ATCCode in the xml file and for each of those entries it appends an option tag to the single-select myField. The option has a value, which is the value following the "Code" in the xml file, and a descriptive text, which is "Description" in the xml file. Finally, the current value of the single-select is set to the current value of the text field. This ensures that they are matched when a user enters the form after having previously saved the data.

Result[edit]

The CRF now looks like this:

Method 1: complete CRF


After uploading the XML file to your OC server and uploading your CRF to OC, this is what you should get:

Method 1: CRF in OC


Method 2: Long List in Repeating Group[edit]

There are two main reasons why repeating groups require a different approach:

  • The non-OC single-select won't appear in the repeating group as it isn't part of the template. Therefore another approach is required to provide the user with the long list.
  • Handling the identifiers is more complicated in repeating groups. In method 1, by applying the span tag to the OC text field we had a static link to the field of interest. With repeating groups the fields are dynamically generated and, therefore, the span tag approach no longer suffices.


In the second approach the general idea is as follows:

  • Create an external XML file which describes your long list
  • Create an CRF which contains an OC text field in a group
  • Add some script to your CRF which:
    • Sets the text field to read-only
    • Checks whether the new window has to be opened and, if so, opens the window
  • Create an external HTML file which contains the long list and
    • Reads the XML file and fills the non-OC single-select
    • Copies the selected value back to the OC form

The first step, creating the external XML file, is described in Method 1 and is identical.

The CRF[edit]

The CRF requires one item:

  • A text field, in our example called textOut

However, it becomes clearer if we add some additional fields in the group. Therefore we also add:

  • A text field, text1
  • A text field, text3

text1 is the first item, textOut the second item and text3 the third item in the repeating group. All items are in the same group, "MyGroup" in our example.

The Script[edit]

To open the new window which contains the single-select, some script has to be added to the CRF. One place to paste this would be at the LEFT_ITEM_TEXT.

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script lang="Javascript">
$.noConflict();
jQuery(document).ready(function($){
   function handlePopup(me){
      if(me.attr("ID")!=undefined){
         var myID = me.attr("ID");
         var gIndex = myID.indexOf(gName.toUpperCase());
 
         if(gIndex!=-1){
            var myParent = me.parent();
            var colNr = $(myParent).parent().children().index($(myParent)) + 1;
 
            if(colNr==aCol){
               atcID = myID;
               var handle = window.open("includes/ATC-Form.html");
            }
         }
      }
   }
 
   $("table.aka_form_table").on("click", ":input", function(){
      handlePopup($(this));
   });
 
   var aCol=2;
   var gName = "MyGroup";
   $("#srh").focus();
});
</script>

Three items have to be changed for your specific case:

  • window.open("includes/ATC-Form.html") has to contain the link to the location of html file (explained later on)
  • aCol=2 is the column number of the item for which the new window has to open
  • gName="MyGroup" has to contain the name of the repeating group (GROUP_LABEL) that contains the item for which the new window has to open

The CRF now looks like this:

Method 2: CRF


How it works[edit]

$("table.aka_form_table").on("click", ":input", function(){
   handlePopup($(this));
});

As the user can dynamically add new rows to the repeating group, we can no longer use a simple span tag to obtain the id of the input item of interest. Therefore we fire the click event for every input item. To ensure the event also fires for newly added rows, the "on" function is applied. Next, we call a function to handle our popup.

function handlePopup(me){
   if(me.attr("ID")!=undefined){
      var myID = me.attr("ID");
      var gIndex = myID.indexOf(gName.toUpperCase());
 
      if(gIndex!=-1){
         var myParent = me.parent();
         var colNr = $(myParent).parent().children().index($(myParent)) + 1;

For the item for which the event just fired, we first check whether it has an ID. If it does, we store the ID in the variable myID. As there may be multiple repeating groups in one CRF we need to ensure we open a new window for only the right item in the right repeating group. Luckily, OC bases the IDs in repeating groups on the GROUP_LABEL. Hence, if the identifier of our current item (myID) contains the GROUP_LABEL (gName) of the proper repeating group, we know that the item is in the proper group. This is done by applying the indexOf function. If the return value is -1, myID does not contain the gName string and the if fails. Furthermore the columnnumber of the item is determined. As the index function is 0-based, 1 is added.


if(colNr==aCol){
   me.attr("readonly",true);
   atcID = myID;
   var handle = window.open("includes/ATC-Form.html");
}

If the columnnumber also turns out to be the proper one, the item should open the new window. However, first the field is set to read-only to prevent manual editing and we store myID in the global atcID.

   var aCol=2;
   var gName = "MyGroup";
   $("#srh").focus();

As explained before, aCol and gName are used to ensure the new window opens only for items in the correct repeating group and the correct column. The final statement sets the intial focus of the page to the top save button. This prevents a user from manually editing our textOut field if it receives focus after loading instead of by a user clicking on the field.

The External HTML File[edit]

The external file is a simple page containing the list, which is dynamically filled, and a select button.

<html>
   <head>
      <title>Select ATC-code</title>
      <meta http-equiv="Expires" content="0"> <!-- disable caching -->
      <meta http-equiv="Content-type" content="text/html; charset=UTF-8"/>
      <meta http-equiv="X-UA-Compatible" content="IE=8" />
 
      <script src="jmesa/jquery-1.3.2.min.js"></script>
      <script type="text/javascript">
         jQuery(document).ready(function($) {
            $.ajax({
               type: "GET",
               url: "ATC_Codes.xml",
               dataType: "xml",
               success: parseXML
            });
 
         function parseXML(xml){
            $(xml).find("ATCCode").each(function(){
               field.append($("<option />").val($(this).attr("Code")).text($(this).find("Description").text()));
            });
         }
 
         $("#submit").click(function(){
            var atcElement = window.opener.document.getElementById(window.opener.atcID)
            if(atcElement.value!=$("#atc-list").val()){ 
               atcElement.value=$("#atc-list").val();
               atcElement.onchange();
            }
            close();
         });
 
         var field = $("#atc-list");
      });
      </script>
   </head>
 
   <body>
   <h1>Select an ATC-code</h1>
      <button type="button" id="submit">Select</button>
      <select id="atc-list"/></br>
   </body>
</html>

This form has to be stored on the OC server. In our case it is stored in the includes directory and called "ATC-form.html". Several items have to be changed to suit your specific list:

  • Descriptions concerning the ATC-Code (e.g. title, header, etc.)
  • src="jmesa/jquery-1.3.2.min.js" depends on the location of the html form. It it is placed in the includes directory on the server, you don't have to change it.
  • url: "ATC_Codes.xml" has to contain the link to the location of your xml file
  • In the function parseXML, the "ATCCode", "Code" and "Description" have to match the tags you defined in your xml file
  • id="atc-list" should reflect your list. If you change this, make sure you change all the "#atc-list" strings as well

How it works[edit]

The ajax and parseXML methods are explained in Method 1.

   <body>
   <h1>Select an ATC-code</h1>
      <button type="button" id="submit">Select</button>
      <select id="atc-list"/></br>
   </body>

The body of the HTML document contains some text, a button with ID "submit" and text "Select". Furthermore it contains an empty single-select with id "atc-list".


   $("#submit").click(function(){
      var atcElement = window.opener.document.getElementById(window.opener.atcID)
      if(atcElement.value!=$("#atc-list").val()){ 
         atcElement.value=$("#atc-list").val();
         atcElement.onchange();
      }
      close();
   });

When the user presses the button on the form, the global variable atcID, which was declared in the CRF script, is retrieved and this ID is used to find the actual field (atcElement). If the current value of the field is different from the value selected by the user, the value is updated. The onchange() is called to notify OC the value has to be saved if the save button is pressed. Finally, the window is closed.

Result[edit]

After uploading the HTML form and the XML file your OC server and uploading your CRF to OC, this is what you should get:

Method 2: CRF in OC before selecting option


Method 2: List in new window


Method 2: CRF in OC after selecting option


Method 3: Long List in Non-repeating Group Using Tool-Tips[edit]

In the second method we opened a new window to display the drop-down list. While the new window gives the programmer lots of freedom, a disadvantage is that new windows are sometimes blocked by browsers. A different approach would be to apply the tooltip included in OC. In this third method the general approach is as follows:

  • Create an external XML file which describes your long list
  • Create an CRF which contains an OC text field in a group
  • Add some script to your CRF which:
    • Reads the XML file and stores the contents in a variable
    • Sets the text field to read-only
    • Checks whether the tooltip has to be shown and, if so, shows the tooltip with the single-select
    • Fills the single-select
    • Copies the selected value from the single-select back to the form

The first step, creating the external XML file, is described in Method 1 and is identical; the second step, creating the CRF is described in the second method and is identical.

The Script[edit]

To create the tooltip with the single-select, some script has to be added to the CRF. One place to paste this would be the LEFT_ITEM_TEXT.

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script lang="Javascript">
$.noConflict();
   jQuery(document).ready(function($) {
      var stringData;
 
      $.ajax({
         type: "GET",
         url: "includes/ATC_Codes.xml",
         dataType: "xml",
         success: parseXml
      });
 
      function parseXml(xml){
         stringData = "<option val='None'/>None";
         $(xml).find("ATCCode").each(function(){
            stringData=stringData+"<option val='"+$(this).attr("Code")+"'/>"+$(this).find("Description").text();
         });
      }
 
      startTip = function(atcID){
         var s="<select id='myList' onchange = 'changeValue()' atcID="+atcID+">"+stringData+"</select>";
         Tip(s, CLICKSTICKY, true, CLOSEBTN, true, FOLLOWMOUSE, false, WIDTH, 700);
      }
 
      changeValue = function(){
         var myOutputID = $("#myList").attr("atcID");
         var myOutput = document.getElementById(myOutputID);
         var selectedVal = $('option:selected', "#myList").attr("val");
         if(myOutput.value !== selectedVal){
            myOutput.value = selectedVal;
            myOutput.onchange();
         }
         UnTip();
      }
 
      $("table.aka_form_table").on("click", ":input", function(){ 
         var atcID = $(this).attr("id");
         var myParent = $(this).parent();
         var colNr = $(myParent).parent().children().index($(myParent)) + 1;
         var groupIndex = atcID.indexOf(groupName.toUpperCase());
         if(groupIndex!=-1){
            if(aCol==colNr){
               $(this).attr("readonly",true);       
               atcDescrID = $(this).parent().parent().children(":nth-child(3)").find(":input").attr("id")
               startTip(atcID);
            }
         }
      });
 
      var aCol=2;
      var groupName = "MyGroup";
      $("#srh").focus();
   });
</script>

Four items have to be changed for your specific case:

  • url: "includes/ATC_Codes.xml" has to contain the link to the location of your xml file
  • aCol=2 is the column number of the item for which the new window has to open
  • groupName="MyGroup" has to contain the name of the repeating group (GROUP_LABEL) that contains the item for which the pop-up has to ben shown
  • WIDTH, 700; here the 700 should be the width you want your tooltip to have

How it works[edit]

$.ajax({
   type: "GET",
   url: "includes/ATC_Codes.xml",
   dataType: "xml",
   success: parseXml
});

Once again we employ ajax to parse our XML file.

 
function parseXml(xml){
   stringData = "<option val='None'/>None";
   $(xml).find("ATCCode").each(function(){
      stringData=stringData+"<option val='"+$(this).attr("Code")+"'/>"+$(this).find("Description").text();
   });
}

This time, however, we are creating a string called stringData which contains all our options and description.

$("table.aka_form_table").on("click", ":input", function(){ 
  var atcID = $(this).attr("id");
  var myParent = $(this).parent();
  var colNr = $(myParent).parent().children().index($(myParent)) + 1;
  var groupIndex = atcID.indexOf(groupName.toUpperCase());
  if(groupIndex!=-1){
     if(aCol==colNr){
        $(this).attr("readonly",true);       
        atcDescrID = $(this).parent().parent().children(":nth-child(3)").find(":input").attr("id")
        startTip(atcID);
     }
  }
});

Most of this was already explained in Method 2. However, atcID, which contains the identifier, is now stored locally. Furthermore we call startTip with this identifier.

startTip = function(atcID){
   var s="<select id='myList' onchange = 'changeValue()' atcID="+atcID+">"+stringData+"</select>";
   Tip(s, CLICKSTICKY, true, CLOSEBTN, true, FOLLOWMOUSE, false, WIDTH, 700);
}

The startTip function is what actually starts the tooltip. First we create a String "s", which defines the dropdown employed inside the tooltip. This select has the identifier "myList", which is later used to retrieve the selected value. Next the atcID is stored with the list, which is necessary to be able to find the identifier of the field on which was clicked later on. We then add the stringData, which is the complete string with all the options, which we generated in the parseXml function. The rest are tooltip options (http://www.walterzorn.de/en/tooltip/tooltip_e.htm). The first fixates the tooltip, the second adds a close button, the third prevents the tooltip from following the mouse and the last sets the width.

changeValue = function(){
   var myOutputID = $("#myList").attr("atcID");
   var myOutput = document.getElementById(myOutputID);
   var selectedVal = $('option:selected', "#myList").attr("val");

The final function is the changeValue function, which is called when the single-select's onchange event is fired. To find the field whence the output should be written, the atcID attribute is retrieved from the list. Next, the actual element is retrieved by calling getElementByID using the identifier. We retrieve the selected value by using the "val" attribute of the selected option.

   if(myOutput.value !== selectedVal){
      myOutput.value = selectedVal;
      myOutput.onchange();
   }
   UnTip();

To check whether the field actually needs to be changed, the field's current value is checked against the selected value. If they are not the same, the field is updated to the new value and the onchange function is called. Finally the tooltip is closed.

Result[edit]

After uploading the XML file your OC server and uploading your CRF to OC, this is what you should get:

Method 3: CRF in OC before selecting option


Method 3: CRF in OC showing tooltip


Method 3: CRF in OC after selecting option