/** @jsx React.DOM */
function cx(obj) {
var s = '';
for (var key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
if (obj[key]) {
s += key + ' ';
}
}
return s;
}
var Todo = Backbone.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
defaults: {
title: '',
completed: false
},
// Toggle the `completed` state of this todo item.
toggle: function() {
this.save({
completed: !this.get('completed')
});
}
});
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model: Todo,
// Save all of the todo items under the `"todos"` namespace.
localStorage: new Store('todos-react-backbone'),
// Filter down the list of all todo items that are finished.
completed: function() {
return this.filter(function( todo ) {
return todo.get('completed');
});
},
remaining: function() {
return this.without.apply(this, this.completed());
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function () {
if (!this.length) {
return 1;
}
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function (todo) {
return todo.get('order');
}
});
var Utils = {
pluralize: function( count, word ) {
return count === 1 ? word : word + 's';
}
};
// Begin React stuff
var TodoItem = React.createClass({
handleSubmit: React.autoBind(function(event) {
var val = this.refs.editField.getDOMNode().value;
if (val) {
this.props.onSave(val);
}
return false;
}),
onEdit: React.autoBind(function() {
this.props.onEdit();
this.refs.editField.getDOMNode().focus();
}),
render: function() {
return (
);
}
});
var TodoFooter = React.createClass({
render: function() {
var activeTodoWord = Utils.pluralize(this.props.count, 'todo');
var clearButton = null;
if (this.props.completedCount > 0) {
clearButton = (
);
}
return (
);
}
});
// An example generic Mixin that you can add to any component that should react to changes in a Backbone component.
// The use cases we've identified thus far are for Collections -- since they trigger a change event whenever
// any of their constituent items are changed there's no need to reconcile for regular models.
// One caveat: this relies on getBackboneModels() to always return the same model instances throughout the
// lifecycle of the component. If you're using this mixin correctly (it should be near the top of your
// component hierarchy) this should not be an issue.
var BackboneMixin = {
componentDidMount: function() {
// Whenever there may be a change in the Backbone data, trigger a reconcile.
this.getBackboneModels().map(function(model) {
model.on('add change remove', this.forceUpdate, this);
}.bind(this));
},
componentWillUnmount: function() {
// Ensure that we clean up any dangling references when the component is destroyed.
this.getBackboneModels().map(function(model) {
model.off(null, null, this);
}.bind(this));
}
};
var TodoApp = React.createClass({
mixins: [BackboneMixin],
getInitialState: function() {
return {editing: null};
},
componentDidMount: function() {
// Additional functionality for todomvc: fetch() the collection on init
this.props.todos.fetch();
},
componentDidUpdate: function() {
// If saving were expensive we'd listen for mutation events on Backbone and do this manually.
// however, since saving isn't expensive this is an elegant way to keep it reactively up-to-date.
this.props.todos.map(function(todo) {
todo.save();
});
},
getBackboneModels: function() {
return [this.props.todos];
},
handleSubmit: React.autoBind(function() {
var val = this.refs.newField.getDOMNode().value.trim();
if (val) {
this.props.todos.create({
title: val,
completed: false,
order: this.props.todos.nextOrder()
});
this.refs.newField.getDOMNode().value = '';
}
return false;
}),
toggleAll: React.autoBind(function(event) {
var checked = event.nativeEvent.target.checked;
this.props.todos.map(function(todo) {
todo.set('completed', checked);
});
}),
destroy: function(todo) {
this.props.todos.remove(todo);
},
edit: function(todo) {
this.setState({editing: todo.get('id')});
},
save: function(todo, text) {
todo.set('title', text);
this.setState({editing: null});
},
clearCompleted: React.autoBind(function() {
this.props.todos.completed().map(function(todo) {
todo.destroy();
});
}),
render: function() {
var footer = null;
var main = null;
var todoItems = this.props.todos.map(function(todo) {
return ;
}.bind(this));
var activeTodoCount = this.props.todos.remaining().length;
var completedCount = todoItems.length - activeTodoCount;
if (activeTodoCount || completedCount) {
footer = ;
}
if (todoItems.length) {
main = (