Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Using Knockout.js to Control Cascading Selects

4.79/5 (11 votes)
23 Jul 2014CPOL6 min read 29.8K   2.4K  
An easy way to control the lists of options in multiple related SELECT controls

Introduction

This article provides the source code to enable you to easily configure a web page containing multiple SELECT controls used to filter a collection of records (usually displayed in a grid or table).  In the example, we use a collection of ficticious residential addresses representing homes for rent.  The page will allow the user to filter this list by state, city, zip code, # of bedrooms and residence type.

The page relies on the data binding provided by Knockout.js library.  This binding causes a change in a user's selection to be immediately reflected in the options available in the other controls.  For example, if the user selects Philadelphia in the City control, then the Zip Code control will only display zips that are in Philadelphia, and so on.

Image 1

See on FiddleJS
Note:  The functionality is abstracted out so it can be applied to any kind of collection of records.  You only need to specify the names of the properties to filter on!

Background

In the past I have repeatedly implemented this functionality in plain javascript.  This worked, but the coding is very tedious and labor-intensive.  I recently started playing with the javascript libray Knockout (http://knockoutjs.com/), so I decided to see if Knockout could make the problem easier.

I made this library flexible, so that:

1.  Any number of select controls can be linked in a heirarchical manner.  You configure each by specifying a property name from the records you want to filter, and optionally a property name corresponding to a parent control.

2.  The controls can be a mix of either drop-downs or multi-select listboxes.  

3.  The controls can be heirarchically related as parents and children to any level, or some of them can be independent of the others.

4.  The controls are created dynamically, so it is not necessary to hard-code each control.  This means that the page could be configured to use variable sets of controls.

Internal Structure

All the internal javascript code was placed in the file SelectFilters.js.  This allows easy portability of the classes.  Multiple web pages in an application can refer to this file.

In SelectFilters.js, two classes are defined.  The first is the sfViewModel  object.  This is a sub-model, contained in the modelView object defined on the page itself.  As with all Knockout pages, the data bindings in the controls on the page refer to properties on the view-model or one of its contained objects.  

The second class is called selectFilter.  One selectFilter object is created for each SELECT control involved, and the SELECT control is bound directly to this object using Knockout data binding.  The sfViewModel has a collection of selectFilter objects.

When a user makes a selection with one of the SELECT controls, a message is sent to the corresponding selectFilter object by virtue of the data-bind attribute defined on the SELECT control:

HTML
<code><select style="vertical-align:top" multiple data-bind="attr: { multiple:multiSelect}, options: availableValues, value: value, selectedOptions: values" ></code>

This object in turn calls <span style="color: black; font-family: Consolas, 'Courier New', Courier, mono; font-size: 9pt; white-space: pre; background-color: rgb(251, 237, 187);"> sfViewModel.resolveSelections()</span><span style="color: black; font-family: Consolas, 'Courier New', Courier, mono; font-size: 9pt; white-space: pre; background-color: rgb(251, 237, 187);"> </span>.  The sfViewModel in turn goes through its list of selectFilter objects and calls  selectFilter.setAvailableOptions()  on each of them, where the option list is recalculated.  Since the option list (availableValues) is a ko.observableArray and is data-bound to the SELECT control's options, the list of options is automatically updated.

In the call to <span style="color: rgb(0, 0, 0); font-family: Consolas, 'Courier New', Courier, mono; font-size: 12px; white-space: pre; background-color: rgb(251, 237, 187);"> viewModel.resolveSelections() </span>, the list of all records is re-filtered based on user selections.  In our page it is data-bound to the table at the bottom of the page with the data-bind attribute:

HTML
<code>    <tbody data-bind="foreach: selectedItems"></code>

Because the viewModel.selectedItems list is also a ko.observableArray, the displayed table is automatically updated as well.  Also in this function the activeFilters array is updated, and it is also data-bound to the list of filtering values above the table.

Files

The most important files in SelectFiltersExample.zip are:

  • SelectFiltersExample/Views/Home/Index.cshtml -- the HTML for the home page.

  • SelectFiltersExample/Scripts/SelectFilters.js -- the 2 javascript classes and a Knockout custom function called loadByProperties.

  • SelectFiltersExample/Scripts/knockout-3.1.0.js -- the Knockout library, downloaded from knockoutjs.com

Anyone wishing to utilize this concept in another environment, such as PHP, just needs to grab those first 2 files from the zip file.

Using the code

You can easily add the included code into your own project.  I chose to implement it with ASP.NET MVC on the back end, but the choice of server platform makes no difference; it is the javascript and html where the action is.  You can use this code with PHP on a Linux server, or whatever.

To run the code in this project, either open the MVC project zip file and load it in Visual Studio, or just add some of the included code to your existing project.

To add this capability to your own web pages, you need to do these things:

1.  Include a <script> reference to the knockout.js library as well as the SelectFilters.js file, like they are in the top of the HTML files in this project (of course copy the js files to your Scripts folder).

2.  Insert some of this HTML into your page...

Here is the HTML for the SELECT controls.  The expression <span style="color: black; font-family: Consolas, 'Courier New', Courier, mono; font-size: 9pt; white-space: pre; background-color: rgb(251, 237, 187);"> data-bind="foreach: selectFilters" </span>  causes the contents inside the <p> to be repeated once for each selectFilter object in the viewmodel's list.

HTML
<code><!-- SELECT controls from the viewModel.selectFilters collection -->
<p data-bind="foreach: selectFilterVM.selectFilters"></code>
<code>
    <span style="font-weight: bold; vertical-align: top"
        data-bind="text: nameLabel">
    </span>

    <select style="vertical-align: top"
        data-bind="attr: { multiple: multiSelect },
                   options: availableValues,
                   value: value,
                   selectedOptions: values">
    </select></code>
</p>

Below is the HTML for an optional list of currently applied filters, with their "clear" link:

HTML
<code><!-- list the currently active filter values -->
<ul data-bind="foreach: selectFilterVM.activeFilters">
    <li>
        <span style="font-weight:bold" data-bind="text: nameLabel"></span>

        <span data-bind="text: valueText"></span>&nbsp;

        <a href="#" data-bind="event:{ click: reset }">clear</a>
    </li>
</ul></code>

3.  On the server side, provide a way to download the records.  For my MVC project I used a jQuery AJAX call that returns JSON in the ready() function:

JavaScript
"font-size: 9pt"><code>        $.getJSON("/Home/GetHomes", model.loadData(model)); </code>

4.  In the page, modify the code to load your filters, indicating what properties of the downloaded records you want the user to filter on, and also which controls are the parents to which other controls.  Here is the example:

JavaScript
<code>    // Define the filtering select controls this way.
    // Parameters to selectFilter() are:
    //   name:         name of property to filter on
    //   parentName:   name of master select control's property
    //   model:        the model object for this view
    //   multiselect:  whether to allow selection of multiple values
    function loadSelects(model) {
        new selectFilter('State', '', model, 'state', true);
        new selectFilter('City', 'State', model, 'city', false);
        new selectFilter('Zip', 'City', model, 'Zip code', true);
        new selectFilter('BRs', 'Zip', model, '# of bedrooms', true);
        new selectFilter('HomeType', 'BRs', model, 'home type', true);
    }</code>

For example, the second selectFilter() call creates a javascript selectFilter object which corresponds to the 'City' SELECT control:  

JavaScript
"font-size: 9pt">^__strong>new selectFilter('City', 'State', model, 'city', false);</code>

The second parameter matches the name passed to the first selectFilter, 'State', so its parent SELECT control is the 'State' listbox.  

The third parameter is the view model reference.  

the fourth parameter supplies a label value to display to the user.  You can change this depending on language, etc.

The fifth parameter indicates multiselect=false, so it will appear as a dropdown box instead of a list box.

It would also be possible to download these lists of parameters from the server as JSON, to dynamically define the SELECT controls.  Just make sure the first and second parameters match property names in the downloaded data records.

Summary

Feel free to play with the code in the HTML page -- for example, change the name of the parent in some of the selectFilter() constructor calls and observe how the page behaves.  You could also change the last parameter from true to false, etc.

I would appreciate any feedback!  I am fairly new to Knockout and I am not the biggest expert on javascript either, so I'm sure someone out there has some good suggestions for how to improve this code.

History

July 17, 2014 : Initial release

July 22, 2014 : I wanted this code to coexist with other Knockout model objects and bindings on the same page, so I renamed the modelView class in SelectFilters.js to make it a sub-model instead of the main viewmodel of the page.  I renamed that class to fsViewModel. Now I added a new viewModel object on the page iteslf, which has a property named selectFilterVM which is the  fsViewModel sub-model containing the filter selects.  All references to the veiwmodel properties in data bindings now have the prefix  selectFilterFM.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)