Using Knockout View Models in an AngularJS App
Wednesday, November 19, 2014
Angular 1.3 added an ngModelOptions directive that brings some flexibility in the way ngModel does databinding. One interesting option is getterSetter, which tells Angular that the model is actually a getter/setter function.
Before I got into Angular over a year ago, my frontend framework of choice was Knockout. Knockout’s view models consist of observables, observable arrays, and computeds, which are all implemented as getter/setters. So when I saw that Angular added support for binding to getters and setters, I knew I had to try to see if I can use Knockout view models in an Angular app.
So can you use a knockout view model – without modification – in an angular app? It turns out the answer is yes! I’m not sure if it has any real-world uses, but it was definitely an interesting exercise.
I began with a demo from Knockout’s site. It’s a shopping cart that is built with observables, observable arrays, and computeds.
Here are the Cart and CartLine view models from the Knockout app that we will reuse in the Angular app…
var CartLine = function() {
var self = this;
self.category = ko.observable();
self.product = ko.observable();
self.quantity = ko.observable(1);
self.subtotal = ko.computed(function() {
return self.product() ? self.product().price * parseInt("0" + self.quantity(), 10) : 0;
});
// Whenever the category changes, reset the product selection
self.category.subscribe(function() {
self.product(undefined);
});
};
var Cart = function() {
// Stores an array of lines, and from these, can work out the grandTotal
var self = this;
self.lines = ko.observableArray([new CartLine()]); // Put one line in by default
self.grandTotal = ko.computed(function() {
var total = 0;
$.each(self.lines(), function() { total += this.subtotal() })
return total;
});
// Operations
self.addLine = function() { self.lines.push(new CartLine()) };
self.removeLine = function(line) { self.lines.remove(line) };
self.save = function() {
var dataToSave = $.map(self.lines(), function(line) {
return line.product() ? {
productName: line.product().name,
quantity: line.quantity()
} : undefined
});
alert("Could now send this to server: " + JSON.stringify(dataToSave));
};
};
To use it in Angular, we need to first create a module and a controller. The controller couldn’t be simpler, we just need to new up the Cart view model and include the same list of products used by the Knockout demo app…
angular.module('cart', [])
.controller('CartController', CartController);
function CartController() {
this.cart = new Cart();
this.sampleProductCategories = sampleProductCategories;
}
Here is the original shopping cart with Knockout databinding…
<tbody data-bind='foreach: lines'>
<tr>
<td>
<select data-bind='options: sampleProductCategories, optionsText: "name", optionsCaption: "Select...", value: category'> </select>
</td>
<td data-bind="with: category">
<select data-bind='options: products, optionsText: "name", optionsCaption: "Select...", value: $parent.product'> </select>
</td>
<td class='price' data-bind='with: product'>
<span data-bind='text: formatCurrency(price)'> </span>
</td>
<td class='quantity'>
<input data-bind='visible: product, value: quantity, valueUpdate: "afterkeydown"' />
</td>
<td class='price'>
<span data-bind='visible: product, text: formatCurrency(subtotal())'> </span>
</td>
<td>
<a href='#' data-bind='click: $parent.removeLine'>Remove</a>
</td>
</tr>
</tbody>
And here’s what it looks like converted to Angular.
<tbody>
<tr ng-repeat="line in vm.cart.lines()">
<td>
<select ng-options="category.name for category in vm.sampleProductCategories"
ng-model="line.category" ng-model-options="{ getterSetter: true }">
<option value="">Select...</option>
</select>
</td>
<td>
<select ng-options="product.name for product in line.category().products"
ng-model="line.product" ng-model-options="{ getterSetter: true }"
ng-show="line.category()">
<option value="">Select...</option>
</select>
</td>
<td class='price'>
<span ng-bind='line.product().price | currency'></span>
</td>
<td class='quantity'>
<input ng-show="line.product()" ng-model="line.quantity" ng-model-options="{ getterSetter: true }" />
</td>
<td class='price'>
<span ng-show="line.product()" ng-bind="line.subtotal() | currency"></span>
</td>
<td>
<a href='#' ng-click="vm.cart.removeLine(line)">Remove</a>
</td>
</tr>
</tbody>
The conversions are straight forward.
Knockout binding
Angular equivalent
foreach
ng-repeat
options
ng-options
value
ng-model with getterSetter:true
visible
ng-show
formatCurrency (custom fn)
currency filter (built-in)
click
ng-click
The new Angular app works exactly like the original Knockout app. It’d be interesting to see a more complex view model that makes REST API calls, for example. There would definitely need to be some extra work to make sure the Angular digest cycle is triggered properly. But I think if the original view models followed the Single Responsibility Principle and took data services as dependencies, that might actually be doable. Maybe I’ll give it a try next time.
The source code can be found here: https://github.com/anthonychu/knockout-in-angular
And here is a link to the working demo.
Angular 1.3 added an ngModelOptions directive that brings some flexibility in the way ngModel does databinding. One interesting option is getterSetter, which tells Angular that the model is actually a getter/setter function.
Before I got into Angular over a year ago, my frontend framework of choice was Knockout. Knockout’s view models consist of observables, observable arrays, and computeds, which are all implemented as getter/setters. So when I saw that Angular added support for binding to getters and setters, I knew I had to try to see if I can use Knockout view models in an Angular app.
So can you use a knockout view model – without modification – in an angular app? It turns out the answer is yes! I’m not sure if it has any real-world uses, but it was definitely an interesting exercise.
I began with a demo from Knockout’s site. It’s a shopping cart that is built with observables, observable arrays, and computeds.
Here are the Cart and CartLine view models from the Knockout app that we will reuse in the Angular app…
var CartLine = function() {
var self = this;
self.category = ko.observable();
self.product = ko.observable();
self.quantity = ko.observable(1);
self.subtotal = ko.computed(function() {
return self.product() ? self.product().price * parseInt("0" + self.quantity(), 10) : 0;
});
// Whenever the category changes, reset the product selection
self.category.subscribe(function() {
self.product(undefined);
});
};
var Cart = function() {
// Stores an array of lines, and from these, can work out the grandTotal
var self = this;
self.lines = ko.observableArray([new CartLine()]); // Put one line in by default
self.grandTotal = ko.computed(function() {
var total = 0;
$.each(self.lines(), function() { total += this.subtotal() })
return total;
});
// Operations
self.addLine = function() { self.lines.push(new CartLine()) };
self.removeLine = function(line) { self.lines.remove(line) };
self.save = function() {
var dataToSave = $.map(self.lines(), function(line) {
return line.product() ? {
productName: line.product().name,
quantity: line.quantity()
} : undefined
});
alert("Could now send this to server: " + JSON.stringify(dataToSave));
};
};
To use it in Angular, we need to first create a module and a controller. The controller couldn’t be simpler, we just need to new up the Cart view model and include the same list of products used by the Knockout demo app…
angular.module('cart', [])
.controller('CartController', CartController);
function CartController() {
this.cart = new Cart();
this.sampleProductCategories = sampleProductCategories;
}
Here is the original shopping cart with Knockout databinding…
<tbody data-bind='foreach: lines'>
<tr>
<td>
<select data-bind='options: sampleProductCategories, optionsText: "name", optionsCaption: "Select...", value: category'> </select>
</td>
<td data-bind="with: category">
<select data-bind='options: products, optionsText: "name", optionsCaption: "Select...", value: $parent.product'> </select>
</td>
<td class='price' data-bind='with: product'>
<span data-bind='text: formatCurrency(price)'> </span>
</td>
<td class='quantity'>
<input data-bind='visible: product, value: quantity, valueUpdate: "afterkeydown"' />
</td>
<td class='price'>
<span data-bind='visible: product, text: formatCurrency(subtotal())'> </span>
</td>
<td>
<a href='#' data-bind='click: $parent.removeLine'>Remove</a>
</td>
</tr>
</tbody>
And here’s what it looks like converted to Angular.
<tbody>
<tr ng-repeat="line in vm.cart.lines()">
<td>
<select ng-options="category.name for category in vm.sampleProductCategories"
ng-model="line.category" ng-model-options="{ getterSetter: true }">
<option value="">Select...</option>
</select>
</td>
<td>
<select ng-options="product.name for product in line.category().products"
ng-model="line.product" ng-model-options="{ getterSetter: true }"
ng-show="line.category()">
<option value="">Select...</option>
</select>
</td>
<td class='price'>
<span ng-bind='line.product().price | currency'></span>
</td>
<td class='quantity'>
<input ng-show="line.product()" ng-model="line.quantity" ng-model-options="{ getterSetter: true }" />
</td>
<td class='price'>
<span ng-show="line.product()" ng-bind="line.subtotal() | currency"></span>
</td>
<td>
<a href='#' ng-click="vm.cart.removeLine(line)">Remove</a>
</td>
</tr>
</tbody>
The conversions are straight forward.
Knockout binding | Angular equivalent |
foreach | ng-repeat |
options | ng-options |
value | ng-model with getterSetter:true |
visible | ng-show |
formatCurrency (custom fn) | currency filter (built-in) |
click | ng-click |
The new Angular app works exactly like the original Knockout app. It’d be interesting to see a more complex view model that makes REST API calls, for example. There would definitely need to be some extra work to make sure the Angular digest cycle is triggered properly. But I think if the original view models followed the Single Responsibility Principle and took data services as dependencies, that might actually be doable. Maybe I’ll give it a try next time.
The source code can be found here: https://github.com/anthonychu/knockout-in-angular
And here is a link to the working demo.