Click here to Skip to main content
15,885,278 members
Articles / Programming Languages / Javascript

Multi select list using jQuery

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
14 Aug 2013CPOL2 min read 26.9K   2   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. C#
    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. C#
    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.

    JavaScript
    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. JavaScript
    $.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. JavaScript
    <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.
      JavaScript
      $("#" + 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. JavaScript
        .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. JavaScript
      .data("ui-autocomplete")._renderItem = RenderItem;
  13. Now bind the functions in the ready handler.
  14. JavaScript
    $(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:

JavaScript
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.]

JavaScript
$("#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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead 3PillarGlobal
India India
I am a solutions lead working on .NET for about 10 years, focusing on core framework implementations in project. My development interests are in middle tier and backend components and sometimes, identifying design elements for technical solutions. Apart, I collect coins and stamps and do photography whenever I get a chance.

Comments and Discussions

 
QuestionDemo of code Pin
Member 116630494-May-15 3:50
Member 116630494-May-15 3:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.