Replacing Multi-select Lookup field with Checkboxes

The Situation

In SharePoint 2010, when you create a lookup field that allows for multiple selections, you end up with a very non-standard html widget that includes 2 boxes for moving “possible” values to “selected” values. Like so:

Out of the box Multi-select UI

Out of the box Multi-select UI

That’s all well and good if you have short values and a long list of them. But, if you happen to have long values and a short list of them, SharePoint’s UI choice is less than spectacular. As you can see in the image, most of the values are hidden in the select box. Marc Anderson has a function in his popular SpServices library to resize the select boxes to show the complete value. But, that’s not always preferable – as that can make your page very, very wide.

The Solution

Wouldn’t it be nice if you could just make a checkbox interface instead of the multi-select interface? Why, yes, yes it would. And now you can. There are a few moving parts here. I’ll try to break it down.

First, we need a way to select and unselect choices in the multi-select UI. It’s not as easy as just highlighting a value and using jQuery to “click” the add or remove button. Believe me, I tried that. Happily, I found someone else that grappled with this issue, and he got me most of the way there. I had to tweak a couple of things, because I had multiple multi-select fields on a single form, and because I know my users well enough to know that they’ll go crazy checking and unchecking things. So, here are my add choice and remove choice functions.

Add Choice

function addChoice(text, columnName) {

	// let's get the id from the column name
	var thisID = $("[title='" + columnName + " possible values']").prop('id');
	thisID = thisID.substr(0, thisID.indexOf('_SelectCandidate'));
	selector = "[id$=" + thisID + "_MultiLookupPicker]";
    $("[title='" + columnName + " possible values'] option").each(function () {   
        if ($(this).text() == text) {
            $(this).appendTo($("[title='" + columnName + " selected values']"));

            var multilookupPickerVal = $(selector).val();
            if ($(selector).val() == undefined || $(selector).val().length == 0) {
                $(selector).val($(this).val() + "|t" + $(this).text());
            }
            else {
                $(selector).val(multilookupPickerVal + "|t" + $(this).val() + "|t" + $(this).text());
            }
        }
    });
}

Remove Choice

function removeChoice(text, columnName) {
// let's get the id from the column name
	var thisID = $("[title='" + columnName + " selected values']").prop('id');
	thisID = thisID.substr(0, thisID.indexOf('_SelectResult'));
	selector = "[id$=" + thisID + "_MultiLookupPicker]";

	// loop through the selected options
    $("[title='" + columnName + " selected values'] option").each(function () {

        if ($(this).text() == text) {
            $(this).appendTo($("[title='" + columnName + " possible values']"));

            var multilookupPickerVal = $(selector).val();

            // call the internal function which creates a clean array out of the weird string
            var splitValue = GipSplit(multilookupPickerVal);

            // set the 2 array nodes we want to remove
            var valToRemove = $(this).val();
            var textToRemove = $(this).text();

            // Kill the value and text in the array
            splitValue = jQuery.grep(splitValue, function(value) {
        		return value != valToRemove;
      		});

      		splitValue = jQuery.grep(splitValue, function(value) {
        		return value != textToRemove;
      		});

      		var newValue = '';

      		// loop through the cleaned up array and rebuild the selector value
      		for (var i = 0; i < splitValue.length; i++) {
      			if (newValue.length == 0) {
      				newValue = splitValue[i];
      			}
      			else {
      				newValue = newValue + '|t' + splitValue[i];

      			}

			 } // end for loop

          	// set the new value to the selector
            $(selector).val(newValue);

        }
    });
}

Next, we need a function to draw the checkboxes to the screen. I wanted to make this as simple as possible, so this same function draws the checkboxes, hides the old UI, and attaches the remove and add choice functions to the click events of the checkboxes.

Draw Checkboxes

// function used to draw the checkboxes instead of a multi-select lookup
function drawCheckboxes(columnName) {

	// remove spaces from columnName
	var divName = columnName.split(' ').join('') + 'Checkboxes';

	// find the parent td, hide the span, clear the div
	$('div[id="' + divName + '"]').remove();
	$("[title='" + columnName + " possible values']").closest('span').after('<div id="' + divName + '">New div</div>');
	$("[title='" + columnName + " possible values']").closest('span').hide();
	var thisDiv = $('div[id="' + divName + '"]').html('');

	// loop through all the possible options and draw the checkboxes
    $("[title='" + columnName + " possible values'] option").each(function () {
        var thisText = $(this).text();
        var thisVal = $(this).val()
        var thisSnippet = "<input type='checkbox' name='" + columnName + "' value='" + thisText + "' id='" + columnName + "Checkbox" + thisVal + "'/><label for='" + columnName + "Checkbox" + thisVal + "'>" + thisText + "</label><br/>";

        thisDiv.append(thisSnippet);
    });

    // loop through all the selected options and draw the checkboxes
    $("[title='" + columnName + " selected values'] option").each(function () {
        var thisText = $(this).text();
        var thisVal = $(this).val()
        var thisSnippet = "<input type='checkbox' name='" + columnName + "' value='" + thisText + "' id='" + columnName + "Checkbox" + thisVal + "'/><label for='" + columnName + "Checkbox" + thisVal + "'>" + thisText + "</label><br/>";

        thisDiv.append(thisSnippet);
    });

    // Loop through the checklist for outcome statement lookup 

	var boxes = $('input[name="' + columnName + '"]');
	boxes.each(function(index) {

	$(this).click(function() { 
		// get the label

		if( $(this).is(':checked')){
			addChoice($(this).val(), columnName);
		}
		else {
			removeChoice($(this).val(), columnName);
		}
	});

	});  // End loop

} //end draw checkboxes

