From http://spservices.codeplex.com/discussions/316604
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'
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'