Knockout.js Todo Example

HTML

<script src="http://knockoutjs.com/downloads/knockout-2.3.0.js"></script>
<div id="todoapp">
  <header>
    <h1>Todos by Knockout.js</h1>
    <input id="new-todo" type="text" placeholder="What needs to be done?" data-bind="value:inputTitle,event: { keyup: createOnEnter}" />
  </header>

  <section id="main" style="display: block;">
    <div data-bind="visible:todos().length>0">
      <input id="toggle-all" type="checkbox" data-bind="checked:markAll" />
      <label for="toggle-all">Mark all as complete</label>
    </div>
    <ul id="todo-list" data-bind="template:{ name:'item-template',foreach: todos}">
    </ul>
  </section>

  <footer style="display: block;">
    <div data-bind="visible:todos().length>0">
      <div class="todo-count"><b data-bind="text:todos().length"></b> items left</div>
      <!-- ko if: doneTodos() > 0 -->
      <a id="clear-completed" data-bind="click:clear">
              Clear <span data-bind="html:countDoneText(true)"></span>.
          </a>
      <!-- /ko -->
      <br style="clear:both" />
    </div>
  </footer>
</div>

<script type="text/template" id="item-template">
  <li data-bind="event:{ dblclick :$root.toggleEditMode},css : {done:done() }">
    <div class="view">
      <input class="toggle" type="checkbox" data-bind="checked:done" />
      <label data-bind="text:title"></label>
      <a class="destroy"></a>
    </div>
    <input class="edit" type="text" data-bind="value:title,event: { keyup: $root.editOnEnter}" />
  </li>
</script>

CSS

body {
  font-size: 14px;
  background: #eeeeee;
  color: #333333;
  width: 520px;
  margin: 0 auto;
}

#todoapp {
  background: #fff;
  padding: 20px;
  margin-bottom: 40px;
}

#todoapp h1 {
  font-size: 36px;
  text-align: center;
}

#todoapp input[type="text"] {
  width: 466px;
  font-size: 24px;
  line-height: 1.4em;
  padding: 6px;
}

#main {
  display: none;
}

#todo-list {
  margin: 10px 0;
  padding: 0;
  list-style: none;
}

#todo-list li {
  padding: 18px 20px 18px 0;
  position: relative;
  font-size: 24px;
  border-bottom: 1px solid #cccccc;
}

#todo-list li:last-child {
  border-bottom: none;
}

#todo-list li .edit {
  display: none;
}

#todo-list li.editing {
  border-bottom: 1px solid #778899;
}

#todo-list li.editing .view {
  display: none;
}

#todo-list li.editing .edit {
  display: block;
  width: 444px;
  padding: 13px 15px 14px 20px;
  margin: 0;
}

#todo-list li.done label {
  color: #777777;
  text-decoration: line-through;
}

#todo-list .destroy {
  position: absolute;
  right: 5px;
  top: 20px;
  display: none;
  cursor: pointer;
  width: 20px;
  height: 20px;
}

#todoapp footer {
  display: none;
  margin: 0 -20px -20px -20px;
  overflow: hidden;
  color: #555555;
  background: #f4fce8;
  border-top: 1px solid #ededed;
  padding: 0 20px;
  line-height: 37px;
}

.todo-count {
  float: left;
}

.todo-count .count {
  font-weight: bold;
}

#clear-completed {
  float: right;
  line-height: 20px;
  text-decoration: none;
  background: rgba(0, 0, 0, 0.1);
  color: #555555;
  font-size: 11px;
  margin-top: 8px;
  margin-bottom: 8px;
  padding: 0 10px 1px;
  cursor: pointer;
}

JavaScript

$(function() {
  var Todo = function(title, done, order, callback) {
    var self = this;
    self.title = ko.observable(title);
    self.done = ko.observable(done);
    self.order = order;
    self.updateCallback = ko.computed(function() {
      callback(self);
      return true;
    });
  }

  var viewModel = function() {
    var self = this;
    self.todos = ko.observableArray([]);
    self.inputTitle = ko.observable("");
    self.doneTodos = ko.observable(0);
    self.markAll = ko.observable(false);

    self.addOne = function() {
      var order = self.todos().length;
      var t = new Todo(self.inputTitle(), false, order, self.countUpdate);
      self.todos.push(t);
    };

    self.createOnEnter = function(item, event) {
      if (event.keyCode == 13 && self.inputTitle()) {
        self.addOne();
        self.inputTitle("");
      } else {
        return true;
      };
    }

    self.toggleEditMode = function(item, event) {
      $(event.target).closest('li').toggleClass('editing');
    }

    self.editOnEnter = function(item, event) {
      if (event.keyCode == 13 && item.title) {
        item.updateCallback();
        self.toggleEditMode(item, event);
      } else {
        return true;
      };
    }

    self.markAll.subscribe(function(newValue) {
      ko.utils.arrayForEach(self.todos(), function(item) {
        return item.done(newValue);
      });
    });

    self.countUpdate = function(item) {
      var doneArray = ko.utils.arrayFilter(self.todos(), function(it) {
        return it.done();
      });
      self.doneTodos(doneArray.length);
      return true;
    };

    self.countDoneText = function(bool) {
      var cntAll = self.todos().length;
      var cnt = (bool ? self.doneTodos() : cntAll - self.doneTodos());
      var text = "<span class='count'>" + cnt.toString() + "</span>";
      text += (bool ? " completed" : " remaining");
      text += (self.doneTodos() > 1 ? " items" : " item");
      return text;
    }

    self.clear = function()...