Tuesday, December 15, 2009

Using jQuery on the client with JSON data for smart client-side data filtering

 

Background

Recently I started using some of the more advanced features of jQuery via a FUBU MVC project and from this experience I have grown a deep love and appreciation for the power it brings to the web developers set of tools. jQuery is a full-featured library add-in to JavaScript that enables DOM traversal. To more easily grok what jQuery sets out to accomplish, think of traversing the browser DOM in a SQL query like manner. With jQuery I will demonstrate that you can easily parse through a JSON data set stored in the DOM. The solution requires client side filtering for three selectors that reside on a page that displays a grid of data. The users desire an AJAX like selection process. We will store the JSON data in a hidden field mimicking the initial data set. This hidden field will additionally contain fields for the client-side filtering that are not part of the grids initial display.

View Models

FUBU MVC is an opinionated approach to web development. One of the underlying concepts of the FUBU MVC architecture that I think is fundamentally strong is what the authors refer to as the “Thunderdome Principal”. One model in and one model out. As I mentioned at the outset above, our out model defined for the view will need a property containing the entire record set that’s initially sent to the view together with additional filter selectors. To aid in the transformation to and from jQuery to JSON we will utilize an additional jQuery plug-in called jQuery-JSON. Jason Grundy, one of the Elegant Coders provides an insightful discussion on this plug-in and complements the ideas I will discuss here together with applicable code for wiring up the solution. The idea is not that difficult. A word of caution, when your data set is in the thousands of rows this approach may not be the most sensible choice due to the amount of data passing over the wire. The first step is to utilize the JavaScriptSerializer class that is contained within the System.Web.Script.Serialization namespace. To serialize data to the client, simply call the Serialize method and conversely to fill a property with JSON data coming back from the client call the Deserialize method. The MapLocations property below is of type IEnumerable<T> and is initially mapped to a user control contained within our view that will render a list of mapping locations. We will include three drop-down selectors directly above the grid of map locations for implementing the client side filtering requirement. The property TargetMapLocationsListJson which is of type string, will contain the data in JSON format for the grid and also the fields required for filtering.

 outModel.MapLocations = locationList;
 outModel.TargetMapLocationsListJson = _javaScriptSerializer.Serialize( outModel.MapLocations);
 
 

View


 <div id="selectedMapLocations">
                <table class="data-grid">
                    <thead>
                        <tr>
                            <th>
                                Store Name
                            </th>
                            <th>
                                Sales
                            </th>
                            <th>
                                Number of Employees
                            </th>
                            <th>
                                Action
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <%=this.RenderPartial().Using<ListMapLocationsView>().WithDefault
                            ("No locations exist for these choices").WithoutItemWrapper().WithoutListWrapper()
                                                    .ForEachOf(Model.MapLocations)%>
                    </tbody>
                </table>
            </div>



Our view model that displays the grid of map locations passes into the filtered list view a strongly typed list of store locations that are rendered by a ForEachOf HTML helper extension included within the FUBU MVC’s core framework. Ryan Kelley of Elegant Code wrote a great series on FUBU MVC and he covers wiring up the view model with the view in greater detail than I will here. If you are committed to using the FUBU MVC architecture I highly recommend that you check out his 4 part series.


We set up a hidden control on the page to hold the serialized JSON data that will mimic what is displayed within the selectedMapLocations div that is populated with another FUBU MVC helper extension called the HiddenFor control.


<div id="mapLocationsList">

                <%= this.HiddenFor(x => x.TargetMapLocationsListJson).ElementId("targetMapLocationsListJSON")%>

</div>




 


JavaScript


And now we can move on to the JavaScript. There will not be any server involvement until the user clicks the submit button. You will note above in the HTML for the data-grid that we have included a column named Action. This column will contain a button click event allowing the user to add the various map locations to their profile. This will add the location selection to a hidden field that is converted to JSON so that we can Deserialize the selections on the server. We will then add each of the selections to IEnumerable<T> property that is contained within the Domain object.


