This post is part of my weekly tech learning series, where I take one hour each week to try out a piece of technology that I'd like to learn.
This week I decided to use my hour to learn a bit more about Knockout.js. Knockout is described as a Model-View-View Model library.
MVVM
Coming from Rails, I know what a Model-View-Controller is but the View Model is new to me. After going through some examples, it seems like knockout's View Model is what I've been hearing described as a Presenter in the Rails community. Basically something that formats data from the Model for the View.
Basics of Knockout
After I understood a bit more about where knockout.js would fit in the stack, I decided to implement a browser based todo list.
(For reasons described in my CoffeeScript week, a todo list application is something I've done several times and is something that I have a good understanding of the domain. That way most of the time can be used learning the technology itself, rather than the domain).
I started going through knockout's documentation and came across its interactive tutorials. This let me go through each tutorial directly in the browser and pick up enough to work on the todo application.
Sidebar: this is one reason I've become really interested in JavaScript, that you can easily make interactive documentation for your users and help them get started. Easier successes helps learning.
I ended up spending about 45 minutes going through two tutorials so I only had 15 minutes left to build the application.
Building a todo list with knockout.js
First I grabbed a HTML5 broiler-plate and downloaded knockout.js. Since this was going to just be HTML and client side JavaScript I grabbed a rackup file I use to host static sites and had my page hosted easily (code below).
There were five things I wanted to application to do:
- List all open todo items
- Display how many open and completed todo items there are
- Add a new todo item
- Delete a todo item
- Mark a todo item as complete
Knockout.js uses the data-bind attribute to bind on different elements and provide data replacement or simple flow control (e.g. if foreach
).
For listing all of the todo items it was simple to use the foreach
binding and connect it to an array of todo items I created in my TodoViewModel. This let me make a basic template for each todo item to be a HTML li
element.
The data binding can also run JavaScript functions so getting a real-time count of the todos was easy: data-bind="text: todos().length"
.
Both of these features were covered in the two tutorials I read, but getting a form to work was something I had to lookup. Like the others, I had to use the data-bind attribute but this time I used the "submit" type which overrides the form's submission event and runs a JavaScript function instead.
Unfortunately I didn't have enough time to finish the deleting and completing of todo items. I have the code for it but it wasn't working right and I haven't debugged it yet.
Screencast
Here is a screencast of the application with a short walkthough of the code.
Summary
I really liked knockout.js. It make connecting JavaScript data to the HTML elements really easy. Like I said above, it took me about 15 minutes to build most of a todo application with it once I went through the tutorials. I can think of a few applications where knockout would let me simplify my data logic a lot.
I'm interested to contrast knockout.js with backbone.js, as it seems like "everyone" is using backbone.js.
Code
This is a Rack config file I use to host static sites. It is great for working with pure JavaScript applications and hosting them on Heroku.
\# config.ru
use(Rack::Static, :urls => \["/js", "/css", "/images"\])
run lambda { |env|
\[
200,
{
'Content-Type' => 'text/html',
'Cache-Control' => 'public, max-age=86400'
},
File.open('index.html', File::RDONLY)
\]
}
Basic HTML5 page (index.html). Knockout.js only uses the data-bind attributes, the data-class was something extra I thought would be useful, but wasn't.
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<h1>Todo</h1>
<ol id="todos" data-bind="foreach: todos">
<li>
<a href="#" data-class="todo-complete" data-bind="click: $root.completeTodo">[O]</a>
<span data-class="todo-item" data-bind="text: name"></span>
<a href="#" data-class="todo-delete" data-bind="click: $root.removeTodo">Delete</a>
</li>
</ol>
<p><span data-bind="text: todos().length"></span> items left. <span data-bind="text: todosCompleted().length"></span> completed.</p>
<h3>Add</h3>
<form data-bind="submit: addNewTodo">
<p>
<input id="todoName" name="name" />
<button type="submit">Add</button>
</p>
</form>
<!-- JS -->
<script type='text/javascript' src='js/knockout-2.1.0.js'></script>
<script type='text/javascript' src='js/app.js'></script>
</body>
</html>
Here is the guts of the todo list application. I was working on the removeTodo() and completeTodo() functions when I ran out of time. Overall, there isn't that much code to completely bind it to the HTML.
// js/app.js
function TodoItem(name) {
var self = this;
self.name = name;
}
function TodoViewModel() {
var self = this;
self.todos = ko.observableArray(\[
new TodoItem("Walk the dog"),
new TodoItem("Upgrade laptop"),
new TodoItem("Learn knockout.js"),
\]);
self.todosCompleted = ko.observableArray(\[\]);
// Add todo item to the list of todos
self.addNewTodo = function(form) {
var todoName = document.getElementById('todoName').value;
if (todoName) {
self.todos.push(new TodoItem(todoName));
}
}
self.removeTodo = function(todo) {
self.todos.remove(todo);
}
self.completeTodo = function(todo) {
self.todosCompleted.push(todo);
self.removeTodo(todo);
}
}
ko.applyBindings(new TodoViewModel());