Thursday, April 29, 2010

Cascading dropdown using jQuery and MVC framework

My page contains 10 cascading dropdowns, so instead of write a JavaScript code to handle each dropdown which need a lot of copy and paste and at the end it will look ugly and hard to maintain. So my idea was to write a simple JavaScript code once included and initiated in my page it will do the work for me.

The idea was to add custom html attributes in the dropdown to specify which control will be affected when the current selected value change and the data source to populate the sibling control from.

First let’s start with the required attributes we need to be able to populate the sibling dropdown when the current one change 

function cascadeDropDown() {
    this.dataUrl;              // the action name to use when populate the child dropdown 
    this.targetcontrolId;      // the dropdown Id that will be populated on change
    this.defText;              // the default text displayed after data population 
    this.defValue;             // the default selected value
}

Then we will add the equivalent attribute in the dropdown html, In our example we will populate the states dropdown based on the selected country.

<select name="Country" id="Country" targetcontrolid="States"
        dataurl="/Admin/JsonStatesCombo" deftext="Select State">
 
<select name="States" id="States" disabled="disabled"></select>
}

 

Next we need to read these attribute in the Country select box and use it to populate the States select box.

function initCascadeDropDowns() {
    $(document).ready(function() {
        $('select').each(function() {
            // Read html attribute and set the cascadeDropDown object properties 
            var dropdown = new cascadeDropDown();
            dropdown.targetcontrolId = $(this).attr('targetcontrolid');
            dropdown.dataUrl = $(this).attr('dataurl');
            dropdown.defValue = $(this).attr('defvalue');
            dropdown.defText = $(this).attr('deftext');
            // Check for min required attribute to populate the next dropdown successfully. 
            if (dropdown.targetcontrolId != null && dropdown.dataUrl != null) {
                // attach onchange event to the current select box if needed attributes found
                $(this).change(function() {
                    if ($(this).val() == '' || $('option', this).size() == 0) {
                        // clear the child dropdowns data by recursively trigger each child onchange event.
                        $('#' + dropdown.targetcontrolId).trigger("change");
                        $('#' + dropdown.targetcontrolId).empty();
                        $('#' + dropdown.targetcontrolId).attr("disabled", "disabled");
                    }
                    else {
                        // populate child dropdown data
                        FillInCascadeDropdown({ parentVal: $(this).find(":selected").val() },
                        '#' + dropdown.targetcontrolId, dropdown.dataUrl, dropdown.defText, dropdown.defValue);
                    }
                });
            }
        });
    });    
}

And here is the implementation of the FillInCascadeDropdown function

function FillInCascadeDropdown(map, dropdown, action, defText, defValue) {
    $(dropdown).empty();
    if (defText != null) $(dropdown).append("<option value=\"\">" + defText + "</option>");
    $.getJSON(action, map,
              function(result) {
                  for (i = 0; i < result.length; i++) {
                      if (result[i].Selected) $(dropdown).append("<option value=\"" + result[i].Value + "\" SELECTED>" + result[i].Text + "</option>");
                      else $(dropdown).append("<option value=\"" + result[i].Value + "\">" + result[i].Text + "</option>");
                  }
                  if (result.length > 0)
                      $(dropdown).removeAttr('disabled');
                  else {
                      $(dropdown).trigger("change");
                      $(dropdown).empty();
                      $(dropdown).attr("disabled", "disabled");
                  }
                  if (defValue == null && defText != null)
                      defValue == "";
                  $(dropdown).val(defValue);
              });
          }
 

 

We are almost done,  we just need to implement the JsonstatesCombo action in the Admin Controller.

[AcceptVerbs(HttpVerbs.Get)]
public JsonResult JsonStatesCombo(int parentVal)
{
   LinqContext context = new LinqContext();
   var model = from s in context.States where CountryId == parentVal
                    select s;
   var list = new SelectList(model, "StateId", "StateName"); 
   return Json(list);
}

 

I included the JavaScript above in external js file so it can be used in many pages as we want, Now when we need to work with cascade dropdowns all we just need to call initCascadeDropDowns() when document is loading after attach the proper html attributes.

 

MVC Extensions 

Need to make my life easier and my html code to be more elegant and that’s not everything, also it will be great to let Visual studio IntelliSense help me while writing the html attributes and not to forget required attribute or mistype the attribute name.

Here MVC extensions become handy, I will add new html helper control and call it CascadeDropDown

public static string CascadeDropdown(this HtmlHelper htmlHelper, string name, string actionName, string controllerName, string targetControlId)
{
    return CascadeDropdown(htmlHelper, name, actionName, controllerName, targetControlId, null, null);
}
 
public static string CascadeDropdown(this HtmlHelper htmlHelper, string name, string actionName, string controllerName, string targetControlId, string defaultText)
{
    return CascadeDropdown(htmlHelper, name, actionName, controllerName, targetControlId, defaultText, null);
}
 
public static string CascadeDropdown(this HtmlHelper htmlHelper, string name, string actionName, string controllerName, string targetControlId, string defaultText, string defaultValue)
{
    var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
    string action = urlHelper.Action(actionName, controllerName, null);
    TagBuilder inputBuilder = new TagBuilder("select");
    inputBuilder.MergeAttribute("dataurl", action);
    inputBuilder.MergeAttribute("targetcontrolid", targetControlId);
    if (defaultText != null) inputBuilder.MergeAttribute("deftext", defaultText);
    if (defaultValue != null) inputBuilder.MergeAttribute("defvalue", defaultValue);
    inputBuilder.MergeAttribute("name", name);
    inputBuilder.MergeAttribute("disabled", "disabled");
    inputBuilder.GenerateId(name);
    return inputBuilder.ToString(TagRenderMode.SelfClosing);
}

 

Now we replace the old select html tag with CascadeDropDown control.

<!--<select name="Country" id="Country" disabled="disabled" targetcontrolid="States"
dataurl="/Admin/JsonStatesCombo" deftext="Select State"></select> -->
 
<%
 = Html.CascadeDropdown("Country", "JsonStatesCombo", "Admin", "States", "Select State") 
%>
                

 

Update: In a later post i extended the cascade dropdown control to support view server binding.

1 comment:

GOM3A said...

awesome thanks Islam