react/examples/todomvc-backbone/js/app.js

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'));