Because I wanted to see what the hype is all about, I dove in the documentation and tutorials of knockout.js.
The tutorial site is great!
But the first thing that bothered me was the data-bind attribute everywhere.
So I pulled up my sleeves and pimped the 'templates and lists' tutorial
The easiest way to test the code is to copy and paste the gist content from the article in the html and javascript fields of the tutorial site.
The html
The final markup of the tutorial looks like this:
<h2>Your seat reservations (<span data-bind="text: seats().length"></span>)</h2> | |
<table> | |
<thead><tr> | |
<th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th> | |
</tr></thead> | |
<tbody data-bind="template: {name:'reservationTemplate', foreach: seats}"></tbody> | |
</table> | |
<script type="text/x-jquery-tmpl" id="reservationTemplate"> | |
<tr> | |
<td><input data-bind="value: name" /></td> | |
<td><select data-bind="options: availableMeals, value: meal, optionsText: 'mealName'"></select></td> | |
<td data-bind="text: formattedPrice"></td> | |
<td><a href="#" data-bind="click: remove">Remove</a></td> | |
</tr> | |
</script> | |
<h3 data-bind="visible: totalSurcharge() > 0"> | |
Total surcharge: $<span data-bind="text: totalSurcharge().toFixed(2)"></span> | |
</h3> | |
<button data-bind="click: addSeat, enable: seats().length < 5">Reserve another seat</button> |
As you can see the data-bind attribute is all over the place.
And this is my markup:
<h2>Your seat reservations</h2> | |
<table> | |
<thead><tr> | |
<th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th> | |
</tr></thead> | |
<tbody id="seats"></tbody> | |
</table> | |
<p><button id="add-seat">Reserve another seat</button></p> | |
<div id="total-container"> | |
<p>Total seats: <span id="total-seats"></span></p> | |
<p>Total surcharge: <span id="total-surcharge"></span></p> | |
</div> | |
<script type="text/x-jquery-tmpl" id="reservationTemplate"> | |
{{each seats}} | |
<tr> | |
<td><input data-bind="value: name"></td> | |
<td><select data-bind="options: availableMeals, value: meal, optionsText: 'mealName'"></select></td> | |
<td data-bind="text: formattedPrice"></td> | |
<td><a href="#" data-bind="click: remove">Remove</a></td> | |
</tr> | |
{{/each}} | |
</script> |
The data-bind attribute is only in the template.
The other bound elements have an id.
Instead of putting the seat count in the title, I put it together with the surcharge sum.
The tutorial code displays the surcharge sum when there actually is a surcharge, my code displays it from the moment a passenger is present.
The javascript code
The tutorial code looks like this:
// Raw catalog data - would come from the server | |
var availableMeals = [ | |
{ mealName: "Standard (sandwich)", price: 0 }, | |
{ mealName: "Premium (lobster)", price: 34.95 }, | |
{ mealName: "Ultimate (whole zebra)", price: 290 } | |
]; | |
// Class to represent a row in the reservations grid | |
var seatReservation = function(name) { | |
this.name = name; | |
this.availableMeals = availableMeals; | |
this.meal = ko.observable(availableMeals[0]); | |
this.formattedPrice = ko.dependentObservable(function() { | |
var price = this.meal().price; | |
return price ? "$" + price.toFixed(2) : "None"; | |
}, this); | |
this.remove = function() { viewModel.seats.remove(this) } | |
} | |
// Overall viewmodel for this screen, along with initial state | |
var viewModel = { | |
seats: ko.observableArray([ | |
new seatReservation("Steve"), | |
new seatReservation("Bert") | |
]), | |
addSeat: function() { | |
this.seats.push(new seatReservation()); | |
} | |
}; | |
viewModel.totalSurcharge = ko.dependentObservable(function() { | |
var total = 0; | |
for (var i = 0; i < this.seats().length; i++) | |
total += this.seats()[i].meal().price; | |
return total; | |
}, viewModel); | |
ko.applyBindings(viewModel); |
Very readable if you are used to do all the legwork in jQuery.
I'm in the knockout camp if I need to create complicated interfaces from now on.
Before I show you my code I will do a blow-by-blow on what I did and what the hurdles were.
The first thing I wanted was to remove the data-bind attribute from the markup.
I knew someone wrote a jQuery plugin to make this possible.
It works fine until you want to use the plugin to bind the html elements in the template.
Because I didn't want to spend too much time looking for a way to make it work, I left the data-bind attributes.
Instead of using a ko.dependentObservable
to make a sum of the surcharges I added it to the view model.
You should only use the method if the code depends on outside factors.
When I added the remove functionality it didn't work. I did some debugging and found out the this
in the remove function was the view model object where it needs to be the seatReservation object.
I guess the unobtrusive plugin has to do something with it, but the quick solution is to bind the seatReservation object to the remove function.
Because jQuery is loaded I used $.proxy
.
The tutorial adds an anonymous passenger but because that isn't allowed anymore, I added a prompt to the addSeat function.
So my code comes down to:
/** | |
* @preserve Unobtrusive Knockout support library for jQuery | |
* | |
* @author Joel Thoms | |
* @version 1.1 | |
*/ | |
(function($) { | |
if (!$ || !$['fn']) throw new Error('jQuery library is required.'); | |
/** | |
* Private method to recursively render key value pairs into a string | |
* | |
* @param {Object} options Object to render into a string. | |
* @return {string} The string value of the object passed in. | |
*/ | |
function render(options) { | |
var rendered = []; | |
for (var key in options) { | |
var val = options[key]; | |
switch (typeof val) { | |
case 'string': rendered.push(key + ':' + val); break; | |
case 'object': rendered.push(key + ':{' + render(val) + '}'); break; | |
case 'function': rendered.push(key + ':' + val.toString()); break; | |
} | |
} | |
return rendered.join(','); | |
} | |
/** | |
* jQuery extension to handle unobtrusive Knockout data binding. | |
* | |
* @param {Object} options Object to render into a string. | |
* @return {Object} A jQuery object. | |
*/ | |
$['fn']['dataBind'] = $['fn']['dataBind'] || function(options) { | |
return this['each'](function() { | |
var opts = $.extend({}, $['fn']['dataBind']['defaults'], options); | |
var attr = render(opts); | |
if (attr != null && attr != '') { | |
$(this)['attr']('data-bind', attr); | |
} | |
}); | |
}; | |
})(jQuery); | |
// Raw catalog data - would come from the server | |
var availableMeals = [ | |
{ mealName: "Standard (sandwich)", price: 0 }, | |
{ mealName: "Premium (lobster)", price: 34.95 }, | |
{ mealName: "Ultimate (whole zebra)", price: 290 } | |
]; | |
// Class to represent a row in the reservations grid | |
var seatReservation = function(name) { | |
this.name = name; | |
this.availableMeals = availableMeals; | |
this.meal = ko.observable(availableMeals[0]); | |
this.formattedPrice = ko.dependentObservable(function() { | |
var price = this.meal().price; | |
return price ? "$" + price.toFixed(2) : "None"; | |
}, this); | |
var remove = function() {console.log(this.meal()); viewModel.seats.remove(this) }; | |
this.remove = $.proxy(remove,this); | |
} | |
$('#seats').dataBind({ template: "'reservationTemplate'"}); | |
$('#add-seat').dataBind({click: 'addSeat',enable: 'seats().length < 2'}); | |
$('#total-container').dataBind({visible: 'seats().length > 0'}); | |
$('#total-seats').dataBind({text: 'seats().length'}); | |
$('#total-surcharge').dataBind({text: 'totalSurcharge() > 0 ? "$"+totalSurcharge().toFixed(2) : "None"'}); | |
// Overall viewmodel for this screen, along with initial state | |
var viewModel = { | |
seats: ko.observableArray([ | |
new seatReservation("Steve"), | |
new seatReservation("Bert") | |
]), | |
addSeat: function() { | |
var name = prompt("What is the passengers name?", "") | |
this.seats.push(new seatReservation(name)); | |
}, | |
totalSurcharge: function() { | |
var total = 0; | |
for (var i = 0; i < this.seats().length; i++) | |
total += this.seats()[i].meal().price; | |
return total; | |
} | |
}; | |
ko.applyBindings(viewModel); |
Conclusion
Amazing you can achieve so much with so little code. And the tutorial is still verbose because the availableMeals
will be fetched using AJAX or generated by a server language, <?php echo json_encode($availableMeals); ?>
I grabbed it and I will not let it loose until something better comes along.