Feed me, Seymour

whoami

Jonathan Groll lives in Cape Town (South Africa). He’s married to the delectable Shira and has two wonderful kids. He’s known to work as a software developer (and is completely passionate about open source software).

WARNING: This site holds hidden puns and needles!




Dynamically hide elements with javascript - SAP CRM 2007

by Jonathan on December 18th

In a recent SAP CRM 2007 implementation, we had the requirement to hide a view (“partial”) in response to a change in a dropdown value.

This is easily done using javascript code on the client side, and what’s more such a solution to the problem is easy to implement and is not really restricted to SAP CRM 2007 or even SAP web development in general.

This is what we wished to achieve – hide block ‘A’ on the following screen if the value in the category dropdown ‘B’ is anything other than “corporate brands”:

This can be done using the ABAP CRM framework, but there are several potential concerns:

  1. Components not previously enhanced may require enhancement
  2. Components are designed to be independent, and making one component rely on another (which we want to do here) involves creating some sort of shared attribute
  3. In order to suppress a view it will be necessary to re-render all views on the current frame and will involve an unnecessary round trip.

It was therefore decided to solve this problem using javascript code.

As SAP CRM 2007 is supported by Firefox (version 2, although version 3 will work with rendering glitches), it is possible to use the Firefox ‘Firebug’ extension to inspect a web page and determine the relationship between elements on screen and appropriate blocks of HTML.

From Firebug analysis (or if you have patience to do this manually) it was determined that this block of HTML code represents the Dropdown B:

<td class="th-ip-td1" style="overflow: hidden; width: 100%;">
<fieldset class="th-if-wrapper-onfocus" style="border: 0pt none ; margin-right: 3px;">
<input id="C14_W59_V60_ClassificationDdlb1" class="th-if th-if-icon" readonly="readonly" 
value="Corporate Brands" onkeydown="thtmlb_ddlbInputKeyDown(this,event);"
style="width: 100%;" size="16" onblur="thtmlb_inputBlur(this);"
onfocus="thtmlb_inputFocus(this);thtmlbSaveKeyboardFocus('C14_W59_V60_ClassificationDdlb1');"
onclick="thtmlb_inputClick(this,7);"/>
</fieldset>

The element we’re interested in detecting a change of value for is the input tag with id “C14_W59_V60_ClassificationDdlb1”

This block of HTML code represents the viewset area A that we will be hiding (ignoring a lot of the inner HTML):

<td class="th-gr-td" valign="top" rowspan="1" colspan="1">
<div id="C11_W43_V44_V48" excevt=""
intevt="c:C11_W43_V44_V48:C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do;" 
automode="true" tgt="" dhe="true" 
style="display: inline;">
<!-- Begin C11_W43_V44_V48 -->
<div id="C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do" class="th-ajax-area">
<!-- Begin C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do -->
<table ....

The element we’re interested in hiding is the div with id=“C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do”

In it’s simplest case, the following javascript, if inserted into the view somewhere after the thtmlb:gridCell tag that declares dropdown element B, does what is required – it will hide block A if the value of dropdown B changes to anything other than “Corporate Brands”:

<script for="ClassificationDdlb1" event=onchange type="text/javascript">
var dd = document.getElementById("C14_W59_V60_ClassificationDdlb1");
var hidee = document.getElementById("C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do");
if(dd.value != "Corporate Brands"){
  hidee.style.display = 'none';
}
else {
  hidee.style.display = 'inline';
}
</script>

Of course, we’re going to require something a little more complicated! The problem is that the id for the elements under consideration is not fixed – the C14_W59_V60 will be different for different roles and is affected by configuration changes. One way to solve this problem is to write a javascript function that will go through all elements and check for a regular expression match based on the contents of id. However the javascript function getElementById only accepts an exact match on id.

A custom function fuzzyElementSearch was created to find the element of interest (this function is contained within the file jonathan.js which will be listed afterwards), this is what the call to this function looks like:

<script src="/sap/bc/bsp/sap/crmcmp_ic_frame/scripts/common/jonathan.js" type="text/javascript">
</script>
<script for="ClassificationDdlb1" event=onchange type="text/javascript">

var dd       = fuzzyElementSearch("th-ip-td1","td",null,"input","ClassificationDdlb1", 0)[0];
var dd2      = dd[0];
var hide     = fuzzyElementSearch("th-gr-td","td", null, "div","productdetailviewset", 1)[0];
if ((hide) && (dd2) && (hide.length > 0)) {
  var hide2    = hide[0];

  if ((dd2.value) != "Corporate Brands") {
    hide2.style.display = 'none';
  }
  else {
    hide2.style.display = 'inline';
  }
}

</script>

The file jonathan.js contains the following javascript:-

