@samitouri / QOS-React-2 / commits / 78d305eb16

Fix backbone todo example bugs.

Fixed: - New todo not submitting correctly (page refreshes. `preventDefault` wasn't there. - Old checked todo being removed will leave the checkmark on the next todo replacing its position. - Cannot change todo (`value`'s now a controlled field). - `autofocus` (should be `autoFocus`, how ironic given the current situation) on new todo input isn't working. Switched to manual `focus()` in `componentDidMount` for now. - More consistent breathing space between lines. - Gutter at 80. Added: - Use todomvc-common base.css. The old one had to change ids to classes. No longer necessary. - Give `cx` a better name and move it in `Utils`. - Trim input upon finishing edit. - Remove todo if the new edited value is empty. - Submit edited todo value on input blur. - README to explain the existence of this example. Being able to maintain a non-compilant version allows nice deviations from the todomvc specs, such as animations, in the future.

Cheng Lou committed Sep 6, 2013 at 16:40 UTC 78d305eb167af7f5446c5d4812a160946f493234
4 files changed +309 -113
examples/todomvc-backbone/README.md new
+3
@@ -0,0 +1,3 @@
1 +# TodoMVC-Backbone
2 +
3 +This is a lightweight version of TodoMVC. Its primary purpose is to demo the Backbone integration rather than being feature-complete (refer to `todomvc-director` for a full TodoMVC-compilant app).
examples/todomvc-backbone/css/base.css
+191 -53
@@ -34,7 +34,7 @@ body {
34 font-smoothing: antialiased;
35 }
36
37 -.todoapp {
37 +#todoapp {
38 background: #fff;
39 background: rgba(255, 255, 255, 0.9);
40 margin: 130px 0 40px 0;
@@ -46,7 +46,7 @@ body {
46 0 25px 50px 0 rgba(0, 0, 0, 0.15);
47 }
48
49 -.todoapp:before {
49 +#todoapp:before {
50 content: '';
51 border-left: 1px solid #f5d6d6;
52 border-right: 1px solid #f5d6d6;
@@ -57,16 +57,16 @@ body {
57 height: 100%;
58 }
59
60 -.todoapp input::-webkit-input-placeholder {
60 +#todoapp input::-webkit-input-placeholder {
61 font-style: italic;
62 }
63
64 -.todoapp input:-moz-placeholder {
64 +#todoapp input::-moz-placeholder {
65 font-style: italic;
66 color: #a9a9a9;
67 }
68
69 -.todoapp h1 {
69 +#todoapp h1 {
70 position: absolute;
71 top: -120px;
72 width: 100%;
@@ -83,12 +83,12 @@ body {
83 text-rendering: optimizeLegibility;
84 }
85
86 -.header {
86 +#header {
87 padding-top: 15px;
88 border-radius: inherit;
89 }
90
91 -.header:before {
91 +#header:before {
92 content: '';
93 position: absolute;
94 top: 0;
@@ -109,7 +109,7 @@ body {
109 border-top-right-radius: 1px;
110 }
111
112 -.new-todo,
112 +#new-todo,
113 .edit {
114 position: relative;
115 margin: 0;
@@ -135,7 +135,7 @@ body {
135 font-smoothing: antialiased;
136 }
137
138 -.new-todo {
138 +#new-todo {
139 padding: 16px 16px 16px 60px;
140 border: none;
141 background: rgba(0, 0, 0, 0.02);
@@ -143,17 +143,17 @@ body {
143 box-shadow: none;
144 }
145
146 -.main {
146 +#main {
147 position: relative;
148 z-index: 2;
149 border-top: 1px dotted #adadad;
150 }
151
152 -.toggle-all-label {
152 +label[for='toggle-all'] {
153 display: none;
154 }
155
156 -.toggle-all {
156 +#toggle-all {
157 position: absolute;
158 top: -42px;
159 left: -4px;
@@ -162,50 +162,50 @@ body {
162 border: none; /* Mobile Safari */
163 }
164
165 -.toggle-all:before {
165 +#toggle-all:before {
166 content: '»';
167 font-size: 28px;
168 color: #d9d9d9;
169 padding: 0 25px 7px;
170 }
171
172 -.toggle-all:checked:before {
172 +#toggle-all:checked:before {
173 color: #737373;
174 }
175
176 -.todo-list {
176 +#todo-list {
177 margin: 0;
178 padding: 0;
179 list-style: none;
180 }
181
182 -.todo-list li {
182 +#todo-list li {
183 position: relative;
184 font-size: 24px;
185 border-bottom: 1px dotted #ccc;
186 }
187
188 -.todo-list li:last-child {
188 +#todo-list li:last-child {
189 border-bottom: none;
190 }
191
192 -.todo-list li.editing {
192 +#todo-list li.editing {
193 border-bottom: none;
194 padding: 0;
195 }
196
197 -.todo-list li.editing .edit {
197 +#todo-list li.editing .edit {
198 display: block;
199 width: 506px;
200 padding: 13px 17px 12px 17px;
201 margin: 0 0 0 43px;
202 }
203
204 -.todo-list li.editing .view {
204 +#todo-list li.editing .view {
205 display: none;
206 }
207
208 -.todo-list li .toggle {
208 +#todo-list li .toggle {
209 text-align: center;
210 width: 40px;
211 /* auto, since non-WebKit browsers doesn't support input styling */
@@ -222,7 +222,7 @@ body {
222 appearance: none;
223 }
224
225 -.todo-list li .toggle:after {
225 +#todo-list li .toggle:after {
226 content: '✔';
227 line-height: 43px; /* 40 + a couple of pixels visual adjustment */
228 font-size: 20px;
@@ -230,16 +230,17 @@ body {
230 text-shadow: 0 -1px 0 #bfbfbf;
231 }
232
233 -.todo-list li .toggle:checked:after {
233 +#todo-list li .toggle:checked:after {
234 color: #85ada7;
235 text-shadow: 0 1px 0 #669991;
236 bottom: 1px;
237 position: relative;
238 }
239
240 -.todo-list li label {
240 +#todo-list li label {
241 + white-space: pre;
242 word-break: break-word;
242 - padding: 15px;
243 + padding: 15px 60px 15px 15px;
244 margin-left: 45px;
245 display: block;
246 line-height: 1.2;
@@ -250,12 +251,12 @@ body {
251 transition: color 0.4s;
252 }
253
253 -.todo-list li.completed label {
254 +#todo-list li.completed label {
255 color: #a9a9a9;
256 text-decoration: line-through;
257 }
258
258 -.todo-list li .destroy {
259 +#todo-list li .destroy {
260 display: none;
261 position: absolute;
262 top: 0;
@@ -273,7 +274,7 @@ body {
274 transition: all 0.2s;
275 }
276
276 -.todo-list li .destroy:hover {
277 +#todo-list li .destroy:hover {
278 text-shadow: 0 0 1px #000,
279 0 0 10px rgba(199, 107, 107, 0.8);
280 -webkit-transform: scale(1.3);
@@ -283,23 +284,23 @@ body {
284 transform: scale(1.3);
285 }
286
286 -.todo-list li .destroy:after {
287 +#todo-list li .destroy:after {
288 content: '✖';
289 }
290
290 -.todo-list li:hover .destroy {
291 +#todo-list li:hover .destroy {
292 display: block;
293 }
294
294 -.todo-list li .edit {
295 +#todo-list li .edit {
296 display: none;
297 }
298
298 -.todo-list li.editing:last-child {
299 +#todo-list li.editing:last-child {
300 margin-bottom: -1px;
301 }
302
302 -.footer {
303 +#footer {
304 color: #777;
305 padding: 0 15px;
306 position: absolute;
@@ -311,7 +312,7 @@ body {
312 text-align: center;
313 }
314
314 -.footer:before {
315 +#footer:before {
316 content: '';
317 position: absolute;
318 right: 0;
@@ -326,12 +327,12 @@ body {
327 0 44px 2px -6px rgba(0, 0, 0, 0.2);
328 }
329
329 -.todo-count {
330 +#todo-count {
331 float: left;
332 text-align: left;
333 }
334
334 -.filters {
335 +#filters {
336 margin: 0;
337 padding: 0;
338 list-style: none;
@@ -340,21 +341,21 @@ body {
341 left: 0;
342 }
343
343 -.filters li {
344 +#filters li {
345 display: inline;
346 }
347
347 -.filters li a {
348 +#filters li a {
349 color: #83756f;
350 margin: 2px;
351 text-decoration: none;
352 }
353
353 -.filters li a.selected {
354 +#filters li a.selected {
355 font-weight: bold;
356 }
357
357 -.clear-completed {
358 +#clear-completed {
359 float: right;
360 position: relative;
361 line-height: 20px;
@@ -366,12 +367,12 @@ body {
367 box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
368 }
369
369 -.clear-completed:hover {
370 +#clear-completed:hover {
371 background: rgba(0, 0, 0, 0.15);
372 box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
373 }
374
374 -.info {
375 +#info {
376 margin: 65px auto 0;
377 color: #a6a6a6;
378 font-size: 12px;
@@ -379,29 +380,25 @@ body {
380 text-align: center;
381 }
382
382 -.info a {
383 +#info a {
384 color: inherit;
385 }
386
386 -.submitButton {
387 - display: none;
388 -}
389 -
387 /*
388 Hack to remove background from Mobile Safari.
389 Can't use it globally since it destroys checkboxes in Firefox and Opera
390 */
391 @media screen and (-webkit-min-device-pixel-ratio:0) {
395 - .toggle-all,
396 - .todo-list li .toggle {
392 + #toggle-all,
393 + #todo-list li .toggle {
394 background: none;
395 }
396
400 - .todo-list li .toggle {
397 + #todo-list li .toggle {
398 height: 40px;
399 }
400
404 - .toggle-all {
401 + #toggle-all {
402 top: -56px;
403 left: -15px;
404 width: 65px;
@@ -413,6 +410,147 @@ body {
410 }
411 }
412
416 -.hidden{
417 - display:none;
413 +.hidden {
414 + display: none;
415 +}
416 +
417 +hr {
418 + margin: 20px 0;
419 + border: 0;
420 + border-top: 1px dashed #C5C5C5;
421 + border-bottom: 1px dashed #F7F7F7;
422 +}
423 +
424 +.learn a {
425 + font-weight: normal;
426 + text-decoration: none;
427 + color: #b83f45;
428 +}
429 +
430 +.learn a:hover {
431 + text-decoration: underline;
432 + color: #787e7e;
433 +}
434 +
435 +.learn h3,
436 +.learn h4,
437 +.learn h5 {
438 + margin: 10px 0;
439 + font-weight: 500;
440 + line-height: 1.2;
441 + color: #000;
442 +}
443 +
444 +.learn h3 {
445 + font-size: 24px;
446 +}
447 +
448 +.learn h4 {
449 + font-size: 18px;
450 +}
451 +
452 +.learn h5 {
453 + margin-bottom: 0;
454 + font-size: 14px;
455 +}
456 +
457 +.learn ul {
458 + padding: 0;
459 + margin: 0 0 30px 25px;
460 +}
461 +
462 +.learn li {
463 + line-height: 20px;
464 +}
465 +
466 +.learn p {
467 + font-size: 15px;
468 + font-weight: 300;
469 + line-height: 1.3;
470 + margin-top: 0;
471 + margin-bottom: 0;
472 +}
473 +
474 +.quote {
475 + border: none;
476 + margin: 20px 0 60px 0;
477 +}
478 +
479 +.quote p {
480 + font-style: italic;
481 +}
482 +
483 +.quote p:before {
484 + content: '“';
485 + font-size: 50px;
486 + opacity: .15;
487 + position: absolute;
488 + top: -20px;
489 + left: 3px;
490 +}
491 +
492 +.quote p:after {
493 + content: '”';
494 + font-size: 50px;
495 + opacity: .15;
496 + position: absolute;
497 + bottom: -42px;
498 + right: 3px;
499 +}
500 +
501 +.quote footer {
502 + position: absolute;
503 + bottom: -40px;
504 + right: 0;
505 +}
506 +
507 +.quote footer img {
508 + border-radius: 3px;
509 +}
510 +
511 +.quote footer a {
512 + margin-left: 5px;
513 + vertical-align: middle;
514 +}
515 +
516 +.speech-bubble {
517 + position: relative;
518 + padding: 10px;
519 + background: rgba(0, 0, 0, .04);
520 + border-radius: 5px;
521 +}
522 +
523 +.speech-bubble:after {
524 + content: '';
525 + position: absolute;
526 + top: 100%;
527 + right: 30px;
528 + border: 13px solid transparent;
529 + border-top-color: rgba(0, 0, 0, .04);
530 +}
531 +
532 +/**body*/.learn-bar > .learn {
533 + position: absolute;
534 + width: 272px;
535 + top: 8px;
536 + left: -300px;
537 + padding: 10px;
538 + border-radius: 5px;
539 + background-color: rgba(255, 255, 255, .6);
540 + transition-property: left;
541 + transition-duration: 500ms;
542 +}
543 +
544 +@media (min-width: 899px) {
545 + /**body*/.learn-bar {
546 + width: auto;
547 + margin: 0 0 0 300px;
548 + }
549 + /**body*/.learn-bar > .learn {
550 + left: 8px;
551 + }
552 + /**body*/.learn-bar #todoapp {
553 + width: 550px;
554 + margin: 130px auto 40px auto;
555 + }
556 }
examples/todomvc-backbone/index.html
+1 -1
@@ -10,7 +10,7 @@
10 <![endif]-->
11 </head>
12 <body>
13 - <div id="todoapp"></div>
13 + <div id="container"></div>
14 <script src="../../build/react.js"></script>
15 <script src="../../build/JSXTransformer.js"></script>
16 <script type="text/javascript" src="../shared/thirdparty/jquery.min.js" charset="utf-8"></script>
examples/todomvc-backbone/js/app.js
+114 -59
@@ -1,20 +1,6 @@
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: {
@@ -28,7 +14,6 @@ var Todo = Backbone.Model.extend({
14 completed: !this.get('completed')
15 });
16 }
31 -
17 });
18
19 var TodoList = Backbone.Collection.extend({
@@ -68,6 +53,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
@@ -75,31 +73,47 @@ var Utils = {
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 );
@@ -113,37 +127,46 @@ var TodoFooter = React.createClass({
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
@@ -152,21 +175,28 @@ var TodoApp = React.createClass({
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({
@@ -176,48 +206,63 @@ var TodoApp = React.createClass({
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>
@@ -226,23 +271,33 @@ var TodoApp = React.createClass({
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 +);