Tuesday, December 18, 2007

Lookup List DropDowns in SharePoint

For today's absurdity in SharePoint, I give you lookup list dropdown handling in the SharePoint UI.

Try this little exercise. Create three custom lists. In the third list create a lookup field connected to the title column of the first list. Create a second lookup field connected to the title column of the second list. Create 19 records in your first list, then create 20 records in your second list. Try putting "one, two, three, etc" in the title field for the records you are creating in these first two lists.

Now, create a record in the third list and notice what the dropdowns look like:

Notice how they are different? This is what it looks like when you click down on the first one vs. clicking down on the second one:

I'm guessing the thought process was that once the number of records in a list gets to be large (read: 20 or more) then the display of a lookup field based on that list is going to look unruly on the screen. It's actually not a bad looking dropdown or a bad idea, except it's a total PITA when trying to read/manipulate the contents of the control in JavaScript.

In a nutshell, the steps to programmatically select an item in the prettied-up dropdown through JavaScript is to emulate a click event on the down arrow image causing the list to appear, and then to emulate a double-click on the item you want. That's right, when you are selecting an item in this prettied-up dropdown through the UI, you have to double-click.

That's actually a simplified version of what you have to do. The in-between steps are so obnoxious between figuring out if the control has been changed, finding all the control pieces it's using and changing their states that I went about about finding a shortcut and present it to you here.

I've created a JavaScript wrapper object I call the SharePoint LookupDropDown. Here is the code:

var SP = function() {}
SP.LookupDropDown = function(cn) // cn = "cell name"
{
    var cll = ydom.getElementsBy(function(el) { return(el.id.indexOf(cn) > -1); }, "td")[0];
    var btn, fld, sel, IsFarged = false;
    this.Value;

    if (typeof(yelm) == "undefined")
        throw("Error in SP.LookupDropDown: \'yelm\' is missing.");
    if (typeof(cll) == "undefined")
        throw("Error in SP.LookupDropDown: invalid CellName specified."); else cll = new yelm(cll);
    if (cll.getElementsByTagName("input").length == 1) IsFarged = true;
    if (IsFarged)
    {
        btn = cll.getElementsByTagName("img")[0];
        fld = cll.getElementsByTagName("input")[0];
        this.Value = fld.value;
        try { btn.click(); } catch(x) { }; // trapping -- btn may be hidden
        sel = ydom.get(fld.opt);
        try { sel.style.display = "none"; } catch(x) { } // trapping -- control may be hidden
    }
    else
    {
        sel = cll.getElementsByTagName("select")[0];
        this.Value = sel.options[sel.selectedIndex].value;
    }
    this.GetSelectId = function()
    {
        return(sel.id);
    }
    this.GetValue = function()
    {
        return(this.Value);
    }
    this.SetValue = function(val)
    {
        this.Value = val;
        if (IsFarged)
        {
            try { ShowDropdown(fld.id); } catch(x) { }
                // ShowDropDown is a function baked into SharePoint;
                // will throw an error when the controls are hidden
            sel = ydom.get(fld.opt);
            for (var i = 0; i < sel.options.length; i++)
            {
               if (sel.options[i].value == val) sel.selectedIndex = i; continue;
            }
            try { OptLoseFocus(sel); } catch(x) { }
                // OptLoseFocus is a function baked into SharePoint;
                // will throw an error when the controls are hidden
        }
        else
        {
            for (var i = 0; i < sel.options.length; i++)
            {
               if (sel.options[i].value == val) { sel.options[i].selected = true; continue; }
            }
        }
    }
}

This function sits on top of a namespace I've created called "SP". You can also see that I have a couple objects called "ydom" and "yelm". These are friendly names to two components of the Yahoo User Interface Library (which I use a ton and highly recommend), YAHOO.util.Dom and YAHOO.util.Element. I've left them here because it saves me a ton of time from coding their equivalent functions, but if you want to code around it manually or substitute your own equivalent functions from your favorite library be my guest.

To use this script include it on your page where you have one of these prettied-up dropdowns. In SharePoint Designer (or your favorite html editor) find the cell/<td> element holding the lookup dropdown and give it an ID, such as "LookupCell". Make sure you haven't added any other controls into this cell, either. In your own JavaScript code pass in this ID when you instantiate an instance of this object:

var MyLookupDropDown = new SP.LookupDropDown("LookupCell");

I elected to instantiate the wrapper this way (by using the ID of the enclosing cell/<td> element) because the ID of the actual dropdown is somewhat random and can change. So now you'll be able to get/set the value of the dropdown by simply calling the GetValue or SetValue methods:

MyLookupDropDown.SetValue("blahblahblah");
var TheValue = MyLookupDropDown.GetValue();

There is another method in there called GetSelectId which I don't use anymore, but you may find it helpful. It returns the SharePoint-assigned ID of the core select control for the prettied-up dropdown.

Where might you use this? Well, I'll tell you where I've used it. I tend to assign lookup fields to the ID field of the source list, which means a lookup dropdown doesn't look too hot on a page unless you're some sort of savant who knows which item they want to select based on their ID number alone.

So, for a custom edit form I'll add the lookup field to the page as I normally would, but then hide the row containing it by adding a "style='display:none'" attribute in the <tr> element. I then assign an ID to the enclosing cell/<td> element holding this lookup field and instantiate the wrapper object using this cell ID. I call this the "source" dropdown.

Then I'll add an old fashioned select element to the page and populate it either through some onerous JavaScript or binding it to a data source. I call this the "surrogate" dropdown. I then attach an "onchange" attribute to the surrogate, which calls the SetValue method of the source. Now, when I save the record the correct information is being saved and the dropdown lists looks the way I want it to look.

A couple notes here:

  1. This dropdown list tweaking only occurs in IE.
  2. Yes, this wrapper object does detect whether or not the dropdown has been tweaked by SharePoint and sets the value of the"source" object accordingly.

4 comments:

Andrea said...

Great post, however I don't understand ydom.get() and ydom.getelementsby().As far as i know they aren't javascript, where do they come from and how can I manage to use them?

Michael said...

Hi Andrea!

Wow, this is an oldie but goodie. Here's a quote from the post which answers your question:

"You can also see that I have a couple objects called "ydom" and "yelm". These are friendly names to two components of the Yahoo User Interface Library (which I use a ton and highly recommend), YAHOO.util.Dom and YAHOO.util.Element. I've left them here because it saves me a ton of time from coding their equivalent functions, but if you want to code around it manually or substitute your own equivalent functions from your favorite library be my guest."

This post is kind of old, and to be honest I haven't used the YUI library in a while so it may have changed a great deal (I use jQuery a LOT now).

Andrea said...

Yes, sorry for such a silly comment... The fact is I'm struggling with this for a long time; I really don't understand why firing the click event on the image that opens the dropdown fails, or calling the "ShowDropdown" function fails... I'm dealing with dropdowns that have more than 20 items, obviously. So I grabbed the dropdown control, but can't manipulate it. I really need it!

Michael said...

Andrea, shoot me an email at mavickers (at) lpsol (dot) com and I'll see if I can give you a hand.