SPCascadingDropdowns() - Problem with Child Column of type Multi-Select-Hybrid

Feb 15, 2012 at 4:20 PM

Mark,

I encountered a problem with SPCascadeDropdowns() (in production) when the Child column is a Multi-Select-Hybrid (in your code; Type=M) and it has selected values.  When both of these conditions are true, Sharepoint's groupeditempicker.js throws an exception at line 380 indicating that .data.lenght is invalid.

My setup

  • SP 2007
  • jQuery 1.6.4
  • SPServices 0.6.2

I walked through your code and found the problem and applied a solution that seems to be working. Although I'm using an old version of SPServices (I'm ashamed.. but it take time for me to upgrade productive applications), I did review the code in 0.7.1a and it seems to be present there  as well... I can setup a simpler (from my production usage) on a test site using 0.7.1a if you are unable to recreate.

From the 0.7.1a code, the problem is on line 1534 where you call GipRemoveSelectedItems(master);... this function (located in groupeditempicker.js) does the following:

GipRemoveSelectedItems() (below on line 1653) calls
        GipSelectCandidateItems() who calls
               GipSelectCandidateItems() whow then calls
                        GipDisplayDescription() which attempts to use master.data[] (note: an array) and throws exception (line 380 of SP's groupeditempicker.js.

Your code sets master.data to an empty string on line 1520...  If I read the code correctly in groupeditempicker.js, this seems to happen only if the Multi-Select  hybrid field has selected items.

The fix I applied to my version of SPServices was to move the "master.data = GipGetGroupData(newMultiLookupPickerdata);" up and before you call GipRemoveSelectedItems(). Here is the change I made (again, to v.0.6.2 of SPServices):

 

[[ Starting around line 1629, inside of cascadeDropdown() ]]

case "M": MultiLookupPickerdata.attr("value", newMultiLookupPickerdata); // Clear any prior selections that are no longer valid $(currentSelection).find("option").each(function() { var thisSelected = $(this); $(this).attr("selected", "selected"); $(childSelect.Obj).find("option").each(function() { if($(this).html() === thisSelected.html()) { thisSelected.attr("selected", ""); } }); }); // PT: Moved the setting of master.data here (from linke 1655) // because: // GipRemoveSelectedItems() (below on line 1653) calls // GipSelectCandidateItems() who calls // GipSelectCandidateItems() whow then calls // GipDisplayDescription() which attempts to // use master.data[] and throws exception (line 380 of SP's // groupeditempicker.js. master.data = GipGetGroupData(newMultiLookupPickerdata); GipRemoveSelectedItems(master); // Hide any options in the candidate list which are already selected $(childSelect.Obj).find("option").each(function() { var thisSelected = $(this); $(currentSelection).find("option").each(function() { if($(this).html() === thisSelected.html()) { thisSelected.remove(); } }); }); GipAddSelectedItems(master); // Set master.data to the newly allowable values // PT: Commented out code below and moved it above, to line 1641. // master.data = GipGetGroupData(newMultiLookupPickerdata); // Trigger a dblclick so that the child will be cascaded if it is a multiselect. childSelect.Obj.trigger("dblclick"); break;

--

Paul

 

Coordinator
Feb 15, 2012 at 5:04 PM

Paul:

Great sleuthing. I haven't seen this in any of my testing. I wonder what might be unique about your setup? Does the function work, even though it throws an error? What action is the user taking when you see the error? (e.g., loading the page, adding a new multi-select value, removing one...)

I have a setup where the parent is State and the child is City. The City column is a multi-select, and I've hit it every which way and can't see an issue. There must be something I'm missing.

M.

Feb 15, 2012 at 5:28 PM

Mark,

Thanks Makr.

In my case, I "hide" then entire .aspx page ($("form:first").css("display", "none") is hidden) and only make it visible to the user after the setup of the dropdowns is done... Because the js exception is thrown, my code never executes and makes the page visible.

On your test case, does the City field have selected values?

In my case, the user had selected some values (1) when the Row was first recreated... the problem was seen when doing an EDIT on that item.

I'll try to setup a simpler test case in a test environment to see if I can recreate it there...

Paul

Coordinator
Feb 15, 2012 at 5:40 PM

Yes, I have tested it with and without values set on the EditForm. If you could isolate it more, that would be great.

Thanks,
M.

Feb 16, 2012 at 10:47 PM
Edited Feb 16, 2012 at 10:48 PM

Mark,

I have been able to recreate this again on a test site with simpler setup than my production environment... the error (in Firefox) does not buble out to the browser because SP seems to use it's own method to capture exception (they bind a function to window.onerror)... It is "visible" in IE and the debuger.  I used v0.7.1.a to recreate this.

The problem also only happens when you first load the EDIT page; after that, the changing of the field values does not seem to case a problem. Here is my simpler test case that is able to recreate this problem consistently:

States List:

  • NJ
  • MA

Cities List:

  1. NJ | Newark
  2. NJ | Trenton
  3. MA | Boston

On a new list, that includes a State and City column, both Lookup columns to the respectic tables above, I setup cascading from States to Cities on the Edit/New pages... I create a new item, and select MA for State, then pick Boston from City. *Important* Boston is the only value available for selection of City.  After saving/creating the item, I Edit it and get the "error" condition.

I can validate that there is a problem by placing an alert("setup done") right after calling CascadingDropdowns(), which was never triggered. The selected value for City (that was selected during the creation of the new row) is also not on the right side (selected) of the multi-select field.

Here is the code:

 

$(document).ready(function(){

    $().SPServices.SPCascadeDropdowns({
        relationshipList: "Customer",
        relationshipListParentColumn: "Product",
        relationshipListChildColumn: "Title",
        parentColumn:   "product",
        childColumn:    "customer",
        debug:          true,
        completefunc:   function(){
            
        } 
    });
    alert("Done with cascade setup");

});

 

I hope this helps you recreate the problem and hope that the solution I suggested is the right one (I'm moving forward with it in the next Mtc. release of the production app.)

 

Paul

 

(FYI... IN Firefox, there actually is another way to force the error to come out in the console: in Firebug, I placed a conditional stop point in groupeditempicker.js, line 381 that would break if group === undefined. If execution was halted there, then I know the exception was going to be trown... I ran the following in the console to set back the default .onerror event handling: window.onerror = SPOnError_cachedOriginalOnError... After this, I let the continuation of the script go and the exception gets logged to the firebug console.  I know... probably too much info.)

Coordinator
Feb 17, 2012 at 3:02 AM

Oh, if everyone were as thorough as you! I'll try to reproduce it and get a fix into the next alpha. This issue has me eyeballing a few other things now that I'm in the bowels of the function again.

M.

Feb 29, 2012 at 6:39 PM

Marc,

Have you had a chance to look at my proposed fix on this (to move a line of code to be executed earlier in the flow of the program)... Just looking to see if you feel there is any downside to it.

Thanks

Paul

Coordinator
Feb 29, 2012 at 7:33 PM

Paul:

To be honest, I haven't looked into it yet. I've been traveling, and as you've seen I'm way behind on SPServices stuff.

M.

Mar 1, 2012 at 2:35 AM

No problem Marc. I'm getting ready to finally making a fix available and figured I would check. Thanks.

Paul

Sent from mobile

Apr 16, 2012 at 11:55 PM
Edited Apr 17, 2012 at 12:12 AM

Marc, I wanted to update this post and say that Paul's fix worked for me. I was experiencing the exact same results as him (On edit, the cascade worked, but the saved selected items did not show in the "selected" box). 

I moved the master.data = ... line as Paul suggested and pushed the changes to the server. It is now functioning properly. 

 

Thanks for all your hard work! This is a wonderful library!

 

Update: I just retested this with multiple selections on item create and it does not show all of my selections on the right. It only shows the last selection (example: 1, 2, 3, selected on creation, only 3 visible on edit). It works every time with a single selection, but multiple seems to break it again. I hope this helps. 

Apr 17, 2012 at 12:49 AM
Thanks for posting. I will try this again on the test setup I made just for this item. Don't remember now if I tested it with multiple items selected on the child multi select lookup field.

_____
Paul

Sent from mobile device.
Apr 17, 2012 at 6:33 AM

I haven't been able to fix this yet, I've tried the simpleChild: true setting and I didn't see any changes. I even tried matchOnId: true out of desperation lol. I'll start looking into the code tomorrow to see why only the last item is being displayed. 

Where is the method, 

 

GipRemoveSelectedItems();

 

located in the js file? I don't see it defined anywhere.  

Apr 17, 2012 at 2:18 PM

jrose,

I was not able to recreate the problem with multiple items...  In your use case, are there any items left on the LEFT select box? or are they all "selected" and thus on the RIGHT select box?

GipRemoveSelectedItems is in groupeditempicker.js (a sharepoint .js file)

Paul.

 

Apr 17, 2012 at 3:14 PM

Paul,

It doesn't seem to matter how many I select from the left, just as long as there are more than 1 items selected in the right. I re-tested with the following scenarios:

Child Mulit-Select 1:

A, B, C, D

Child Multi-Select 2:

1, 2, 3, 4

I select A, B from 1 and 1, 2 from 2, save the item. 

If I edit the item, all I see is B in 1 (right) with A, C, D in left and 2 in 2 (right) with 1, 3, 4 in left. 

The child Multi-Select's are filtered and cascading properly, for both forms and I can go to the display page and see all the values that I selected correctly (A;B; in 1 and 1;2; in 2). 

I'm using I'm using SPServices 0.7.1a and jQuery 1.7.2

I've seen some people go back to jQuery 1.6.2 and say they see it working. Is that what version you're using?

Apr 17, 2012 at 3:54 PM

In the test site, I'm using SPServices 0.7.1a... jQuery 1.6.4...

Can you describe your field setup a little more... Is your parent column a Lookup (single) select and your child column a Lookup Multi-Select, correct?

I'll try to play a little more later to see if I can recreate it.

Paul.

Apr 17, 2012 at 4:24 PM

Paul,

My parent is a single lookup on a list with only the Title Column. 

My 1st Multi-Select contains a Title Column and a lookup column to my parent list's Title column. 

My 2nd Multi-Select contains a Title Column and a lookup column to my parent list's Title column. 

 

It is very similar to Marc's Country, State, City setup. Here is how i have it setup:

 

            // Setup the cascading from Parent Lookup field to Multiselect 1 field
            $().SPServices.SPCascadeDropdowns({
                relationshipWebURL: "/sites/Test", <-- This is used because my form is in a subsite and the lookup list is in the root site
                relationshipList: "Multiselect 1",
                relationshipListParentColumn: "ParentLookup",
                relationshipListChildColumn: "Title",
                parentColumn: "Parentlookup",
                childColumn: "Multiselect 1",
                matchOnId: true,
                debug: true
            });

            // Setup the cascading from Parent Lookup field to Multiselect 2 field
            $().SPServices.SPCascadeDropdowns({
                relationshipWebURL: "/sites/Test", <-- This is used because my form is in a subsite and the lookup list is in the root site
                relationshipList: "Multiselect 2",
                relationshipListParentColumn: "ParentLookup",
                relationshipListChildColumn: "Title",
                parentColumn: "Parentlookup",
                childColumn: "Multiselect 2",
                matchOnId: true,
                debug: true
            });

 

 

Apr 17, 2012 at 4:40 PM

Paul,

FYI, I just tested it with jquery 1.6.4 and have the same issue. 

Apr 17, 2012 at 5:31 PM
Edited Apr 17, 2012 at 5:41 PM

I noticed the following code section within the cascadingDropdowns method (Line 1522):

case "M":
    // Find the important bits of the multi-select
    MultiLookupPickerdata = childSelect.Obj.closest("span").find("input[name$='MultiLookupPicker$data']");
    master = window[childSelect.Obj.closest("tr").find("button[id$='AddButton']").attr("id").replace(/AddButton/,'MultiLookupPicker_m')];
    currentSelection = childSelect.Obj.closest("span").find("select[ID$='SelectResult'][Title^='" + opt.childColumn + " ']");
    // Clear the master    
    master.data = "";

    MultiLookupPickerdata.attr("value", newMultiLookupPickerdata);
    // Clear any prior selections that are no longer valid					

    $(currentSelection).find("option").each(function() {
        var thisSelected = $(this);
        var thisValue = $(this).html();
        $(this).attr("selected", "selected");
        $(childSelect.Obj).find("option:contains('" + thisValue + "')").each(function() {
            if($(this).html() === thisValue) {
	        thisSelected.removeAttr("selected");
	    }
        });
    });


What I noticed is that after this code block runs, the $(currentSelection) options are still set to true for the Selected attribute. However, the $(childSelect.Obj) options are set to false for the Selected attribute. I may be misunderstanding what the code is supposed to do, but it looks like it should cycle through the current selection (2 in my case), find a matching option in the $(childSelect.Obj) element, and if a match is found, remove the selected attribute from the $(currentSelection) option element. I'm actually seeing the opposite and this is with jQuery 1.6.4.


Also, when I look at the master.data after this call and compare that with the master.data before the above code I see:

Before: master.currentSelection = [1,1 - A,2,2 - B]

After: master.currentSelection = [1,1 - A,2,2 - B]

Before: master.data = [ ,1,1 - A, ,2,2 - B, ,3,3 - C, ,4,4 - D, ]

After: master.data = [ ,1,A, ,2,B, ,,,,]

Before: master.initialSelection = [1,1 - A,2,2 - B]

After: master.initialSelection = [1,1 - A,2,2 - B]

The 1 - A is now just A (Same with B), and now there are 4 extra commas after the last value. I think this is the issue, but I'm still trying to wrap my head around the big picture of how this works.

Apr 17, 2012 at 6:36 PM

I think I figured it out. 

$(currentSelection) contains options that have a number and the text for the .html() value (Ex: 1 - A). The actual option html looks like this:

"<OPTION title=\"A\" selected value=1>1 - A</OPTION>"

$(childSelect.Obj) contains options that do not have a number and the text for .html() value (Ex: A). This actual option html looks like this:

"<OPTION title=A value=1>A</OPTION>"

Therefore, the code:

$(childSelect.Obj).find("option:contains('" + thisValue + "')").each(function() {
            if($(this).html() === thisValue) {
	        thisSelected.removeAttr("selected");
	    }
        });
never removes the "selected" attribute from thisSelected because $(childSelect.Obj) options do not contain the "1 - A", it only contains "A". 

Likewise, the code following:

 

// Set master.data to the newly allowable values
master.data = GipGetGroupData(newMultiLookupPickerdata);
GipRemoveSelectedItems(master);
// Hide any options in the candidate list which are already selected
$(childSelect.Obj).find("option").each(function() {
    var thisSelected = $(this);
    $(currentSelection).find("option").each(function() {
        if($(this).html() === thisSelected.html()) {
	    thisSelected.remove();
	}
    });
});
GipAddSelectedItems(master);

thisSelected.remove() will not be called because it does not contain "A", it would be "1 - A". I say would be because in my debugger, $(currentSelection) does not contain any childNodes any more. Somewhere between the first time it is called (Line: 1524) and the 2nd time (Line: 1541), it lost it's children. 

When the multi-select values are shown on the form, they do not display as "1 - A", they display as "A". I'm thinking this is done when the lookup column in SP retrieves it's values initially (ItemID - Title) to keep the selections unique. Somehow, these are not being consistently translated. 

I hope this is helping you. 

Coordinator
Apr 17, 2012 at 6:46 PM

jrose:

I'm watching the thread, but I haven't had a chance to look at it yet. I think you are using v0.7.1a. Could you also test the same thing with v0.7.0 and let me know if you see anything different?

Thanks,
M.

Apr 27, 2012 at 6:44 AM

I'm sorry it has taken me a while to get back to this thread, but I've done some more testing and researching.

I am provisioning my Lookup fields via field definitions in Visual Studio. I create my lookup fields similar to this:

 

<Field ID="{F9E3BD77-AEC6-4CF2-A231-868C64120CFD}"
         Name="My_LookupField"
         DisplayName="My Lookup Field"
         Group="Columns"
         Type="LookupMulti"
         Mult="TRUE"
         List="Lists/MyList"
         ShowField="Title"
         PrependId="TRUE"
         Required="FALSE"
         DisplaceOnUpgrade="TRUE"
         SourceID="{655b3843-a6eb-4355-85bf-722e11c14c59}" />

 

Notice the PrependId attribute in my definition. This causes the Candidate options to look like this: 1 - Option1, 2 - Option2, etc. In the cascadeDropdown function, you are rebuilding the master data:

 

// ows_Title does not contain the Id like the Candidate and SelectResult fields do by default.
// Instead of 1 - OptionA, ows_Title returns just OptionA
var thisValue = $(this).attr("ows_" + opt.relationshipListChildColumn);

// Lets say the thisValue = "OptionA"
var thisValue = $(this).attr("ows_" + opt.relationshipListChildColumn);  
// It skips the split() block
if(thisValue !== undefined && thisValue.indexOf(";#") > 0) { 
  var splitValue = thisValue.split(";#"); 
  thisOptionId = splitValue[0]; 
  thisOptionValue = splitValue[1]; 
} 
else { 
  // thisOptionId will now be 1
  thisOptionId = $(this).attr("ows_ID"); 
  // thisOptionValue will now be OptionA
  thisOptionValue = thisValue;  
}

// A little further on down
case "M":				
   // <option value='1'>OptionA</option>				
   childSelect.Obj.append("<option value='" + thisOptionId + "'>" +    thisOptionValue + "</option>");		
   // 1|tOptionA|t |t |t						
   newMultiLookupPickerdata += thisOptionId + "|t" + thisOptionValue + "|t |t |t";
   break;

 

By doing so, this is creating a different option and data string. The original option in the Candidate was <option value="1">1 - OptionA</option>. However, we now end up with a new option; <option value="1">OptionA</option> in our childSelect.Obj options. Now, a little further on down:

 

case "M":
   // Find the important bits of the multi-select
   MultiLookupPickerdata = childSelect.Obj.closest("span").find("input[name$='MultiLookupPicker$data']");
   master = window[childSelect.Obj.closest("tr").find("button[id$='AddButton']").attr("id").replace(/AddButton/,'MultiLookupPicker_m')];
   currentSelection = childSelect.Obj.closest("span").find("select[ID$='SelectResult'][Title^='" + opt.childColumn + " ']");

   // Clear the master
   master.data = "";

   MultiLookupPickerdata.attr("value", newMultiLookupPickerdata);
   // Clear any prior selections that are no longer valid					
   $(currentSelection).find("option").each(function() {
      var thisSelected = $(this);
      // I changed this to use .val() instead of .html()
      var thisValue = $(this).val();
      $(this).attr("selected", "selected");
      // I changed this to .find().each() instead of a contains
      $(childSelect.Obj).find("option").each(function() {
         // I changed this to use .val() instead of .html()
         if($(this).val() == thisValue) {
            thisSelected.removeAttr("selected");
         }
      });

   // Removed the following code because:
   // currentSelection.option.html() is actually Value - Text (Ex: 1 - OptionA and 2 - OptionB)
   // childSelect.Obj.option.html() is actually Text (Ex: OptionA and OptionB)
   // If the options in childSelect are [OptionA, OptionB, OptionC] and currentSelection are [1 - OptionA, 2 - OptionB, 3 - OptionC],
   // childSelect will never find an option that contains the currentSelection option 

   //$(childSelect.Obj).find("option:contains('" + thisValue + "')").each(function() {
   //	if($(this).html() === thisValue) {
   //      thisSelected.removeAttr("selected");
   //   }
   //});
   });

   // Set master.data to the newly allowable values
   master.data = GipGetGroupData(newMultiLookupPickerdata);

   // GipRemoveSelectedItems() will take any options in the _SearchResult field (Selected Values) that are marked as "selected"
   // and move them to the _Candidate field (Possible Values)
   GipRemoveSelectedItems(master);

   // Hide any options in the candidate list which are already selected
   $(childSelect.Obj).find("option").each(function() {
      var thisSelected = $(this);
      $(currentSelection).find("option").each(function() {
         // I changed this to use the values instead of the html()
         if($(this).val() == thisSelected.val()) {
            thisSelected.remove();
         }
      });

   // Removed the following code because:
   // currentSelection.option.html() is actually Value - Text (Ex: 1 - OptionA and 2 - OptionB)
   // childSelect.Obj.option.html() is actually Text (Ex: OptionA and OptionB)
   // If the options in childSelect are [OptionA, OptionB, OptionC] and currentSelection are [1 - OptionA, 2 - OptionB, 3 - OptionC],
   // childSelect will never match an option in the currentSelection options

   //$(currentSelection).find("option").each(function() {
   //   if($(this).html() === thisSelected.html()) {
   //      thisSelected.remove();
   //   }
   //});
   });
   
   // GipAddSelectedItems() will take any options in the _Candidate field (Possible Values) that are marked as "selected"
   // and move them to the _SearchResult field (Selected Values)
   GipAddSelectedItems(master);

   // Set master.data to the newly allowable values
   //master.data = GipGetGroupData(newMultiLookupPickerdata);

   // Trigger a dblclick so that the child will be cascaded if it is a multiselect.
   childSelect.Obj.trigger("dblclick");
   break;

If you look at the commented out code (the original) you will see that I changed it to use .val() instead of .html() and I removed the .find("option:contains..") to .find("option").each(). By changing this code I can now update, save, and reload the multiselect boxes without loosing any data at all. 

I also went back and turned PrependId to false and retested. Sure enough, it works fine. So I got to thinking about that and I updated the code to support the PrependId option. Here it is:

 

// In $.fn.SPServices.SPCascadeDropdowns

// Get information about the childColumn from the current list
$().SPServices({
	operation: "GetList",
	async: false,
	listName: opt.listName,
	completefunc: function(xData, Status) {
		$(xData.responseXML).find("Fields").each(function() {
			$(this).find("Field[DisplayName='" + opt.childColumn + "']").each(function() {
				// Determine whether childColumn is Required
				childColumnRequired = ($(this).attr("Required") === "TRUE") ? true : false;
				childColumnStatic = $(this).attr("StaticName");
				// Jeremy Rose - 4/27/2012: Add this new variable to hold the PrependId value
                                childColumnPrependId = $(this).attr("PrependId");
				// Stop looking; we're done
				return false;
			});
		});
	}
});

// Save data about each child column on the parent
// Jeremy Rose - 4/27/2012: Add Prepend variable to column data
var childColumn = {opt: opt, childSelect: childSelect, childColumnStatic: childColumnStatic, childColumnRequired: childColumnRequired, childColumnPrependId: childColumnPrependId};

// In function cascadeDropdown

function cascadeDropdown(parentColumn, parentSelect) {
	var choices = "";
	var parentSelectSelected;
	var childSelectSelected = null;
	var master;
	var MultiLookupPickerdata;
	var newMultiLookupPickerdata;
	var numChildOptions;
	var firstChildOptionId;
	var firstChildOptionValue;

	// Filter each child column
	var childColumns = parentSelect.Obj.data("SPCascadeDropdownsChildColumns");
	$(childColumns).each(function() {

		// Break out the data objects for this child column
		var opt = this.opt;
		var childSelect = this.childSelect;
		var childColumnStatic = this.childColumnStatic;
		var childColumnRequired = this.childColumnRequired;
		// Jeremy Rose - 4/27/2012: Get the PrependId variable here
                var childColumnPrependId = (this.childColumnPrependId != undefined) ? this.childColumnPrependId : "FALSE";
		
		// Get the parent column selection(s)
		parentSelectSelected = getDropdownSelected(parentSelect, opt.matchOnId);

		// If the selection hasn't changed, then there's nothing to do right now.  This is useful to reduce
		// the number of Web Service calls when the parentSelect.Type = "C" or "M", as there are multiple propertychanges
		// which don't require any action.  The attribute will be unique per child column in case there are
		// multiple children for a given parent.
		if(parentSelect.Obj.data("SPCascadeDropdown_Selected_" + childColumnStatic) === parentSelectSelected.join(";#")) {
			return;
		}
		parentSelect.Obj.data("SPCascadeDropdown_Selected_" + childColumnStatic, parentSelectSelected.join(";#"));

		// Get the current child column selection(s)
		childSelectSelected = getDropdownSelected(childSelect, true);

		// When the parent column's selected option changes, get the matching items from the relationship list
		// Get the list items which match the current selection
		var sortColumn = (opt.relationshipListSortColumn.length > 0) ? opt.relationshipListSortColumn : opt.relationshipListChildColumn;
		var camlQuery = "<Query><OrderBy><FieldRef Name='" + sortColumn + "'/></OrderBy><Where><And>";
		if(opt.CAMLQuery.length > 0) {
			camlQuery += "<And>";
		}

		// Build up the criteria for inclusion
		if(parentSelectSelected.length === 0) {
			// Handle the case where no values are selected in multi-selects
			camlQuery += "<Eq><FieldRef Name='" + opt.relationshipListParentColumn + "'/><Value Type='Text'></Value></Eq>";
		} else if(parentSelectSelected.length === 1) {
			// Only one value is selected
			camlQuery += "<Eq><FieldRef Name='" + opt.relationshipListParentColumn +
				(opt.matchOnId ? "' LookupId='True'/><Value Type='Integer'>" : "'/><Value Type='Text'>") +
				escapeColumnValue(parentSelectSelected[0]) + "</Value></Eq>";
		} else {
			var compound = (parentSelectSelected.length > 2) ? true : false;
			for(i=0; i < (parentSelectSelected.length - 1); i++) {
				camlQuery += "<Or>";
			}
			for(i=0; i < parentSelectSelected.length; i++) {
				camlQuery += "<Eq><FieldRef Name='" + opt.relationshipListParentColumn +
					opt.matchOnId ? "' LookupId='True'/><Value Type='Integer'>" : "'/><Value Type='Text'>" +
					escapeColumnValue(parentSelectSelected[0]) + "</Value></Eq>";
				if(i>0 && (i < (parentSelectSelected.length - 1)) && compound) {
					camlQuery += "</Or>";
				}
			}
			camlQuery += "</Or>";
		}

		if(opt.CAMLQuery.length > 0) {
			camlQuery += opt.CAMLQuery + "</And>";
		}

		// Make sure we don't get any items which don't have the child value
		camlQuery += "<IsNotNull><FieldRef Name='" + opt.relationshipListChildColumn + "' /></IsNotNull>";

		camlQuery += "</And></Where></Query>";
	
		$().SPServices({
			operation: "GetListItems",
			// Force sync so that we have the right values for the child column onchange trigger
			async: false,
			webURL: opt.relationshipWebURL,
			listName: opt.relationshipList,
			// Filter based on the currently selected parent column's value
			CAMLQuery: camlQuery,
			// Only get the parent and child columns
			CAMLViewFields: "<ViewFields><FieldRef Name='" + opt.relationshipListParentColumn + "' /><FieldRef Name='" + opt.relationshipListChildColumn + "' /></ViewFields>",
			// Override the default view rowlimit and get all appropriate rows
			CAMLRowLimit: 0,
			// Even though setting IncludeMandatoryColumns to FALSE doesn't work as the docs describe, it fixes a bug in GetListItems with mandatory multi-selects
			CAMLQueryOptions: "<QueryOptions><IncludeMandatoryColumns>FALSE</IncludeMandatoryColumns></QueryOptions>",
			completefunc: function(xData, Status) {

				// Handle errors
				$(xData.responseXML).find("errorstring").each(function() {
					var thisFunction = "SPServices.SPCascadeDropdowns";
					var errorText = $(this).text();
					if(opt.debug && errorText === "One or more field types are not installed properly. Go to the list settings page to delete these fields.") {
						errBox(thisFunction,
							"relationshipListParentColumn: " + opt.relationshipListParentColumn + " or " +
							"relationshipListChildColumn: " + opt.relationshipListChildColumn,
							"Not found in relationshipList " + opt.relationshipList);
					} else if (opt.debug && errorText === "Guid should contain 32 digits with 4 dashes (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).") {
						errBox(thisFunction,
							"relationshipList: " + opt.relationshipList,
							"List not found");
					}
					return;
				});

				// Add an explanatory prompt
				switch(childSelect.Type) {
					case "S":
						// Remove all of the existing options
						$(childSelect.Obj).find("option").remove();
						// If the column is required or the promptText option is empty, don't add the prompt text
						if(!childColumnRequired && (opt.promptText.length > 0)) {
							childSelect.Obj.append("<option value='0'>" + opt.promptText.replace(/\{0\}/g, opt.childColumn) + "</option>");
						} else if(!childColumnRequired){
							childSelect.Obj.append("<option value='0'>" + opt.noneText + "</option>");
						}
						break;
					case "C":
						// If the column is required, don't add the "(None)" option
						choices = childColumnRequired ? "" : opt.noneText + "|0";
						childSelect.Obj.attr("value", "");
						break;
					case "M":
						// Remove all of the existing options
						$(childSelect.Obj).find("option").remove();
						newMultiLookupPickerdata = "";
						break;
					default:
						break;
				}
				// Get the count of items returned and save it so that we can select if it's a single option 
				// The item count is stored thus: <rs:data ItemCount="1">
				numChildOptions = parseFloat($(xData.responseXML).SPFilterNode("rs:data").attr("ItemCount"));

				// Add an option for each child item
				$(xData.responseXML).SPFilterNode("z:row").each(function() {

					var thisOptionId, thisOptionValue;

					// If relationshipListChildColumn is a Lookup column, then the ID should be for the Lookup value,
					// else the ID of the relationshipList item
					var thisValue = $(this).attr("ows_" + opt.relationshipListChildColumn);
					
					if(thisValue !== undefined && thisValue.indexOf(";#") > 0) {
						var splitValue = thisValue.split(";#");
						thisOptionId = splitValue[0];
						thisOptionValue = splitValue[1];
					} else {
						thisOptionId = $(this).attr("ows_ID");
						thisOptionValue = thisValue;						
					}
 
                                        // Jeremy Rose - 4/27/2012: If PrependId is "TRUE", then prepend the Id to the value 
					if (childColumnPrependId == "TRUE") {
						thisOptionValue = thisOptionId + ' - ' + thisValue;
					}
					
					// If the relationshipListChildColumn is a calculated column, then the value isn't preceded by the ID,
					// but by the datatype.  In this case, thisOptionId should be the ID of the relationshipList item.
					// e.g., float;#12345.67
					if(isNaN(thisOptionId)) {
						thisOptionId = $(this).attr("ows_ID");
					}
				
					// Save the id and value for the first child option in case we need to select it (selectSingleOption option is true)
					firstChildOptionId = thisOptionId;
					firstChildOptionValue = thisOptionValue;
				
					switch(childSelect.Type) {
						case "S":
							var selected = ($(this).attr("ows_ID") === childSelectSelected[0]) ? " selected='selected'" : "";
							childSelect.Obj.append("<option" + selected + " value='" + thisOptionId + "'>" + thisOptionValue + "</option>");
							break;
						case "C":
							if(thisOptionId === childSelectSelected[0]) {
								childSelect.Obj.attr("value", thisOptionValue);
							}
							choices = choices + ((choices.length > 0) ? "|" : "") + thisOptionValue + "|" + thisOptionId;
							break;
						case "M":
							childSelect.Obj.append("<option value='" + thisOptionId + "'>" + thisOptionValue + "</option>");
							newMultiLookupPickerdata += thisOptionId + "|t" + thisOptionValue + "|t |t |t";
							break;
						default:
							break;
					}
				});

				switch(childSelect.Type) {
					case "S":
						childSelect.Obj.trigger("change");
						// If there is only one option and the selectSingleOption option is true, then select it
						if(numChildOptions === 1 && opt.selectSingleOption === true) {
							$(childSelect.Obj).find("option[value!='0']:first").attr("selected", "selected");
						}
						break;
					case "C":
						// Set the allowable choices
						childSelect.Obj.attr("choices", choices);
						// If there is only one option and the selectSingleOption option is true, then select it
						if(numChildOptions === 1 && opt.selectSingleOption === true) {
							// Set the input element value
							$(childSelect.Obj).attr("value", firstChildOptionValue);
							// Set the value of the optHid input element
							$("input[id='" + childSelect.Obj.attr("optHid") + "']").val(firstChildOptionId);
						}
						// If there's no selection, then remove the value in the associated hidden input element (optHid)
						if(childSelect.Obj.val() === "") {
							$("input[id='" + childSelect.Obj.attr("optHid") + "']").val("");
						}
						break;
					case "M":
						// Find the important bits of the multi-select
						MultiLookupPickerdata = childSelect.Obj.closest("span").find("input[name$='MultiLookupPicker$data']");
						master = window[childSelect.Obj.closest("tr").find("button[id$='AddButton']").attr("id").replace(/AddButton/,'MultiLookupPicker_m')];
						currentSelection = childSelect.Obj.closest("span").find("select[ID$='SelectResult'][Title^='" + opt.childColumn + " ']");
						// Clear the master
						master.data = "";

						MultiLookupPickerdata.attr("value", newMultiLookupPickerdata);
						
						// Clear any prior selections that are no longer valid
						$(currentSelection).find("option").each(function() {
							var thisSelected = $(this);
							// Jeremy Rose - 4/27/2012: Changed $(this).html() to $(this).val()
                                                        var thisValue = $(this).val();
							$(this).attr("selected", "selected");
							// Jeremy Rose - 4/27/2012: Changed .find("option:contains...") to .find("option").each()
$(childSelect.Obj).find("option").each(function() {
                                                                // Jeremy Rose - 4/27/2012: Changed $(this).html() to $(this).val()
								if($(this).val() == thisValue) {
									thisSelected.removeAttr("selected");
								}
							});
							
							// Jeremy Rose - 4/27/2012: Removed the following code because:
							// currentSelection.option.html() is actually Value - Text (Ex: 1 - OptionA and 2 - OptionB)
							// childSelect.Obj.option.html() is actually Text (Ex: OptionA and OptionB)
							// If the options in childSelect are [OptionA, OptionB, OptionC] and currentSelection are [1 - OptionA, 2 - OptionB, 3 - OptionC],
							// childSelect will never find an option that contains the currentSelection option 
							
							//$(childSelect.Obj).find("option:contains('" + thisValue + "')").each(function() {
							//	if($(this).html() === thisValue) {
							//		thisSelected.removeAttr("selected");
							//	}
							//});
						});
						
                                                // Jeremy Rose - 4/27/2012: Moved the following lines from below to here as suggested by Paul (ptavares)
						// Set master.data to the newly allowable values
						master.data = GipGetGroupData(newMultiLookupPickerdata);
						// Jeremy Rose - 4/27/2012: Added details to comment
						// GipRemoveSelectedItems() will take any options in the _SearchResult field (Selected Values) that are marked as "selected"
						// and move them to the _Candidate field (Possible Values)
						GipRemoveSelectedItems(master);
						
						// Hide any options in the candidate list which are already selected
						$(childSelect.Obj).find("option").each(function() {
							var thisSelected = $(this);
							$(currentSelection).find("option").each(function() {
                                                                // Jeremy Rose - 4/27/2012: Changed $(this).html() to $(this).val() and thisSelected.html() to thisSelected.val()
								if($(this).val() == thisSelected.val()) {
									thisSelected.remove();
								}
							});
							
							// Jeremy Rose - 4/27/2012: Removed the following code because:
							// currentSelection.option.html() is actually Value - Text (Ex: 1 - OptionA and 2 - OptionB)
							// childSelect.Obj.option.html() is actually Text (Ex: OptionA and OptionB)
							// If the options in childSelect are [OptionA, OptionB, OptionC] and currentSelection are [1 - OptionA, 2 - OptionB, 3 - OptionC],
							// childSelect will never match an option in the currentSelection options 
							
							//$(currentSelection).find("option").each(function() {
							//	if($(this).html() === thisSelected.html()) {
							//		thisSelected.remove();
							//	}
							//});
						});
						// Jeremy Rose - 4/27/2012: Added details to comment
						// GipAddSelectedItems() will take any options in the _Candidate field (Possible Values) that are marked as "selected"
						// and move them to the _SearchResult field (Selected Values)
						GipAddSelectedItems(master);
					
                                                // Jeremy Rose - 4/27/2012: Commented out the following lines as suggested by Paul (ptavares)	
						// Set master.data to the newly allowable values
						//master.data = GipGetGroupData(newMultiLookupPickerdata);

						// Trigger a dblclick so that the child will be cascaded if it is a multiselect.
						childSelect.Obj.trigger("dblclick");

						break;
					default:
						break;
				}
			}
		});
		// If present, call completefunc when all else is done
		if(opt.completefunc !== null) {
			opt.completefunc();
		}
	}); // $(childColumns).each(function()

} // End cascadeDropdown

I hope you have time to review this and maybe it will help anyone else looking for a solution. Also, these changes will need to be made to the FilterDown feature as well. 

Apr 27, 2012 at 6:46 AM

BTW, I hate the code editor :(

Look how ugly that is lol

Coordinator
Apr 27, 2012 at 7:45 PM

Nice work, Jeremy. I've never even seen the prependId option in the wild. It certainly has a valid use (you've proven that), and it explains why I had no idea what you were talkkng about!

I'll turn this into a task in the Issue Tracker. It looks like you are all set for now, and since I've never run into this with anyone else before, I will get your fixes in but it may not be the next release.

Thanks!

M.

Coordinator
Apr 27, 2012 at 7:47 PM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.
Apr 28, 2012 at 3:06 AM
Jeremy, Mark: thanks looking into this issue. I'm glad to see that this issue will be resolved in the future.

Jeremy: nice work on chasing this one.

_____
Paul

Sent from mobile device.
May 3, 2012 at 2:58 AM

Not a problem Marc/Paul. I'm glad I could contribute to such a great project. 

It's funny, my co-workers say I seem to always have a knack for doing things not normally found in the wild :)

Oct 18, 2013 at 2:36 PM
I had a similar issue to those above.

First, I tried moving:

GipRemoveSelectedItems(master);

as suggested but like with jrose it was only leaving a single value in the cascade.

Then, after reading these posts and thinking about my environment, I realized that I am running on an environment which had jQuery 1.6 in the masterpage.

I thought that there might be an issue using the 'selected' attribute with jQuery 1.6.

Since I couldn't remove the old jQuery as I didn't have control over the masterpage. I namespaced it out and then loaded 1.8.3 in (I don't load 1.9 just because of deprecated features).

Sure enough, as soon as I did that, everything worked!

So, moral of the story, check your jQuery version!
Oct 29, 2013 at 2:22 PM
Edited Oct 29, 2013 at 2:23 PM
spcascade.org uses SPServices to provide best free solution which support multi level dropdown list with no limits.
It is javascript/jquery based solution with no page refreshes and involves only one look up list for multiple columns.
Saves data in single line text instead of Ids that are used by typical look up columns.
Very easy to use syntax too!
$().SPCascade({
    destListColTitles: ['Continent','Country','State','City'], 
    srcListColInternalNames: ['Continent','Country','State','Title'],
    srcListName: 'CitiesLookupList',
    srcListFilterCAMLQuery: "<Query><Where><Contains>
    <FieldRef Name='Continent'/><Value Type='Text'>America</Value>
    </Contains></Where></Query>"

});
Image

http://www.spcascade.org
Dec 20, 2013 at 1:54 AM
I had the same concern as ptavares. applying the fix at the top of the post fixed it for me as well. The concern only occurred in IE. Chrome was OK.
My setup

SP 2010
jquery-1.10.2.min
SPServices-2013.01


Thanks for the great code!
Coordinator
Dec 20, 2013 at 2:25 PM
Did you have to put in a fix with 2013.01? If so, can you post what you did? Also, you may want to try 2013.02BETA3, as its release is imminent.

M.
Dec 20, 2013 at 7:44 PM
Yes, I did have to put it in 2013.01 as well. I pasted in exactly what ptavares had in the first comment (Including his comments).

I tried jquery.SPServices-2013.02BETA3 and unfortunately it exhibits the same concern.
                case "M":
    MultiLookupPickerdata.attr("value", newMultiLookupPickerdata);
    // Clear any prior selections that are no longer valid
    $(currentSelection).find("option").each(function() {
        var thisSelected = $(this);
        $(this).attr("selected", "selected");
        $(childSelect.Obj).find("option").each(function() {
            if($(this).html() === thisSelected.html()) {
                thisSelected.attr("selected", "");
            }
        });
    });
    // PT: Moved the setting of master.data here (from linke 1655)
    //     because:
    //     GipRemoveSelectedItems() (below on line 1653) calls
    //          GipSelectCandidateItems() who calls
    //              GipSelectCandidateItems() whow then calls
    //                  GipDisplayDescription() which attempts to
    //     use master.data[] and throws exception (line 380 of SP's
    //     groupeditempicker.js. 
    master.data = GipGetGroupData(newMultiLookupPickerdata);
    GipRemoveSelectedItems(master);
    // Hide any options in the candidate list which are already selected
    $(childSelect.Obj).find("option").each(function() {
        var thisSelected = $(this);
        $(currentSelection).find("option").each(function() {
            if($(this).html() === thisSelected.html()) {
                thisSelected.remove();
            }
        });
    });
    GipAddSelectedItems(master);
    // Set master.data to the newly allowable values
    // PT: Commented out code below and moved it above, to line 1641.
//                        master.data = GipGetGroupData(newMultiLookupPickerdata);

    // Trigger a dblclick so that the child will be cascaded if it is a multiselect.
    childSelect.Obj.trigger("dblclick");

    break;
Coordinator
Dec 27, 2013 at 5:35 PM
I want to get a fix for this into 2013.02 if I can, but I also want to get the release out asap.

Can either of you give me quick steps to reproduce the issue?

Thanks,
M.
Coordinator
Dec 27, 2013 at 7:46 PM
Paul and I just tested this out and can't find any existing error with 2013.02. Going to release as-is.

M.
Dec 27, 2013 at 7:50 PM
All,
I just recreated this problem in SP2010 (office 365 account) using the versions that I initially reported it against (jquery 1.6.4 / SPServices 0.6.2). When I replaced those with the following versions, it all worked as expected and the error was not triggered:
  • jQuery 1.10.2
  • SPServices 2013.02 BETA3
I (Personally) consider this fixed now...

Thanks Marc.