The basic solution

Great – now we have functions that will draw checkboxes and add and remove choices when the checkboxes are clicked. What do you do with it? First put all your scripts together in a text file and upload your text file to your site. (Note, there is one line you’ll need to edit in this text file.) (I typically use the site assets library.) On your page with your data form web part, you’ll need to include a content editor web part and link to your text file.  Run your page, and voila – checkboxes! (Note that in this image, I’ve shown both interfaces.)

Showing both the standard and checkbox UI.

Showing both the standard and checkbox UI.

Of course, around here, we’re integrating this with SPServices, to filter the multi-select. For that, you need a few more steps. First, you’ll need a copy of SPServices in your site, and you’ll need to include a reference to SPServices in your text file. Next, you’ll need to make a function that will work as a call-back to the SPServices.SPFilterDropdown or SPCascadeDropdowns function. Functions passed as callbacks can’t have parameters (at least, I haven’t figured out how to do it). So, you’ll need a wrapper function for drawCheckboxes that passes in the column you want. This is convenient because you can also do other stuff at the same time, if need be. Here’s my example, using a filter.

Wrapper Function

// need this function to pass a parameter-free function to the complete function of the dropdown filter
function drawProgramAreaCheckboxes() {
	drawCheckboxes("Collaborators Program Area");	
}
//end drawProgramAreaCheckboxes

Calling the SpServices Filter function with callback

//Filter and draw checkboxes for program areas
$().SPServices.SPFilterDropdown({
  relationshipList: "Program Area",
  relationshipListColumn: "Title",
  relationshipListSortColumn: "ID",
  columnName: "Collaborators Program Area",
  CAMLQuery: " 1",
  completefunc: drawProgramAreaCheckboxes,
  debug: true
});

//End program areas

Here’s the complete file for using checkboxes with SpServices. Note, there are multiple parts you’ll need to customize in here.

Editing Forms

Now that we have adding forms figured out, we need to do something about editing. It’s essentially the same, but you need to set the checkboxes that require setting. Again, for this, we’re using SPServices, and we need a few more function calls.

Step one – helper functions to set checkbox values

We need a couple of helper functions to set the checkboxes. The first sets a checkbox’s selected property to true based on the value of the checkbox.

function checkByValue(value) {

	$(":checkbox").filter(function() {
        return this.value == value;
    }).prop("checked", "true");

}

The second takes in a string as returned by spSpervices GetListItems for multi-select fields and the column name and loops through the string, setting each checkbox. The string returned is a series of ID’s and values, separated by semi-colons and hash tags. So, we’ll split the string on the ;# combo, and then check only when we have a value, not a number. Caveat – if your values ARE numbers, this won’t work for you, and you’d need to do check every other value, instead of the textual values.

function setMultiSelectValueCheckboxes(inString, column) {

 	var outString = inString.split(";#");

 	$.each(outString, function( index, value ) {

		  if (isNaN(value)) {

		  	checkByValue(value);
		  }
	});

} // end set MultiSelectValueCheckboxes

Step two – get the ID from the query string.

SpServices has a helper function that lets you get parameters from the query string. SharePoint always passes an ID parameter to edit forms. So, you just need to grab it, like so:

// This function from SPServices gets all of the Query String parameters 
  	var queryStringVals = $().SPServices.SPGetQueryString();
  	var effortID = queryStringVals.ID;

Step three – build the checkboxes

Just like in the add scenario, you’ll need to build your checkboxes. You want to build them before you try to check them, or this won’t work. For brevity, you can refer to the completed text file for this section, since it’s no different than the add form.

Step four- use GetListItems to get the thing you’re editing

My test list is called “Educational Effort.” Here I’m passing in a CAML query to get just the item we’re editing. I’ve returned more viewfields than you’d really need. But, you get the idea. If we have a successful status, we then set the value of the checkboxes, using our helper function from above.

// Get the related effort
		$().SPServices({
		    operation: "GetListItems",
		    async: true,
		    listName: "Educational Effort",
		    CAMLQuery: "" + effortID + "",
		    CAMLViewFields: "",
		    completefunc: function (xData, Status) {

				if (Status == 'success') {

				$(xData.responseXML).SPFilterNode("z:row").each(function() {

 				// get outcome statement and program area values
		        var outcomeStatementLookup = $(this).attr("ows_OutcomeStatementLookup");
		        var collaboratorsProgramArea = $(this).attr("ows_CollaboratorsProgramArea");

		        // set checkboxes
				setMultiSelectValueCheckboxes(outcomeStatementLookup, 'Outcome Statement Lookup'); 
 				setMultiSelectValueCheckboxes(collaboratorsProgramArea , 'Collaborators Program Area'); 

		      });	// end each function	
		      } //end if status = success.		
		    } // end complete function
		  }); // end spservices
