Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Multi select list using jQuery

0.00/5 (No votes)
14 Aug 2013 1  
Multi select list using jQuery.

Introduction

jQuery has a auto-complete pluggin in its jquery-UI collection. We can use that for having a multi-select list or single select autocomplete using our own datasource. This post will showcase methodology to create a wrapper library using auto-complete and necessary steps to take to make your own auto-complete rock and roll.

  1. Add a static script function in codebehind which returns desired data. Annotate it with ScriptMethod attribute. The function should return an IEnumerable<T>; where T: List<string> for plain text display; Dictionary<KeyValuePair> for a display and a value field; or Tuple<T1,.. Tn> <entity>where one field is for display and other as value fields.
  2. public partial class _Default : Page
    {
        [System.Web.Script.Services.ScriptMethod (UseHttpGet = true, 
           ResponseFormat = System.Web.Script.Services.ResponseFormat.Json)]
        public static IEnumerable> GetAllDepartments()
        {
            return Repository.GetAllDepartments();
        }
    }
    
    public static class Repository
    {
        internal static IEnumerable> GetAllDepartments()
        {
            // Get list of departments 
            // Department name is taken as Key (meant to be displayed)
            // and id is taken as Value (meant to be used as selection)
        }
    }
  3. Add files jquery.ui.css and jquery.ui.js in the header section along with jquery.js.
  4. Declare an array within JavaScript for each type of entity to be bound.
  5. var dept = [];
    var people = [];
  6. Call the script function through ajax, and on success, typecast the JSON to required format. The AutoComplete library conventionally the display field to have “label” field and other properties to be “value”, “value1”.... Hence the casting is required to such format, and each casted object is stored in a predefined array; which serves as the datasource for the autocomplete dropdown.
  7. Alternatively, if a long list of items need to be binded (through a Tuple<T1... Tn), create own JSON object without any need to cast to label/value (although that would more suit the convention. The AJAX method shown takes the datasource object to be populated and the corresponding method in the Script Service involved. This enables a single function to be generalized for populating as many datasources as required.

    function PopulateItems(arrayRef, scriptName) {
         var serviceUrl = '<=ResolveClientUrl("~//MyPage.aspx")%>' + "/" + scriptName;
        $.ajax({
            url: serviceUrl,
            type: "POST",
            contentType: "application/json; charset=utf-8",
            processData: true,
            success: function (output) {
                // Check if returned item has any values
                if (output.d.length > 0) {
                    // clear existing array
                    arrayRef.length = 0;
                    $(output.d).each(function () {
                        // Create json array as per our requirement
                        arrayRef.push({
                            "value": this.Value,
                            "label": this.Key
                        });
                    });
                }
            },
            error: function OnError(request, status, error) {
                alert(error.statusText);
            }
        });
    }
  8. Add autocomplete filter override to match start of word rather than any place.
  9. $.ui.autocomplete.filter = function (array, term) {
        var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
        $.grep(array, function (value) { return matcher.test(value.label); });
    };
  10. Add input fields/textbox wrapped in a <div class="ui-widget"> to load up the auto-complete widget. Only the display purpose control is visible, the value purpose fields are hidden.
  11. <div class="ui-widget">
       <input type="text" runat="server" id="txtDept" />
       <input type="text" runat="server" id="txtDeptValue" style="display: none" />
    </div>
    
    <div class="ui-widget">
        <input type="text" runat="server" id="txtPersonName" />
        <input type="text" runat="server" id="txtPersonId" style="display: none" />
    </div>
  12. Bind text control input events:
    1. Keydown:
      1. Tab: show autocomplete
      2. Delete / backspace key: delete only the last item selected.
      $("#" + textControlId)
      .bind("keydown", function (event) {
           if ((event.KeyCode == $.ui.keyCode.TAB)  &
             $(this).data("ui-autocomplete").menu.active)) {
              event.preventDefault();
             }
         RemoveItem(isMulti, event, textControlId, valueControlId);
      });
    2. AutoComplete 
      1. Source: datasource direct or filtered (for multi-item)
      2. Focus: disable event to prevent value insertion
      3. Select: map functionality to select the text (single/multi) and the corresponding value fields (if any).
      4. .autocomplete({
         minLength: 2,
         source: datasource,
         focus: function (event, ui) { return false; },
         select: function (event, ui) {
              $("#" + textControlId).val(ui.item.label);
              $("#" + valueControlId).val(ui.item.value);
              return false;
              }
        });
    3. Data: render autocomplete list as a dropdown
    4. .data("ui-autocomplete")._renderItem = RenderItem;
  13. Now bind the functions in the ready handler.
  14. $(document).ready(function() { 
           PopulateItems(dept, "GetAllDeptNames"); 
          PopulateItems(people, "GetAllPeopleNames"); 
    
          ToBeExecutedOnDOMReady(true, dept,
              "<%=txtDept.ClientID%>",
              "<%=txtDeptValue.ClientID%>"); 
    
           ToBeExecutedOnDOMReady(false, people, 
               "<%=txtPersonName.ClientID%>", 
               "<%=txtPersonId.ClientID%>");
          };
    );

