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.
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.
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.
<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.
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.
$(document).ready(function () {
vm = new CascadeDdlViewModel();
ko.applyBindings(vm);
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);
}
});
$.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.
<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.
var ddlApp = angular.module('ddlApp', []);
ddlApp.controller('ddlController', ['$scope', '$http', function ($scope, $http) {
$scope.categories = [{ CategoryID: undefined, CategoryName: 'Choose a category ...' }];
$scope.selectedCategoryID = undefined;
$scope.products = [{ ProductID: undefined, ProductName: 'Choose a product ...'}];
$scope.selectedProductID = undefined;
$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!');
});
$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.