249 lines
7.4 KiB
JavaScript
249 lines
7.4 KiB
JavaScript
/** @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 (
|
|
<li class={cx({completed: this.props.todo.get('completed'), editing: this.props.editing})}>
|
|
<div class="view">
|
|
<input
|
|
class="toggle"
|
|
type="checkbox"
|
|
checked={this.props.todo.get('completed') ? 'checked' : null}
|
|
onChange={this.props.onToggle}
|
|
/>
|
|
<label onDoubleClick={this.onEdit}>{this.props.todo.get('title')}</label>
|
|
<button class="destroy" onClick={this.props.onDestroy} />
|
|
</div>
|
|
<form onSubmit={this.handleSubmit}>
|
|
<input ref="editField" class="edit" value={this.props.todo.get('title')} />
|
|
</form>
|
|
</li>
|
|
);
|
|
}
|
|
});
|
|
|
|
var TodoFooter = React.createClass({
|
|
render: function() {
|
|
var activeTodoWord = Utils.pluralize(this.props.count, 'todo');
|
|
var clearButton = null;
|
|
|
|
if (this.props.completedCount > 0) {
|
|
clearButton = (
|
|
<button class="clear-completed" onClick={this.props.onClearCompleted}>Clear completed ({this.props.completedCount})</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<footer class="footer">
|
|
<span class="todo-count"><strong>{this.props.count}</strong>{' '}{activeTodoWord}{' '}left</span>
|
|
{clearButton}
|
|
</footer>
|
|
);
|
|
}
|
|
});
|
|
|
|
// 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 <TodoItem todo={todo} onToggle={todo.toggle.bind(todo)} onDestroy={this.destroy.bind(this, todo)} onEdit={this.edit.bind(this, todo)} editing={this.state.editing === todo.get('id')} onSave={this.save.bind(this, todo)} />;
|
|
}.bind(this));
|
|
|
|
var activeTodoCount = this.props.todos.remaining().length;
|
|
var completedCount = todoItems.length - activeTodoCount;
|
|
if (activeTodoCount || completedCount) {
|
|
footer = <TodoFooter count={activeTodoCount} completedCount={completedCount} onClearCompleted={this.clearCompleted} />;
|
|
}
|
|
|
|
if (todoItems.length) {
|
|
main = (
|
|
<section class="main">
|
|
<input class="toggle-all" type="checkbox" onChange={this.toggleAll} />
|
|
<label class="toggle-all-label">Mark all as complete</label>
|
|
<ul class="todo-list">
|
|
{todoItems}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<section class="todoapp">
|
|
<header class="header">
|
|
<h1>todos</h1>
|
|
<form onSubmit={this.handleSubmit}>
|
|
<input ref="newField" class="new-todo" placeholder="What needs to be done?" autofocus="autofocus" />
|
|
</form>
|
|
</header>
|
|
{main}
|
|
{footer}
|
|
</section>
|
|
<footer class="info">
|
|
<p>Double-click to edit a todo</p>
|
|
<p>Created by{' '}<a href="http://github.com/petehunt/">petehunt</a></p>
|
|
<p>Part of{' '}<a href="http://todomvc.com">TodoMVC</a></p>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
React.renderComponent(<TodoApp todos={new TodoList()} />, document.getElementById('todoapp'));
|