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

Cascading Dropdown List Implementation by Knockout and AngularJS

5.00/5 (2 votes)
11 Nov 2016CPOL4 min read 12.6K  
How to implement cascading dropdown list by Knockout and AngularJS

Introduction

Knockout and AngularJS are two popular JavaScript frameworks using Model-View-ViewModel(MVVM) design pattern. MVVM has three components.

Model: The persistent data on the server side.
View: The HTML page.
ViewModel: A pure-JavaScript object representing the model.

The Model and ViewModel are normally synchronized by AJAX calls. The ViewModel and View are bound by the JavaScript framework (Knockout or AngularJS). When a view element is changed, the ViewModel is automatically updated by the JavaScript framework. When the ViewModel data is changed, the View is automatically updated by the framework. The framework sychronizes the View and ViewModel automatically to save this part of the code the developer has to do.

Using the code

Server ASP.NET MVC controllers

In this article, ASP.NET MVC framework is used on the server side to return dropdown list data.

First, create two user classes.

C#
public class Category
{
    public int CategoryID { get; set; }
    public string CategoryName { get; set; }
}

public class Product
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public int CategoryID { get; set; }
}

Second, create the following user objects and MVC controller methods to return data for client side framework Knockout and AngularJS.

C#
List<Category> lstCat = new List<Category>()
{
    new Category() { CategoryID = 1, CategoryName = "Dairy" },
    new Category() { CategoryID = 2, CategoryName = "Meat" },
    new Category() { CategoryID = 3, CategoryName = "Vegetable" }
};

List<Product> lstProd = new List<Product>()
{
    new Product() { ProductID = 1, ProductName = "Cheese", CategoryID = 1 },
    new Product() { ProductID = 2, ProductName = "Milk", CategoryID = 1 },
    new Product() { ProductID = 3, ProductName = "Yogurt", CategoryID = 1 },
    new Product() { ProductID = 4, ProductName = "Beef", CategoryID = 2 },
    new Product() { ProductID = 5, ProductName = "Lamb", CategoryID = 2 },
    new Product() { ProductID = 6, ProductName = "Pork", CategoryID = 2 },
    new Product() { ProductID = 7, ProductName = "Broccoli", CategoryID = 3 },
    new Product() { ProductID = 8, ProductName = "Cabbage", CategoryID = 3 },
    new Product() { ProductID = 9, ProductName = "Pepper", CategoryID = 3 }
};

public ActionResult GetCategories()
{
    return Json(lstCat, JsonRequestBehavior.AllowGet);
}

public ActionResult GetProducts(int intCatID)
{
    var products = lstProd.Where(p => p.CategoryID == intCatID);
    return Json(products, JsonRequestBehavior.AllowGet);
}

Cascading dropdown list by Knockout

First, create HTML markups with Knockout custom attributes for the two dropdown lists.

HTML
<div>
    <label for="categoryList">Categories</label>
    <select id="categoryList" data-bind="options: categories,
                                            optionsText: 'CategoryName',
                                            optionsValue: 'CategoryID',
                                            value: selectedCategoryID,
                                            optionsCaption: 'Choose a category ...'">
    </select>
</div>
<div>
    <label for="productList">Products</label>
    <select id="productList" data-bind="options: products,
                                            optionsText: 'ProductName',
                                            optionsValue: 'ProductID',
                                            value: selectedProductID,
                                            optionsCaption: 'Choose a product ...'">
    </select>
</div>

Notice the data-bind attribute inside select element. Knockout uses this attribute to connect View to ViewModel. Here is the explanation of the bind names for select element.

options: An array of objects for the options tags.
optionsText: The text of the option tag.
optionsValue: The value of the option tag.
value: The selected value of the select tag.
optionsCaption: The text of the default first option. The value is undefined.

Second, create a ViewModel object constructor function representing the cascading dropdown list objects.

JavaScript
function CascadeDdlViewModel() {
    this.categories = ko.observableArray();
    this.selectedCategoryID = ko.observable();
    this.products = ko.observableArray();
    this.selectedProductID = ko.observable();
};

Notice the observable and observableArray functions of the top level Knockout object ko. Function observable makes the property being observed. If the property is changed, the UI bound to the property is changed. On the other hand, if the UI bound to the property is changed, the property is changed. Function observableArray makes the array property being observed. Note it only makes array object addition and deletion observable. It won't make any property change inside the object observable.

Third, add the following code to implement cascading dropdown lists.