On document ready we created a function that contains initialization methods, event handling routines and associated helper routines. We set three global Boolean properties to false. As previously noted we use three filters that are invoked by dropdown controls. On initialization each of these dropdown controls are set to null. Then each of the dropdown event handlers are instantiated. When a user changes a selection the event handler associated to the dropdown control fires as noted below for State.


            function initSelectedStateChangeEventHandling() {
                $("#list-maplocations-state").change(function() {
                    if (stateChanged == false && $(this).val() == "") return;
                    $("#list-maplocations-state option:selected").each(function() {
                        stateChanged = true;
                        findMapLocationsByFilter($(this).val(), 'state');
                    })
                });
            }





What should be evident from the above JavaScript snippet is that when the Boolean property is no longer false and the selection value is not null then we for each through the users selection choices and call into a function that performs the client-side filtering. Lets have a look at the findMapLocationsByFilter function.


            function findMapLocationsByFilter(criteria, filterType) {
                var originalList = $('tbody');
                originalList.find('tr,td').remove();
                var data = $("#targetMapLocationListJSON").val();
                if (data == "") return;
                $.each($.evalJSON(data), function() {
                    originalList.append(getSelectedFilteredMapsHtml(this, criteria, filterType));
                });
            }



This is a generic function that is used by all dropdown selectors. In this function we utilize jQuery selectors for the existing content within the tbody element. We then modify the selector to contain only the elements within the tr and td tags. We then call the remove method on the content contained within the elements. A new selector is instantiated with the content contained in the hidden field that is structured as JSON data. We utilize the jQuery for each method together with the jQuery-JSON plugin’s evalJSON method. Each data element in the hidden field is passed into another function that will append the filtered content back into the selector that we previously cleared. Lets now have a look and the getSelectedFilteredMapsHtml function.


    function getSelectedFilteredmapsHtml(mapData, criteria, filterType) {   
        if (filterHasData(mapData, criteria, filterType)) {   

return "<tr id=" + "maplocations-maplist-row-" + mapData.mapId + "" + " stateId=" + mapData.StateId

                + " countyId=" + mapData.CountyId + " zipId=" + mapData.ZipCodeId + ">"   
                + "<td id='storeName'>" + mapData.StoreName + "</td>"   
                + "<td id='sales'>" + mapData.Sales + "</td>"   
                + "<td id='numEmployees'>" + mapData.NumberOfEmployees + "</td>"   
                + "<td id='includemaps'>"  
                + "<span class='form-item'" + ">"  
                + "<input id=" + "save-maplocations-" + mapData.mapId + " type=button" +  " value='Include Location'" + "></input>"  
                + "</span>"  
                + "</td></tr>"; 

}

              }






Clearly there will not be any row returned if the filter contains no data. The filterHasData function compares the stateId within the JSON data to the value of the selectors changed stateId. If they are equal then we return and append content back to the div we cleared earlier. The filterHasData function is quite simple and shown below.


            function filterHasData(mapData, criteria, filterType) {
                switch (filterType) {
                    case "state":
                        $("#list-maplocations-county").val("");
                        $("#list-maplocations-zip").val(""); 
                        return (mapData.StateId == criteria);
                        break;
                    case "county":
                        $("#list-maplocations-state").val("");
                        $("#list-maplocations-zip").val("");                        
                        return (mapData.CountyId == criteria);
                        break;
                    case "zip":
                        $("#list-maplocations-state").val("");
                        $("#list-maplocations-county").val("");
                        return (mapData.ZipCodeId == criteria);
                        break;
                    default:
                        return null;
                }
            }



What has become quite evident to me from having dabbled in a couple of MVC frameworks is the importance of knowing how to use JavaScript correctly. Adding a few extensions to it really assists you in developing smarter and more powerful UI experiences. For added assistance I would recommend the following books:



  1. Manning – jQuery in Action, Bear Bibeault & Yehuda Katz
  2. O’Reilly – JavaScript: The Good Parts, Douglas Crockford

I hope to post a detailed solution with all code in the near future. I appreciate any comments or alternatives the approach I have taken.