function fuzzyElementSearch(classname, tagname, root, tag2, subbie, level) {

    // Call function from Rhino book that returns
    // an array of DOM elements that are members of the specified class,
    // have the specified tagname, and are descendants of the specified root.
    var elements = getElements(classname, tagname, root);

    // Find children (of tag2) for each of these elements
    var subfound = [];

    for(var i = 0; i < elements.length; i++) {
        var subchild  = elements[i];
        var grandkids = subchild.getElementsByTagName(tag2);
        if (grandkids) subfound.push(grandkids);
    }

    // search "level" deep for each of the above matches
    // for an element where the id contains the substring
    var pattern = new RegExp(subbie, "g");
    var matches = [];
    for(var i = 0; i < subfound.length; i++) {
        var matchee  = subfound[i];
        if (matchee.length > level) {
          var matchin = matchee[level];
          var matchid = matchin.id;
          var reslt = pattern.test(matchid);
          if(reslt == true) {
             matches.push(matchee);
          }
        }
    }

    // Note that we always return an array, even if it is empty
    return matches;


 // *** The following attribution applies to the rest of the code in this file
 // This code is from the book JavaScript: The Definitive Guide, 5th Edition,
 // by David Flanagan. Copyright 2006 O'Reilly Media, Inc. (ISBN #0596101996)
 // *** 

 /* Rest of code == EXAMPLE 15-4 From David Flanagan, "Javascript"
 * getElements(classname, tagname, root):
 * Return an array of DOM elements that are members of the specified class,
 * have the specified tagname, and are descendants of the specified root.
 *
 * If no classname is specified, elements are returned regardless of class.
 * If no tagname is specified, elements are returned regardless of tagname.
 * If no root is specified, the document object is used.  If the specified
 * root is a string, it is an element id, and the root
 * element is looked up using getElementsById()
 */
    function getElements(classname, tagname, root) {
        // If no root was specified, use the entire document
        // If a string was specified, look it up
        if (!root) root = document;
        else if (typeof root == "string") root = document.getElementById(root);

        // if no tagname was specified, use all tags
        if (!tagname) tagname = "*";

        // Find all descendants of the specified root with the specified tagname
        var all = root.getElementsByTagName(tagname);

        // If no classname was specified, we return all tags
        if (!classname) return all;

        // Otherwise, we filter the element by classname
        var elements = [];  // Start with an emtpy array
        for(var i = 0; i < all.length; i++) {
            var element = all[i];
            if (isMember(element, classname)) // isMember() is defined below
                elements.push(element);       // Add class members to our array
        }

        // Note that we always return an array, even if it is empty
        return elements;

        // Determine whether the specified element is a member of the specified
        // class.  This function is optimized for the common case in which the
        // className property contains only a single classname.  But it also
        // handles the case in which it is a list of whitespace-separated classes.
        function isMember(element, classname) {
            var classes = element.className;  // Get the list of classes
            if (!classes) return false;             // No classes defined
            if (classes == classname) return true;  // Exact match

            // We didn't match exactly, so if there is no whitespace, then
            // this element is not a member of the class
            var whitespace = /\s+/;
            if (!whitespace.test(classes)) return false;

            // If we get here, the element is a member of more than one class and
            // we've got to check them individually.
            var c = classes.split(whitespace);  // Split with whitespace delimiter
            for(var i = 0; i < c.length; i++) { // Loop through classes
                if (c[i] == classname) return true;  // and check for matches
            }

            return false;  // None of the classes matched
        }
    }
}

The function fuzzyElementSearch accepts five parameters, and is used to find the element based on it’s relation to an an outer element. In other words, we find the element we’re interested in based on the context of the surrounding HTML code.

So, if we consider the case where we are looking for the element that represents viewset A that we want to hide:

<td class="th-gr-td" valign="top" rowspan="1" colspan="1">
<div id="C11_W43_V44_V48" excevt=""
intevt="c:C11_W43_V44_V48:C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do;" 
automode="true" tgt="" dhe="true" style="display: inline;">
<!-- Begin C11_W43_V44_V48 -->
<div id="C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do" class="th-ajax-area">
<!-- Begin C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do -->
<table ....

The outer element in this case is <td class="th-gr-td" valign="top" rowspan="1" colspan="1">
The inner element (which is what we want in the end) will be <div id="C1_W1_V2_C1_W1_V2_V3_C11_W43_V44_C11_W43_V44_V48_productdetailviewset.do" class="th-ajax-area">

The following five parameters supplied to the fuzzyElementSearch are what the function uses to robustly identify the element we want:-

  • classname = the CSS class of the outer element, or th-gr-td in this case.
  • tagname = the tag type of the outer element, td in this case
  • root = the root element from which we start searching. We’ll search the entire document so can leave this as null
  • tag2 = the tag type of the inner element, or div in this case
  • subbie = substring (actually a regular expression string) that will match the id of the inner element
  • level = number of levels down from outer tag to inner tag. In the above case, we are interested in the 2nd div tag nested inside the td tag. Since javascript counting starts at 0 the level we are interested in is level 1.

Putting it all together, the following call to the fuzzyElementSearch gives us the inner element we want every time (it will be the first item in the array returned):

var hide     = fuzzyElementSearch("th-gr-td","td", null, "div","productdetailviewset", 1)[0];

How does fuzzyElementSearch work? It makes use of a function getElements that is from David Flanagan’s book “Javascript” (ISBN 0-596-10199-6, also known as the Rhino book) – this function allows us to search for elements based on class and/or tag name. With the results of this function similar code is used to find all the children of these elements that have the correct tag and regular expression match. I heartily endorse David Flanagan’s book, and if you are interested in learning javascript then consider visiting the book’s web site.

Add a comment:

name
email
not displayed
homepage
not displayed
 
Name and either email or homepage required.