JavaScript
$(document).ready(function () {
    vm = new CascadeDdlViewModel();
    ko.applyBindings(vm);

    // Subscribe to category dropdown list change.
    vm.selectedCategoryID.subscribe(function (newValue) {
        if (newValue !== undefined) {
            $.getJSON('/home/GetProducts', { intCatID: newValue }, function (data) {
                vm.products(data);
            }).fail(function () {
                alert('Error getting products!');
            });
        }
        else {
            vm.products.removeAll();
            vm.selectedProductID(undefined);
        }
    });

    // Get a list of categories.
    $.getJSON('/home/GetCategories', null, function (data) {
        vm.categories(data);
    }).fail(function () {
        alert('Error getting categories!');
    });
});

The code is wrapped inside jQuery DOM ready function so the code is called after the web page is loaded. The code first creates a view model object. Then it calls Knockout applyBindings function to activate Knockout so it automatically synchronizes the View and ViewModel.

To get notification of the first dropdown list change and repopulate the second dropdown list, explicit subscription is used. The subscribe function of the ViewModel property selectedCategoryID is called to register its subscription to this observed property. When the category dropdown list selection is changed, the property selectedCategoryID which is bound to the value of the dropdown list is changed. When the value of this property is changed, the callback function passed as the parameter of subscribe function is called. The callback function calls jQuery's getJSON function to get products data belonging to the new category and updates the products property value. When products property value is changed, the options list of the products dropdown list is updated. This series of events are automatically handled by Knockout.

The last piece of code simply gets the catagory list from the server and populates the category dropdown list.

Cascading dropdown list by AngularJS

First, create HTML markups with AngularJS directives for the two dropdown lists.

HTML
<div id="ddlAppSection" ng-app="ddlApp" ng-controller="ddlController">
    <h2>Dropdown List Test</h2>
    <div>
        <label for="categoryList">Categories</label>
        <select id="categoryList"
            ng-model="selectedCategoryID"
            ng-options="c.CategoryID as c.CategoryName for c in categories">
        </select>
    </div>
    <div>
        Selected category: {{ selectedCategoryID }}
    </div>
    <div>
        <label for="productList">Products</label>
        <select id="productList"
            ng-model="selectedProductID"
            ng-options="p.ProductID as p.ProductName for p in products">
        </select>
    </div>
    <div>
        Selected product: {{ selectedProductID }}
    </div>
</div>

The custom attributes such as ng-app are AngularJS directives. Here is the explanation of the directives used here.

ng-app: The application name.
ng-controller: The controller name.
ng-model: The object property name which the HTML element binds.
ng-options: The data for option tags inside select tag.

The {{}} syntax is used for AngularJS expression.

Second, create the following JavaScript code to implement cascading dropdown list.

JavaScript
var ddlApp = angular.module('ddlApp', []);
ddlApp.controller('ddlController', ['$scope', '$http', function ($scope, $http) {
    // Define model properties
    $scope.categories = [{ CategoryID: undefined, CategoryName: 'Choose a category ...' }];
    $scope.selectedCategoryID = undefined;
    $scope.products = [{ ProductID: undefined, ProductName: 'Choose a product ...'}];
    $scope.selectedProductID = undefined;

    // Get a list of categories
    $http.get('/home/GetCategories').then(function (response) {
        $scope.categories = response.data;
        $scope.categories.unshift({ CategoryID: undefined, CategoryName: 'Choose a category ...' });
    }, function (errResponse) {
        alert('Error getting categories!');
    });

    // Watch selected category value change and update products array.
    $scope.$watch('selectedCategoryID', function (newValue, oldValue) {
        if (newValue !== undefined) {
            $http.get('/home/GetProducts', { params: { intCatID: newValue} }).then(function (response) {
                $scope.products = response.data;
                $scope.products.unshift({ ProductID: undefined, ProductName: 'Choose a product ...' });
                $scope.selectedProductID = undefined;
            }, function (errResponse) {
                alert('Error getting products!');
            });
        }
        else {
            $scope.products = [{ ProductID: undefined, ProductName: 'Choose a product ...'}];
            $scope.selectedProductID = undefined;
        }
    });
} ]);

This piece of code first creates a module. Then it defines a controller. The controller construction function has two parameters $scope and $http. Object $scope is the view model which is bound to the view. Object $http is an AngularJS service which faciliates AJAX server calls. The method $watch listens to view model property change. In this case, when the selected category id is changed, the function specified by $watch is called. This updates the product dropdown list according to the newly selected category id. AngularJS automatically handles the synchronization between view model and view.

Points of Interest

Both Knockout and AngularJS help web developers create and maintain dynamic web applications. My preference is AngularJS since it has a much bigger developer community.

 

 

License

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