1
/** @jsx React.DOM */
2
3
-function cx(obj) {
4
- var s = '';
5
- for (var key in obj) {
6
- if (!obj.hasOwnProperty(key)) {
7
- continue;
8
- }
9
- if (obj[key]) {
10
- s += key + ' ';
11
- }
12
- }
13
- return s;
14
-}
15
-
3
var Todo = Backbone.Model.extend({
17
-
4
// Default attributes for the todo
5
// and ensure that each todo created has `title` and `completed` keys.
6
defaults: {
14
completed: !this.get('completed')
15
});
16
}
31
-
17
});
18
19
var TodoList = Backbone.Collection.extend({
53
var Utils = {
54
pluralize: function( count, word ) {
55
return count === 1 ? word : word + 's';
56
+ },
57
+
58
+ stringifyObjKeys: function(obj) {
59
+ var s = '';
60
+ for (var key in obj) {
61
+ if (!obj.hasOwnProperty(key)) {
62
+ continue;
63
+ }
64
+ if (obj[key]) {
65
+ s += key + ' ';
66
+ }
67
+ }
68
+ return s;
69
}
70
};
71
73
74
var TodoItem = React.createClass({
75
handleSubmit: function(event) {
78
- var val = this.refs.editField.getDOMNode().value;
76
+ var val = this.refs.editField.getDOMNode().value.trim();
77
if (val) {
78
this.props.onSave(val);
79
+ } else {
80
+ this.props.onDestroy();
81
}
82
return false;
83
},
84
+
85
onEdit: function() {
86
this.props.onEdit();
87
this.refs.editField.getDOMNode().focus();
88
},
89
+
90
render: function() {
91
+ var classes = Utils.stringifyObjKeys({
92
+ completed: this.props.todo.get('completed'), editing: this.props.editing
93
+ });
94
return (
90
- <li class={cx({completed: this.props.todo.get('completed'), editing: this.props.editing})}>
95
+ <li class={classes}>
96
<div class="view">
97
<input
98
class="toggle"
99
type="checkbox"
95
- checked={this.props.todo.get('completed') ? 'checked' : null}
100
+ checked={this.props.todo.get('completed')}
101
onChange={this.props.onToggle}
102
+ key={this.props.key}
103
/>
98
- <label onDoubleClick={this.onEdit}>{this.props.todo.get('title')}</label>
104
+ <label onDoubleClick={this.onEdit}>
105
+ {this.props.todo.get('title')}
106
+ </label>
107
<button class="destroy" onClick={this.props.onDestroy} />
108
</div>
109
<form onSubmit={this.handleSubmit}>
102
- <input ref="editField" class="edit" value={this.props.todo.get('title')} />
110
+ <input
111
+ ref="editField"
112
+ class="edit"
113
+ defaultValue={this.props.todo.get('title')}
114
+ onBlur={this.handleSubmit}
115
+ autoFocus="autofocus"
116
+ />
117
</form>
118
</li>
119
);
127
128
if (this.props.completedCount > 0) {
129
clearButton = (
116
- <button class="clear-completed" onClick={this.props.onClearCompleted}>Clear completed ({this.props.completedCount})</button>
130
+ <button id="clear-completed" onClick={this.props.onClearCompleted}>
131
+ Clear completed ({this.props.completedCount})
132
+ </button>
133
);
134
}
135
136
return (
121
- <footer class="footer">
122
- <span class="todo-count"><strong>{this.props.count}</strong>{' '}{activeTodoWord}{' '}left</span>
137
+ <footer id="footer">
138
+ <span id="todo-count">
139
+ <strong>{this.props.count}</strong>{' '}
140
+ {activeTodoWord}{' '}left
141
+ </span>
142
{clearButton}
143
</footer>
144
);
145
}
146
});
147
129
-// An example generic Mixin that you can add to any component that should react to changes in a Backbone component.
130
-// The use cases we've identified thus far are for Collections -- since they trigger a change event whenever
131
-// any of their constituent items are changed there's no need to reconcile for regular models.
132
-// One caveat: this relies on getBackboneModels() to always return the same model instances throughout the
133
-// lifecycle of the component. If you're using this mixin correctly (it should be near the top of your
134
-// component hierarchy) this should not be an issue.
148
+// An example generic Mixin that you can add to any component that should react
149
+// to changes in a Backbone component. The use cases we've identified thus far
150
+// are for Collections -- since they trigger a change event whenever any of
151
+// their constituent items are changed there's no need to reconcile for regular
152
+// models. One caveat: this relies on getBackboneModels() to always return the
153
+// same model instances throughout the lifecycle of the component. If you're
154
+// using this mixin correctly (it should be near the top of your component
155
+// hierarchy) this should not be an issue.
156
var BackboneMixin = {
157
componentDidMount: function() {
158
// Whenever there may be a change in the Backbone data, trigger a reconcile.
138
- this.getBackboneModels().map(function(model) {
139
- model.on('add change remove', this.forceUpdate, this);
140
- }.bind(this));
159
+ this.getBackboneModels().forEach(function(model) {
160
+ model.on('add change remove', this.forceUpdate.bind(this, null), this);
161
+ }, this);
162
},
163
+
164
componentWillUnmount: function() {
143
- // Ensure that we clean up any dangling references when the component is destroyed.
144
- this.getBackboneModels().map(function(model) {
165
+ // Ensure that we clean up any dangling references when the component is
166
+ // destroyed.
167
+ this.getBackboneModels().forEach(function(model) {
168
model.off(null, null, this);
146
- }.bind(this));
169
+ }, this);
170
}
171
};
172
175
getInitialState: function() {
176
return {editing: null};
177
},
178
+
179
componentDidMount: function() {
180
// Additional functionality for todomvc: fetch() the collection on init
181
this.props.todos.fetch();
182
+ this.refs.newField.getDOMNode().focus();
183
},
184
+
185
componentDidUpdate: function() {
160
- // If saving were expensive we'd listen for mutation events on Backbone and do this manually.
161
- // however, since saving isn't expensive this is an elegant way to keep it reactively up-to-date.
162
- this.props.todos.map(function(todo) {
186
+ // If saving were expensive we'd listen for mutation events on Backbone and
187
+ // do this manually. however, since saving isn't expensive this is an
188
+ // elegant way to keep it reactively up-to-date.
189
+ this.props.todos.forEach(function(todo) {
190
todo.save();
191
});
192
},
193
+
194
getBackboneModels: function() {
195
return [this.props.todos];
196
},
169
- handleSubmit: function() {
197
+
198
+ handleSubmit: function(event) {
199
+ event.preventDefault();
200
var val = this.refs.newField.getDOMNode().value.trim();
201
if (val) {
202
this.props.todos.create({
206
});
207
this.refs.newField.getDOMNode().value = '';
208
}
179
- return false;
209
},
210
+
211
toggleAll: function(event) {
212
var checked = event.nativeEvent.target.checked;
183
- this.props.todos.map(function(todo) {
213
+ this.props.todos.forEach(function(todo) {
214
todo.set('completed', checked);
215
});
216
},
187
- destroy: function(todo) {
188
- this.props.todos.remove(todo);
189
- },
217
+
218
edit: function(todo) {
219
this.setState({editing: todo.get('id')});
220
},
221
+
222
save: function(todo, text) {
223
todo.set('title', text);
224
this.setState({editing: null});
225
},
226
+
227
clearCompleted: function() {
198
- this.props.todos.completed().map(function(todo) {
228
+ this.props.todos.completed().forEach(function(todo) {
229
todo.destroy();
230
});
231
},
232
+
233
render: function() {
234
var footer = null;
235
var main = null;
236
var todoItems = this.props.todos.map(function(todo) {
206
- 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)} />;
207
- }.bind(this));
237
+ return (
238
+ <TodoItem
239
+ key={Math.random()}
240
+ todo={todo}
241
+ onToggle={todo.toggle.bind(todo)}
242
+ onDestroy={todo.destroy.bind(todo)}
243
+ onEdit={this.edit.bind(this, todo)}
244
+ editing={this.state.editing === todo.get('id')}
245
+ onSave={this.save.bind(this, todo)}
246
+ />
247
+ );
248
+ }, this);
249
250
var activeTodoCount = this.props.todos.remaining().length;
251
var completedCount = todoItems.length - activeTodoCount;
211
- if (activeTodoCount || completedCount) {
212
- footer = <TodoFooter count={activeTodoCount} completedCount={completedCount} onClearCompleted={this.clearCompleted} />;
252
+ if (activeTodoCount || completedCount) {
253
+ footer =
254
+ <TodoFooter
255
+ count={activeTodoCount}
256
+ completedCount={completedCount}
257
+ onClearCompleted={this.clearCompleted}
258
+ />;
259
}
260
261
if (todoItems.length) {
262
main = (
217
- <section class="main">
218
- <input class="toggle-all" type="checkbox" onChange={this.toggleAll} />
219
- <label class="toggle-all-label">Mark all as complete</label>
220
- <ul class="todo-list">
263
+ <section id="main">
264
+ <input id="toggle-all" type="checkbox" onChange={this.toggleAll} />
265
+ <ul id="todo-list">
266
{todoItems}
267
</ul>
268
</section>
271
272
return (
273
<div>
229
- <section class="todoapp">
230
- <header class="header">
274
+ <section id="todoapp">
275
+ <header id="header">
276
<h1>todos</h1>
277
<form onSubmit={this.handleSubmit}>
233
- <input ref="newField" class="new-todo" placeholder="What needs to be done?" autofocus="autofocus" />
278
+ <input
279
+ ref="newField"
280
+ id="new-todo"
281
+ placeholder="What needs to be done?"
282
+ />
283
</form>
284
</header>
285
{main}
286
{footer}
287
</section>
239
- <footer class="info">
288
+ <footer id="info">
289
<p>Double-click to edit a todo</p>
241
- <p>Created by{' '}<a href="http://github.com/petehunt/">petehunt</a></p>
290
+ <p>
291
+ Created by{' '}
292
+ <a href="http://github.com/petehunt/">petehunt</a>
293
+ </p>
294
<p>Part of{' '}<a href="http://todomvc.com">TodoMVC</a></p>
295
</footer>
296
</div>
297
);
298
}
299
});
248
-React.renderComponent(<TodoApp todos={new TodoList()} />, document.getElementById('todoapp'));
300
+
301
+React.renderComponent(
302
+ <TodoApp todos={new TodoList()} />, document.getElementById('container')
303
+);