The code has been written to allow a single item list as well as a multi item list. To enable singe/multi, pass the first parameter true/false accordingly.

The full code along with all the helper functions is listed below:

var dept = [];
var people = [];

function split(csv) {
    return csv.split(/;\s*/);
};

function SetSelectedValue(sourceType, textControlId, valueControlId, csv) {
    var datasource;
    switch (sourceType) {
        case 1: datasource = dept; break;
        case 2: datasource = people; break;
    }

    var selectedValues = split(csv);    // returns array of items
    var itemId = "";
    var itemName = "";
    var i = datasource.length;
    for (var iter in selectedValues) {
        var value = selectedValues[iter];
        while (i--) {
            var currItem = datasource[i];
            if (currItem.value == value) {
                itemId = currItem.value + ";" + itemId;
                itemName = currItem.label + ";" + itemId;
                break;
            }
        }
    }
    // Remove trailing semicolon at end
    itemId = itemId.replace(/^\s+l;\s+$/g, "");
    itemName = itemName.replace(/^\s+l;\s+$/g, "");
    $("#" + textControlId).val(itemName);
    $("#" + valueControlId).val(itemId);
}

function PopulateItems(arrayRef, scriptName) {
    var serviceUrl = '<%=ResolveClientUrl("~//Service/Services.aspx")%>' + "/" + scriptName;
    $.ajax({
        url: serviceUrl,
        type: "POST",
        contentType: "application/json; charset=utf-8",
        processData: true,
        success: function (output) {
            // Check if returned item has any values
            if (output.d.length > 0) {
                // clear existing array
                arrayRef.length = 0;
                $(output.d).each(function () {
                    // Create json array as per our requirement
                    arrayRef.push({
                        "value": this.Value,
                        "label": this.Key
                    });
                });
            }
        },
        error: function OnError(request, status, error) {
            alert(error.statusText);
        }
    });
}

function ExtractLastTerm(term) {
    return split(term).pop();
}

function ClearValues(textControlId, valueControlId) {
    $("#" + textControlId).val("");
    $("#" + valueControlId).val("");
}

