Commit b7c5c920 authored by Kevin Malakoff's avatar Kevin Malakoff Committed by Sam Saccone

Update Knockback to Plain Old JavaScript

parent 083ba9fe
{print} = require 'util'
{spawn} = require 'child_process'
task 'build', 'Build js/ from src/', ->
coffee = spawn 'coffee', ['-c', '-o', 'js', 'src']
coffee.stderr.on 'data', (data) ->
message = data.toString()
if message.search('is now called') < 0
process.stderr.write message
coffee.stdout.on 'data', (data) ->
print data.toString()
coffee.on 'exit', (code) ->
callback?() if code is 0
task 'watch', 'Watch src/ for changes', ->
coffee = spawn 'coffee', ['-w', '-c', '-o', 'js', 'src']
coffee.stderr.on 'data', (data) ->
process.stderr.write data.toString()
coffee.stdout.on 'data', (data) ->
print data.toString()
\ No newline at end of file
......@@ -12,8 +12,8 @@
<h1>todos</h1>
<input class="new-todo" type="text" data-bind="value: title, valueUpdate: 'afterkeydown', event: {keyup: onAddTodo}" placeholder="What needs to be done?" autofocus>
</header>
<section class="main" data-bind="visible: tasks_exist">
<input class="toggle-all" type="checkbox" data-bind="checked: all_completed">
<section class="main" data-bind="visible: todoStats().tasksExist">
<input class="toggle-all" type="checkbox" data-bind="checked: toggleCompleted">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list" data-bind="foreach: todos">
<li data-bind="css: {completed: completed, editing: editing}">
......@@ -22,24 +22,27 @@
<label data-bind="text: title"></label>
<button class="destroy" data-bind="click: onDestroy"></button>
</div>
<input class="edit" type="text" data-bind="value: edit_title, event: {blur: onCheckEditEnd, keyup: onCheckEditEnd}">
<input class="edit" type="text" data-bind="value: editTitle, event: {blur: onCheckEditEnd, keyup: onCheckEditEnd}">
</li>
</ul>
</section>
<footer class="footer" data-bind="visible: tasks_exist">
<span class="todo-count" data-bind="html: loc.remaining_message"></span>
<footer class="footer" data-bind="visible: todoStats().tasksExist">
<span class="todo-count">
<strong data-bind="text: todoStats().remainingCount">0</strong>
<span data-bind="text: getLabel(todoStats().remainingCount)"></span> left
</span>
<ul class="filters">
<li>
<a href="#/" data-bind="css: {selected: list_filter_mode()==''}">All</a>
<a href="#/" data-bind="css: {selected: filterMode()==''}">All</a>
</li>
<li>
<a href="#/active" data-bind="css: {selected: list_filter_mode()=='active'}">Active</a>
<a href="#/active" data-bind="css: {selected: filterMode()=='active'}">Active</a>
</li>
<li>
<a href="#/completed" data-bind="css: {selected: list_filter_mode()=='completed'}">Completed</a>
<a href="#/completed" data-bind="css: {selected: filterMode()=='completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" data-bind="text: loc.clear_message, visible: loc.clear_message, click: onClearCompleted"></button>
<button class="clear-completed" data-bind="visible: todoStats().completedCount, click: onClearCompleted">Clear completed</button>
</footer>
</section>
<footer class="info">
......@@ -61,7 +64,8 @@
<script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script>
<!-- App and Components -->
<script src="js/models/todo_collection.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script>
<script src="js/viewmodels/todo.js"></script>
<script src="js/viewmodels/app.js"></script>
</body>
......
/*global Backbone */
var app = app || {};
(function () {
'use strict';
// Todo Collection
// ---------------
// The collection of todos is backed by *localStorage* instead of a remote server.
app.Todos = Backbone.Collection.extend({
// Reference to this collection's model.
model: app.Todo,
// Save all of the todo items under the `"todos"` namespace.
localStorage: new Backbone.LocalStorage('todos-knockback')
});
})();
/*global Backbone */
var app = app || {};
(function () {
'use strict';
// Todo Model
// ----------
// Our basic **Todo** model has `title` and `completed` attributes.
app.Todo = Backbone.Model.extend({
// Default attributes for the todo
defaults: {
title: '',
completed: false
}
});
})();
// Generated by CoffeeScript 1.10.0
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
window.TodoCollection = (function(superClass) {
extend(TodoCollection, superClass);
function TodoCollection() {
return TodoCollection.__super__.constructor.apply(this, arguments);
}
TodoCollection.prototype.localStorage = new Store('todos-knockback');
return TodoCollection;
})(Backbone.Collection);
}).call(this);
// Generated by CoffeeScript 1.10.0
(function() {
var ENTER_KEY,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
/*global Knockback */
var app = app || {};
ENTER_KEY = 13;
(function () {
'use strict';
window.AppViewModel = (function() {
function AppViewModel() {
this.onClearCompleted = bind(this.onClearCompleted, this);
this.onAddTodo = bind(this.onAddTodo, this);
var filter_fn, fn, i, len, ref, route, router;
this.list_filter_mode = ko.observable('');
filter_fn = ko.computed((function(_this) {
return function() {
switch (_this.list_filter_mode()) {
case 'active':
return (function(model) {
return !model.get('completed');
});
case 'completed':
return (function(model) {
return model.get('completed');
});
default:
return (function() {
return true;
});
}
};
})(this));
this.todos = kb.collectionObservable(new TodoCollection(), TodoViewModel, {
filters: filter_fn
});
this.todos.collection().fetch();
this._todos_changed = kb.triggeredObservable(this.todos.collection(), 'change add remove');
this.tasks_exist = ko.computed((function(_this) {
return function() {
_this._todos_changed();
return !!_this.todos.collection().models.length;
};
})(this));
this.title = ko.observable('');
this.completed_count = ko.computed((function(_this) {
return function() {
_this._todos_changed();
return _this.todos.collection().filter(function(model) {
return model.get('completed');
}).length;
};
})(this));
this.remaining_count = ko.computed((function(_this) {
return function() {
_this._todos_changed();
return _this.todos.collection().length - _this.completed_count();
};
})(this));
this.all_completed = ko.computed({
read: (function(_this) {
return function() {
return !_this.remaining_count();
};
})(this),
write: (function(_this) {
return function(completed) {
return _this.todos.collection().each(function(model) {
return model.save({
completed: completed
});
});
};
})(this)
});
this.loc = {
remaining_message: ko.computed((function(_this) {
return function() {
return "<strong>" + (_this.remaining_count()) + "</strong> " + (_this.remaining_count() === 1 ? 'item' : 'items') + " left";
};
})(this)),
clear_message: ko.computed((function(_this) {
return function() {
if (_this.completed_count()) {
return 'Clear completed';
} else {
return '';
}
};
})(this))
};
router = new Backbone.Router;
ref = ['', 'active', 'completed'];
fn = (function(_this) {
return function(route) {
return router.route(route, null, function() {
return _this.list_filter_mode(route);
});
};
})(this);
for (i = 0, len = ref.length; i < len; i++) {
route = ref[i];
fn(route);
}
Backbone.history.start();
}
var ENTER_KEY = 13;
AppViewModel.prototype.onAddTodo = function(vm, event) {
if (!$.trim(this.title()) || (event.keyCode !== ENTER_KEY)) {
return;
}
this.todos.collection().create({
title: $.trim(this.title())
});
return this.title('');
};
// Application View Model
// ---------------
AppViewModel.prototype.onClearCompleted = function() {
var i, len, model, ref, results;
ref = this.todos.collection().filter(function(model) {
return model.get('completed');
});
results = [];
for (i = 0, len = ref.length; i < len; i++) {
model = ref[i];
results.push(model.destroy());
}
return results;
};
// The application View Model is created and bound from the HTML using kb-inject.
window.AppViewModel = kb.ViewModel.extend({
constructor: function () {
var self = this;
kb.ViewModel.prototype.constructor.call(this);
return AppViewModel;
// New todo title.
this.title = ko.observable('');
})();
// The function used for filtering is dynamically selected based on the filterMode.
this.filterMode = ko.observable('');
var filterFn = ko.computed(function () {
switch (self.filterMode()) {
case 'active':
return (function (model) { return !model.get('completed'); });
case 'completed':
return (function (model) { return model.get('completed'); });
};
return (function () { return true; });
});
}).call(this);
// A collectionObservable can be used to hold the instance of the collection.
this.todos = kb.collectionObservable(new app.Todos(), app.TodoViewModel, {filters: filterFn});
// Note: collectionObservables do not track nested model attribute changes by design to avoid
// list redrawing when models change so changes need to be manually tracked and triggered.
this.todoAttributesTrigger = kb.triggeredObservable(this.todos.collection(), 'change add remove');
this.todoStats = ko.computed(function () {
self.todoAttributesTrigger(); // manual dependency on model attribute changes
return {
tasksExist: !!self.todos.collection().length,
completedCount: self.todos.collection().where({completed: true}).length,
remainingCount: self.todos.collection().where({completed: false}).length
};
});
// When the checkbox state is written to the observable, all of the models are updated
this.toggleCompleted = ko.computed({
read: function () { return !self.todoStats().remainingCount; },
write: function (value) { self.todos.collection().each(function (model) { model.save({completed: value}); }); }
});
// Fetch the todos and the collectionObservable will update once the models are loaded
this.todos.collection().fetch();
// Use a Backbone router to update the filter mode
new Backbone.Router().route('*filter', null, function (filter) { self.filterMode(filter || ''); });
Backbone.history.start();
},
// Create a new model in the underlying collection and the observable will automatically synchronize
onAddTodo: function (self, e) {
if (e.keyCode === ENTER_KEY && $.trim(self.title())) {
self.todos.collection().create({title: $.trim(self.title())});
self.title('');
}
},
// Operate on the underlying collection instead of the observable given the observable could be filtered
onClearCompleted: function (self) { _.invoke(self.todos.collection().where({completed: true}), 'destroy'); },
// Helper function to keep expressions out of markup
getLabel: function (count) { return ko.utils.unwrapObservable(count) === 1 ? 'item' : 'items'; }
});
})();
// Generated by CoffeeScript 1.10.0
(function() {
var ENTER_KEY, ESCAPE_KEY,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
ENTER_KEY = 13;
ESCAPE_KEY = 27;
window.TodoViewModel = (function(superClass) {
extend(TodoViewModel, superClass);
function TodoViewModel(model, options) {
this.onCheckEditEnd = bind(this.onCheckEditEnd, this);
this.onCheckEditBegin = bind(this.onCheckEditBegin, this);
this.onDestroy = bind(this.onDestroy, this);
TodoViewModel.__super__.constructor.call(this, model, {
requires: ['title', 'completed']
}, options);
this.completed.subscribe((function(_this) {
return function(completed) {
return _this.model().save({
completed: completed
});
};
})(this));
this.edit_title = ko.observable();
this.editing = ko.observable(false);
}
TodoViewModel.prototype.onDestroy = function() {
return this.model().destroy();
};
TodoViewModel.prototype.onCheckEditBegin = function() {
if (this.editing()) {
return;
}
this.edit_title(this.title());
this.editing(true);
return $('.todo-input').focus();
};
TodoViewModel.prototype.onCheckEditEnd = function(vm, event) {
var title;
if (!this.editing()) {
return;
}
if (event.keyCode === ESCAPE_KEY) {
this.editing(false);
}
if ((event.keyCode === ENTER_KEY) || (event.type === 'blur')) {
$('.todo-input').blur();
title = this.edit_title();
if ($.trim(title)) {
this.model().set({
title: title
}).save({
title: $.trim(title)
});
} else {
_.defer((function(_this) {
return function() {
return _this.model().destroy();
};
})(this));
}
return this.editing(false);
}
};
return TodoViewModel;
})(kb.ViewModel);
}).call(this);
/*global Knockback */
var app = app || {};
(function () {
'use strict';
var ENTER_KEY = 13;
var ESC_KEY = 27;
// Todo View Model
// ---------------
app.TodoViewModel = kb.ViewModel.extend({
constructor: function (model, options) {
// 'keys' option ensures two-way observables are created only for the title and completed attributes.
// 'requires' option allows observables to be created for any attributes in addition to ensuring title and completed.
// 'excludes' option blocks observables being created for specific attributes.
kb.ViewModel.prototype.constructor.call(this, model, {keys: ['title', 'completed']}, options);
// Use editTitle to delay updating model attributes until changes are accepted.
this.editTitle = ko.observable();
this.editing = ko.observable(false);
// Subscribe to changes in completed so that they can be saved automatically
this.completed.subscribe(function (completed) { this.model().save({completed: completed}); }.bind(this));
},
onDestroy: function (self) { self.model().destroy(); },
// Start editing if not already editing.
onCheckEditBegin: function (self) {
if (!self.editing()) {
self.editTitle(self.title());
self.editing(true);
$('.todo-input').focus(); // give the input focus
}
},
// Stop editing if already editing.
onCheckEditEnd: function (self, event) {
if (self.editing()) {
if (event.keyCode === ESC_KEY) {
self.editing(false);
}
if ((event.keyCode === ENTER_KEY) || (event.type === 'blur')) {
self.editing(false);
// Save the editTitle in the model's title or delete the model if blank
var title = self.editTitle();
$.trim(title) ? self.model().save({title: $.trim(title)}) : self.model().destroy();
}
}
}
});
})();
......@@ -18,22 +18,3 @@ Here are some links you may find helpful:
* [Knockback.js on Twitter](http://twitter.com/knockbackjs)
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Running
This app is written in [CoffeeScript](http://coffeescript.org/). If you wish to make changes, follow these steps to re-compile the code.
To install CoffeeScript globally:
npm install -g coffee-script
To compile once:
# from examples/knockback
cake build
To compile on save
# from examples/knockback
cake watch
class window.TodoCollection extends Backbone.Collection
localStorage: new Store('todos-knockback') # Save all of the todos under the "todos-knockback" namespace.
ENTER_KEY = 13
class window.AppViewModel
constructor: ->
@list_filter_mode = ko.observable('')
filter_fn = ko.computed(=>
switch @list_filter_mode()
when 'active' then return ((model) -> return !model.get('completed'))
when 'completed' then return ((model) -> return model.get('completed'))
else return (-> return true)
)
@todos = kb.collectionObservable(new TodoCollection(), TodoViewModel, {filters: filter_fn})
@todos.collection().fetch()
# Note: collectionObservables only track additions and removals so model attribute changes need to be manually triggered
@_todos_changed = kb.triggeredObservable(@todos.collection(), 'change add remove')
@tasks_exist = ko.computed(=> @_todos_changed(); return !!@todos.collection().models.length)
@title = ko.observable('')
@completed_count = ko.computed(=> @_todos_changed(); return @todos.collection().filter((model) -> return model.get('completed')).length)
@remaining_count = ko.computed(=> @_todos_changed(); return @todos.collection().length - @completed_count())
@all_completed = ko.computed({
read: => return not @remaining_count()
write: (completed) => @todos.collection().each((model) -> model.save({completed}))
})
@loc =
remaining_message: ko.computed(=> return "<strong>#{@remaining_count()}</strong> #{if @remaining_count() == 1 then 'item' else 'items'} left")
clear_message: ko.computed(=> return if @completed_count() then 'Clear completed' else '')
router = new Backbone.Router
for route in ['', 'active', 'completed'] then do (route) =>
router.route(route, null, => @list_filter_mode(route))
Backbone.history.start()
onAddTodo: (vm, event) =>
return if not $.trim(@title()) or (event.keyCode isnt ENTER_KEY)
@todos.collection().create({title: $.trim(@title())})
@title('')
onClearCompleted: => model.destroy() for model in @todos.collection().filter((model) -> return model.get('completed'))
ENTER_KEY = 13
ESCAPE_KEY = 27
class window.TodoViewModel extends kb.ViewModel
constructor: (model, options) ->
super(model, {requires: ['title', 'completed']}, options)
@completed.subscribe((completed) => @model().save({completed}))
@edit_title = ko.observable()
@editing = ko.observable(false)
onDestroy: => @model().destroy()
onCheckEditBegin: =>
return if @editing()
@edit_title(@title())
@editing(true)
$('.todo-input').focus()
onCheckEditEnd: (vm, event) =>
return unless @editing()
if (event.keyCode is ESCAPE_KEY)
@editing(false)
if (event.keyCode is ENTER_KEY) or (event.type is 'blur')
$('.todo-input').blur()
title = @edit_title()
if $.trim(title) then @model().set({title}).save({title: $.trim(title)}) else _.defer(=>@model().destroy())
@editing(false)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment