Skip to content

Commit e990b79

Browse files
committed
Merge pull request tastejs#785 from chenglou/react-bb
React example with Backbone intergration.
2 parents 659cd89 + 28bdc91 commit e990b79

File tree

8 files changed

+536
-0
lines changed

8 files changed

+536
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "todomvc-react-backbone",
3+
"version": "0.0.0",
4+
"dependencies": {
5+
"react": "~0.8.0",
6+
"todomvc-common": "~0.1.9",
7+
"backbone": "~1.1.0",
8+
"backbone.localstorage": "~1.1.7",
9+
"jquery": "~2.0.3",
10+
"underscore": "~1.5.2"
11+
}
12+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!doctype html>
2+
<html lang="en" data-framework="react">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<title>React + Backbone • TodoMVC</title>
7+
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
8+
</head>
9+
<body>
10+
<section id="todoapp"></section>
11+
<footer id="info">
12+
<p>Double-click to edit a todo</p>
13+
<p>Created by <a href="http://github.com/petehunt/">petehunt</a></p>
14+
<p>Part of<a href="http://todomvc.com">TodoMVC</a></p>
15+
</footer>
16+
17+
<script src="bower_components/todomvc-common/base.js"></script>
18+
<script src="bower_components/react/react-with-addons.js"></script>
19+
<script src="bower_components/react/JSXTransformer.js"></script>
20+
<script src="bower_components/jquery/jquery.js"></script>
21+
<script src="bower_components/underscore/underscore.js"></script>
22+
<script src="bower_components/backbone/backbone.js"></script>
23+
<script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script>
24+
25+
<script src="js/todo.js"></script>
26+
<script src="js/todos.js"></script>
27+
<!-- jsx is an optional syntactic sugar that transforms methods in React's
28+
`render` into an HTML-looking format. Since the two models above are
29+
unrelated to React, we didn't need those transforms. -->
30+
<script type="text/jsx" src="js/todoItem.jsx"></script>
31+
<script type="text/jsx" src="js/footer.jsx"></script>
32+
<script type="text/jsx" src="js/app.jsx"></script>
33+
</body>
34+
</html>
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* @jsx React.DOM
3+
*/
4+
/*jshint quotmark:false */
5+
/*jshint white:false */
6+
/*jshint trailing:false */
7+
/*jshint newcap:false */
8+
/*global React, Backbone */
9+
var app = app || {};
10+
11+
(function () {
12+
'use strict';
13+
14+
app.ALL_TODOS = 'all';
15+
app.ACTIVE_TODOS = 'active';
16+
app.COMPLETED_TODOS = 'completed';
17+
var TodoFooter = app.TodoFooter;
18+
var TodoItem = app.TodoItem;
19+
20+
var ENTER_KEY = 13;
21+
22+
// An example generic Mixin that you can add to any component that should
23+
// react to changes in a Backbone component. The use cases we've identified
24+
// thus far are for Collections -- since they trigger a change event whenever
25+
// any of their constituent items are changed there's no need to reconcile for
26+
// regular models. One caveat: this relies on getBackboneCollections() to
27+
// always return the same collection instances throughout the lifecycle of the
28+
// component. If you're using this mixin correctly (it should be near the top
29+
// of your component hierarchy) this should not be an issue.
30+
var BackboneMixin = {
31+
componentDidMount: function () {
32+
// Whenever there may be a change in the Backbone data, trigger a
33+
// reconcile.
34+
this.getBackboneCollections().forEach(function (collection) {
35+
// explicitly bind `null` to `forceUpdate`, as it demands a callback and
36+
// React validates that it's a function. `collection` events passes
37+
// additional arguments that are not functions
38+
collection.on('add remove change', this.forceUpdate.bind(this, null));
39+
}, this);
40+
},
41+
42+
componentWillUnmount: function () {
43+
// Ensure that we clean up any dangling references when the component is
44+
// destroyed.
45+
this.getBackboneCollections().forEach(function (collection) {
46+
collection.off(null, null, this);
47+
}, this);
48+
}
49+
};
50+
51+
var TodoApp = React.createClass({
52+
mixins: [BackboneMixin],
53+
getBackboneCollections: function () {
54+
return [this.props.todos];
55+
},
56+
57+
getInitialState: function () {
58+
return {editing: null};
59+
},
60+
61+
componentDidMount: function () {
62+
var Router = Backbone.Router.extend({
63+
routes: {
64+
'': 'all',
65+
'active': 'active',
66+
'completed': 'completed'
67+
},
68+
all: this.setState.bind(this, {nowShowing: app.ALL_TODOS}),
69+
active: this.setState.bind(this, {nowShowing: app.ACTIVE_TODOS}),
70+
completed: this.setState.bind(this, {nowShowing: app.COMPLETED_TODOS})
71+
});
72+
73+
var router = new Router();
74+
Backbone.history.start();
75+
76+
this.props.todos.fetch();
77+
this.refs.newField.getDOMNode().focus();
78+
},
79+
80+
componentDidUpdate: function () {
81+
// If saving were expensive we'd listen for mutation events on Backbone and
82+
// do this manually. however, since saving isn't expensive this is an
83+
// elegant way to keep it reactively up-to-date.
84+
this.props.todos.forEach(function (todo) {
85+
todo.save();
86+
});
87+
},
88+
89+
handleNewTodoKeyDown: function (event) {
90+
if (event.which !== ENTER_KEY) {
91+
return;
92+
}
93+
94+
var val = this.refs.newField.getDOMNode().value.trim();
95+
if (val) {
96+
this.props.todos.create({
97+
title: val,
98+
completed: false,
99+
order: this.props.todos.nextOrder()
100+
});
101+
this.refs.newField.getDOMNode().value = '';
102+
}
103+
104+
return false;
105+
},
106+
107+
toggleAll: function (event) {
108+
var checked = event.target.checked;
109+
this.props.todos.forEach(function (todo) {
110+
todo.set('completed', checked);
111+
});
112+
},
113+
114+
edit: function (todo, callback) {
115+
// refer to todoItem.jsx `handleEdit` for the reason behind the callback
116+
this.setState({editing: todo.get('id')}, callback);
117+
},
118+
119+
save: function (todo, text) {
120+
todo.save({title: text});
121+
this.setState({editing: null});
122+
},
123+
124+
cancel: function () {
125+
this.setState({editing: null});
126+
},
127+
128+
clearCompleted: function () {
129+
this.props.todos.completed().forEach(function (todo) {
130+
todo.destroy();
131+
});
132+
},
133+
134+
render: function () {
135+
var footer;
136+
var main;
137+
var todos = this.props.todos;
138+
139+
var shownTodos = todos.filter(function (todo) {
140+
switch (this.state.nowShowing) {
141+
case app.ACTIVE_TODOS:
142+
return !todo.get('completed');
143+
case app.COMPLETED_TODOS:
144+
return todo.get('completed');
145+
default:
146+
return true;
147+
}
148+
}, this);
149+
150+
var todoItems = shownTodos.map(function (todo) {
151+
return (
152+
<TodoItem
153+
key={todo.get('id')}
154+
todo={todo}
155+
onToggle={todo.toggle.bind(todo)}
156+
onDestroy={todo.destroy.bind(todo)}
157+
onEdit={this.edit.bind(this, todo)}
158+
editing={this.state.editing === todo.get('id')}
159+
onSave={this.save.bind(this, todo)}
160+
onCancel={this.cancel}
161+
/>
162+
);
163+
}, this);
164+
165+
var activeTodoCount = todos.reduce(function (accum, todo) {
166+
return todo.get('completed') ? accum : accum + 1;
167+
}, 0);
168+
169+
var completedCount = todos.length - activeTodoCount;
170+
171+
if (activeTodoCount || completedCount) {
172+
footer =
173+
<TodoFooter
174+
count={activeTodoCount}
175+
completedCount={completedCount}
176+
nowShowing={this.state.nowShowing}
177+
onClearCompleted={this.clearCompleted}
178+
/>;
179+
}
180+
181+
if (todos.length) {
182+
main = (
183+
<section id="main">
184+
<input
185+
id="toggle-all"
186+
type="checkbox"
187+
onChange={this.toggleAll}
188+
checked={activeTodoCount === 0}
189+
/>
190+
<ul id="todo-list">
191+
{todoItems}
192+
</ul>
193+
</section>
194+
);
195+
}
196+
197+
return (
198+
<div>
199+
<header id="header">
200+
<h1>todos</h1>
201+
<input
202+
ref="newField"
203+
id="new-todo"
204+
placeholder="What needs to be done?"
205+
onKeyDown={this.handleNewTodoKeyDown}
206+
/>
207+
</header>
208+
{main}
209+
{footer}
210+
</div>
211+
);
212+
}
213+
});
214+
215+
React.renderComponent(
216+
<TodoApp todos={app.todos} />,
217+
document.getElementById('todoapp')
218+
);
219+
})();
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @jsx React.DOM
3+
*/
4+
/*jshint quotmark:false */
5+
/*jshint white:false */
6+
/*jshint trailing:false */
7+
/*jshint newcap:false */
8+
/*global React */
9+
var app = app || {};
10+
11+
(function () {
12+
'use strict';
13+
14+
app.TodoFooter = React.createClass({
15+
render: function () {
16+
var activeTodoWord = this.props.count === 1 ? 'item' : 'items';
17+
var clearButton = null;
18+
19+
if (this.props.completedCount > 0) {
20+
clearButton = (
21+
<button
22+
id="clear-completed"
23+
onClick={this.props.onClearCompleted}>
24+
{''}Clear completed ({this.props.completedCount}){''}
25+
</button>
26+
);
27+
}
28+
29+
// React idiom for shortcutting to `classSet` since it'll be used often
30+
var cx = React.addons.classSet;
31+
var nowShowing = this.props.nowShowing;
32+
return (
33+
<footer id="footer">
34+
<span id="todo-count">
35+
<strong>{this.props.count}</strong>
36+
{' '}{activeTodoWord}{' '}left{''}
37+
</span>
38+
<ul id="filters">
39+
<li>
40+
<a
41+
href="#/"
42+
className={cx({selected: nowShowing === app.ALL_TODOS})}>
43+
All
44+
</a>
45+
</li>
46+
{' '}
47+
<li>
48+
<a
49+
href="#/active"
50+
className={cx({selected: nowShowing === app.ACTIVE_TODOS})}>
51+
Active
52+
</a>
53+
</li>
54+
{' '}
55+
<li>
56+
<a
57+
href="#/completed"
58+
className={cx({selected: nowShowing === app.COMPLETED_TODOS})}>
59+
Completed
60+
</a>
61+
</li>
62+
</ul>
63+
{clearButton}
64+
</footer>
65+
);
66+
}
67+
});
68+
})();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*global Backbone */
2+
var app = app || {};
3+
4+
(function () {
5+
'use strict';
6+
7+
// Todo Model
8+
// ----------
9+
10+
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
11+
app.Todo = Backbone.Model.extend({
12+
// Default attributes for the todo
13+
// and ensure that each todo created has `title` and `completed` keys.
14+
defaults: {
15+
title: '',
16+
completed: false
17+
},
18+
19+
// Toggle the `completed` state of this todo item.
20+
toggle: function () {
21+
this.save({
22+
completed: !this.get('completed')
23+
});
24+
}
25+
});
26+
})();

0 commit comments

Comments
 (0)