// End get the related effort

And, voila – automatically checked checkboxes.

Edit page with auto-checked checkboxes.

Edit page with auto-checked checkboxes.

Here’s the complete file for an edit form. Again, note that you’ll need to customize for your environment.

As usual, feel free to point out places where this could be optimized, tweaked, etc. I’ve testing in IE 10, Chrome and Firefox, and it seems to be working for me.

About these ads
Leave a comment

11 Comments

  1. I must say, that is a much nicer user experience – and a better use of screen space – than the ridiculous way SharePoint handles multi-selects. Glad you were able to get it working!

    Reply
  2. Alez

     /  April 17, 2014

    Is it possible to use this also with already cascaded dropdowns? I’ve already implemented the cascaded dropdowns from the SPServices but now want to display additional fields from the list, so that the users have a better impression what they actually select (something like the SPDisplayRelatedInfo, which unfortunately doesn’t support multi-select).
    This solution sounds good, but in the DOM I only have the IDs and not the actual value. Is this somehow possible to realize?

    Reply
    • I’m not sure. I’ve not tried to do it with a cascaded dropdown. I have done with an already filtered dropdown (via spFilterDropDown), and you just have to run it as the complete function of the filter. So, perhaps a similar approach with a cascaded dropdown would work?

      Reply
  3. Excellent idea! I had a go with changing the multi select to a Select2 (http://ivaynberg.github.io/select2/). Ended up having to change from lookup to “single line of text” to get it working.

    Reply
  4. Shahul Hameed M

     /  May 8, 2014

    How Would I Implement the Cascading drop down for multi select dropdown

    Reply
    • You would need to run the draw check boxes script as the complete function of the spservices cascade call.

      Reply
  5. I’m new at this, so sorry if I’m being dim… I got this working fine for the new item form, but the edit item form is not coming up with selected values checked.

    I don’t understand how the column parameter passed to setMultiSelectValueCheckboxes(inString, column) is getting used. I feel I’m missing something about how this is working.

    Reply
  6. That you for this. I’m new at this, and this was the boost I needed to get this done and I learned a lot over the last few days. There are a couple of changes I made in my version that I think improve it…

    1. In function drawCheckboxes, in the second loop (through all the _selected_ options), add the “checked” attribute to the html for thisSnippet. Now you no longer need setMultiSelectValueCheckboxes and checkByValue to check the boxes on the page.

    2. Your drawCheckBoxes function reorders the checkboxes, always putting the checked boxes at the end. I prefer my checkboxes to be consistent. I modified the drawCheckboxes code in the following way:
    a. Add this line before the two loops:
    var ckBox= [];
    b. In each of the two loops, replace “thisDiv.append(thisSnippet);” with:
    ckBox.push(thisSnippet);
    c. After the second loop, add the following code:
    ckBox.sort();
    for (index=0; index < ckBox.length; ++index) {
    thisDiv.append(ckBox[index]);
    }

    This sorts the snippets by value, since the previous text in the snippet is exactly the same for all checkboxes. Make sure the "checked" attribute from 1 above is after the value (I put it as the last attribute in the input tag).

    Here's the entire replacement for the 2 loops in my version:

    var ckBox= [];

    // loop through all the possible options and draw the checkboxes
    $("[title='" + columnName + " possible values'] option").each(function () {
    var thisText = $(this).text();
    var thisVal = $(this).val()
    var thisSnippet = "” + thisText + “”;

    ckBox.push(thisSnippet);
    });

    // loop through all the selected options and draw the checkboxes
    $(“[title='" + columnName + " selected values'] option”).each(function () {
    var thisText = $(this).text();
    var thisVal = $(this).val()
    var thisSnippet = “” + thisText + “”;

    ckBox.push(thisSnippet);
    });

    ckBox.sort();
    for (index=0; index < ckBox.length; ++index) {
    thisDiv.append(ckBox[index]);
    }

    Reply
  7. To be clear, when I said it sorts the choices by value, I meant the html value attribute, which is the contents of thisText. If you want to sort by thisVal, you can simply reverse the order of the value and id attributes in the snippet.

    Reply
  8. Nice – thanks for that. We actually decided against this approach for a couple of other reasons and wound up purchasing a product that did the same thing server-side. But, having the sort stay consistent is definitely a big deal and one that I knew would need to be fixed at some point.

    Reply
  9. Also, I’m not using the GetListItems SPServices call for filtering. I’m using a bit of a kludge by having a hidden field with a copy of the main field, if the conditions are right. i.e. Calculated field: IF([itemisactive],[lookupfield],””).

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: