Knockout.js Playground

Experimenting with Knockout.js

by Doug Hill

HTML

<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
<h1>Another Knockout.js ToDo App</h1>
<div class="app-container">
    <button data-bind="click: onAdd">Add another ToDo item</button>
    <span class="remaining" data-bind="text: remaining"></span>
    <ul class="container" data-bind="foreach: todos">
        <li data-bind="visible: editing">
            <span class="action"><a class="cancel" href="#" data-bind="click: onCancel">Cancel</a></span>
            <span class="action"><a class="update" href="#" data-bind="click: onCommit">Update</a></span>
            <input type="text" class="editing" data-bind="value: toDoText" />
        </li>
        <li data-bind="visible: !editing()">
            <span class="action"><a class="remove" href="#" data-bind="click: $parent.onRemove">Remove</a></span>
            <span class="action"><a class="edit" href="#" data-bind="click: onEdit">Edit</a></span>
            <input type="checkbox" class="complete" data-bind="checked: completed" />
            <span class="item" data-bind="css: { complete: completed }, text: toDoText"></span>
        </li>
    </ul>
</div>

CSS

button {
    margin: 10px;
}

h1 {
    font: 2.1em sans-serif;
    padding: 10px;
    margin-bottom: 10px;
}

li {
    padding: 10px 20px;
    margin: 5px 20px;
    border-radius: 5px;
    background: #444;
    color: white;
    font: 1.3em sans-serif;
}

ul {
    background: lightgray;
    padding: 5px;
}


input.complete {
    margin-right: 10px;
}

input.editing {
    width: 65%;
}

span.action {
    float: right;
    margin-left: 10px;
    font-size: 0.8em;
}

span.item {
    text-shadow: 1px 0 3px black;
}

span.item.complete {
    text-decoration: line-through;
}

.app-container{
    position: relative;
}

.action a {
    text-decoration: none;
    color: orange;
}

.action a:hover {
    text-decoration: underline;
    color: orangered;
}

.remaining {
    font: 1.1em sans-serif;
    right: 25px;
    position: absolute;
    margin: 10px;
}

JavaScript

// A ToDo item
function ToDo(item) {
    var self = this;

    item = item || {};

    // Data
    self.completed = ko.observable( !! item.completed);
    self.editing = ko.observable(false);

    self.listText = item.text || 'Edit this ToDo item';
    self.tmpText = '';
    self.toDoText = ko.computed({
        read: function() {
            return self.editing() ? self.tmpText : self.listText;
        },

        write: function(value) {
            if (self.editing()) {
                self.tmpText = value;
            }
        }
    });

    // Operation
    self.onEdit = function() {
        self.tmpText = self.listText;
        self.editing(true);
    };

    self.onCommit = function() {
        self.listText = self.tmpText;
        self.editing(false);
    };

    self.onCancel = function() {
        self.editing(false);
    };
}

// A ToDo model
function ToDoListModel() {
    var self = this;

    // Data    
    self.todos = ko.observableArray([
        new ToDo({
        text: 'Step one: Create great ToDos App',
        completed: true
    }),
        new ToDo({
        text: 'Step two: ???'
    }),
        new ToDo({
        text: 'Step three: Profit'
    })
        ]);
    
    self.remaining = ko.computed(function() {
        var arr = self.todos(),
            len = arr.length,
            i = 0,
            open = 0,
            txt = '';
        
        for (; i < len; i++) {
            if (!arr[i].completed()) open++;
        }
        
        if (open === 0) return 'No remaining items';
        if (open === 1) return '1 remaining item';
        
        return open + ' remaining items';
    });

    // Operations
    self.onRemove = function(todo) {
        self.todos.remove(todo);
    };

    self.onAdd = function() {
        self.todos.push(new ToDo());
    };
}

// Create a new model and apply the bindings
var model = new ToDoListModel();
ko.applyBindings(model);