function RemoveItem(isMulti, event, textControlId, valueControlId) {
    if (isMulti) {
        if (event.KeyCode == 8) {
            // backspace key, remove last item by popping and rejoining the stack
            var currText = $("#" + textControlId).val();
            var items = split(currText);
            // Remove last partially entered string
            items.pop();
            // Add empty item as the delimiter
            items.push("");
            $("#" + textControlId).val(items.join(";"));

            var currIds = $("#" + valueControlId).val();
            items = split(currIds);
            // Remove last partially entered string
            items.pop();
            // Add empty item as the delimiter
            items.push("");
            $("#" + valueControlId).val(items.join(";"));

            // Prevent deletion of text (default behavior of backspace key)
            event.preventDefault();
            return false;
        } else if (event.KeyCode == 46) {
            // ask user the purpose of hitting delete
            var check = confirm("Only last item can be deleted (using backspace). 
              Click [OK] to delete entire selection or [Cancel] to review.");
            if (check)
                ClearValues(textControlId, valueControlId);
            event.preventDefault();
            return false;
        }
    } else {
        if ((event.KeyCode == 46) || (event.KeyCode == 8)) {
            ClearValues(textControlId, valueControlId);
            event.preventDefault();
            return false;
        }
    }
}

function RenderItem(ul, item) {
    return $("<li>").append("<a>" + item.label + "</a>").appendTo(ul);
}

$.ui.autocomplete.filter = function (array, term) {
    var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
    $.grep(array, function (value) { return matcher.test(value.label); });
};

function ContainedInArray(arrayRef, item) {
    for(index in arrayRef) {
        if (arrayRef[index].value == item) {
            return true;
        }
    }
    return false;
}

function ToBeExecutedOnDOMReady(isMulti, datasource, textControlId, valueControlId) {

    $(document).ready(function () {

        /* 1st check if textcontrolid and valueControlId have been rendered or not.
        It can be that even though DOM has loaded, 
        these controls weren't rendered due to server side visible=false
        */
        if (("#" + textControlId).length > 0) {

            if (isMulti) {

                $("#" + textControlId)
                    .bind("keydown", function (event) {
                        if ((event.KeyCode == $.ui.keyCode.TAB) &&
                            ($(this).data("ui-autocomplete").menu.active)) {
                            event.preventDefault();
                        }
                        RemoveItem(isMulti, event, textControlId, valueControlId);
                    })
                    .autocomplete({
                        minLength: 2,
                        source: function (request, response) {
                            // delegate back to autocomplete, but extract the last term
                            response($.ui.autocomplete.filter(datasource, Ext
                            },
                        focus: function (event, ui) {
                            // Prevent value inserted on focus
                            return false;
                        },
                        select: function (event, ui) {
                            var isPresent = 1;
                            var text = $("#" + textControlId).val();
                            var arrText = split(text);
                            if (arrText.length > 0) {
                                isPresent = ContainedInArray(arrText, ui.item.label);
                            }
                            // remove partial text of item
                            var items = split(this.Value);
                            items.pop();
                            // if element not previously added, then add else reject the selection as duplicate
                            if (!isPresent) {
                                items.push(ui.item.label);
                                items.push("");
                            }
                            // Entered text need extraction
                            $("#" + textControlId).val(items.join(";"));
                            // but value does not, it just needs to be appended
                            if (!isPresent) {
                                var currItem = $("#" + valueControlId).val();
                                items.push(ui.item.label);
                                items.push("");
                            }
                            $("#" + textControlId).val(items.join(";"));
                        }
                    })
                    .data("ui-autocomplete")._renderItem = RenderItem;
            }
            else {
                $("#" + textControlId)
                    .bind("keydown", function (event) {
                        if ((event.KeyCode == $.ui.keyCode.TAB) &&
                            ($(this).data("ui-autocomplete").menu.active)) {
                            event.preventDefault();
                        }
                        RemoveItem(isMulti, event, textControlId, valueControlId);
                    })
                .autocomplete({
                    minLength: 2,
                    source: datasource,
                    focus: function (event, ui) { return false; },
                    select: function (event, ui) {
                        $("#" + textControlId).val(ui.item.label);
                        $("#" + valueControlId).val(ui.item.value);
                        return false;
                    }
                })
                .data("ui-autocomplete")._renderItem = RenderItem;
            }
        }
    });
};

$(document).ready(function() {
    PopulateItems(dept, "GetAllDeptNames");
    PopulateItems(people, "GetAllPeopleNames");

    ToBeExecutedOnDOMReady(true, dept, "<%=txtDept.ClientID%>", "<%=txtDeptValue.ClientID%>");
    ToBeExecutedOnDOMReady(false, people, "<%=txtPersonName.ClientID%>", "<%=txtPersonId.ClientID%>");

    Sys.WebForms.PageRequestManager.getInstance().add_endRequest(EndRequestHandler);

    function EndRequestHandler(sender, args)  {
        if (args.get_Error() == undefined) {
            PopulateItems(dept, "GetAllDeptNames");
            PopulateItems(people, "GetAllPeopleNames");

            ToBeExecutedOnDOMReady(true, dept, "<%=txtDept.ClientID%>", "<%=txtDeptValue.ClientID%>");
            ToBeExecutedOnDOMReady(false, people, "<%=txtPersonName.ClientID%>", "<%=txtPersonId.ClientID%>");
        }
    }
});

This example works if the list is small and can be put as a page level variable. However if the list is too huge that getting all items on the page start will be too heavy, take the datasource as a service method itself. The service will be called through the 'source' parameter and will dynamically update the items.

[Note: This call will be once per character entered, so you need output caching on the service side.]

$("#servicePull")
    .autocomplete({
        source: function(request, response) {
            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: serviceUrl,
                dataType: "json",
                data: "{'RequestId':'" + request + "'}",
                dataFilter: function(data) { return data; },
                success: function(data) {
                    // create custom array of json objects directly
                    // instead of iterating, and return it to caller
                    response($.map(data.d, function(item) {     
                        return {
                            custId: item.Item1,
                            custName: item.Item2,
                            custAdd: item.Item3
                        };
                    }));
                },
                error: function(err) {  alert(err);  }
            })},
        select: function(event, ui) {
            // No 'label' or 'value', just use the json fields as created in success handler
            $("#custId").val(ui.item.custId);                   
            $("#custName").val(ui.item.custName);
            $("#custAdd").val(ui.item.custAdd);
        }
    })
.data("ui-autocomplete")._renderItem = function (ul, item) {
    return $("<li>").append("<a>" + item.custName + "</a>").appendTo(ul);
};

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here