Commit e990b79d authored by Addy Osmani's avatar Addy Osmani

Merge pull request #785 from chenglou/react-bb

React example with Backbone intergration.
parents 659cd89f 28bdc919
{
"name": "todomvc-react-backbone",
"version": "0.0.0",
"dependencies": {
"react": "~0.8.0",
"todomvc-common": "~0.1.9",
"backbone": "~1.1.0",
"backbone.localstorage": "~1.1.7",
"jquery": "~2.0.3",
"underscore": "~1.5.2"
}
}
<!doctype html>
<html lang="en" data-framework="react">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>React + Backbone • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
</head>
<body>
<section id="todoapp"></section>
<footer id="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>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/react/react-with-addons.js"></script>
<script src="bower_components/react/JSXTransformer.js"></script>
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/underscore/underscore.js"></script>
<script src="bower_components/backbone/backbone.js"></script>
<script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script>
<script src="js/todo.js"></script>
<script src="js/todos.js"></script>
<!-- jsx is an optional syntactic sugar that transforms methods in React's
`render` into an HTML-looking format. Since the two models above are
unrelated to React, we didn't need those transforms. -->
<script type="text/jsx" src="js/todoItem.jsx"></script>
<script type="text/jsx" src="js/footer.jsx"></script>
<script type="text/jsx" src="js/app.jsx"></script>
</body>
</html>
/**
* @jsx React.DOM
*/
/*jshint quotmark:false */
/*jshint white:false */
/*jshint trailing:false */
/*jshint newcap:false */
/*global React, Backbone */
var app = app || {};
(function () {
'use strict';
app.ALL_TODOS = 'all';
app.ACTIVE_TODOS = 'active';
app.COMPLETED_TODOS = 'completed';
var TodoFooter = app.TodoFooter;
var TodoItem = app.TodoItem;
var ENTER_KEY = 13;
// 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 getBackboneCollections() to
// always return the same collection 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.getBackboneCollections().forEach(function (collection) {
// explicitly bind `null` to `forceUpdate`, as it demands a callback and
// React validates that it's a function. `collection` events passes
// additional arguments that are not functions
collection.on('add remove change', this.forceUpdate.bind(this, null));
}, this);
},
componentWillUnmount: function () {
// Ensure that we clean up any dangling references when the component is
// destroyed.
this.getBackboneCollections().forEach(function (collection) {
collection.off(null, null, this);
}, this);
}
};
var TodoApp = React.createClass({
mixins: [BackboneMixin],
getBackboneCollections: function () {
return [this.props.todos];
},
getInitialState: function () {
return {editing: null};
},
componentDidMount: function () {
var Router = Backbone.Router.extend({
routes: {
'': 'all',
'active': 'active',
'completed': 'completed'
},
all: this.setState.bind(this, {nowShowing: app.ALL_TODOS}),
active: this.setState.bind(this, {nowShowing: app.ACTIVE_TODOS}),
completed: this.setState.bind(this, {nowShowing: app.COMPLETED_TODOS})
});
var router = new Router();
Backbone.history.start();
this.props.todos.fetch();
this.refs.newField.getDOMNode().focus();
},
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.forEach(function (todo) {
todo.save();
});
},
handleNewTodoKeyDown: function (event) {
if (event.which !== ENTER_KEY) {
return;
}
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: function (event) {
var checked = event.target.checked;
this.props.todos.forEach(function (todo) {
todo.set('completed', checked);
});
},
edit: function (todo, callback) {
// refer to todoItem.jsx `handleEdit` for the reason behind the callback
this.setState({editing: todo.get('id')}, callback);
},
save: function (todo, text) {
todo.save({title: text});
this.setState({editing: null});
},
cancel: function () {
this.setState({editing: null});
},
clearCompleted: function () {
this.props.todos.completed().forEach(function (todo) {
todo.destroy();
});
},
render: function () {
var footer;
var main;
var todos = this.props.todos;
var shownTodos = todos.filter(function (todo) {
switch (this.state.nowShowing) {
case app.ACTIVE_TODOS:
return !todo.get('completed');
case app.COMPLETED_TODOS:
return todo.get('completed');
default:
return true;
}
}, this);
var todoItems = shownTodos.map(function (todo) {
return (
<TodoItem
key={todo.get('id')}
todo={todo}
onToggle={todo.toggle.bind(todo)}
onDestroy={todo.destroy.bind(todo)}
onEdit={this.edit.bind(this, todo)}
editing={this.state.editing === todo.get('id')}
onSave={this.save.bind(this, todo)}
onCancel={this.cancel}
/>
);
}, this);
var activeTodoCount = todos.reduce(function (accum, todo) {
return todo.get('completed') ? accum : accum + 1;
}, 0);
var completedCount = todos.length - activeTodoCount;
if (activeTodoCount || completedCount) {
footer =
<TodoFooter
count={activeTodoCount}
completedCount={completedCount}
nowShowing={this.state.nowShowing}
onClearCompleted={this.clearCompleted}
/>;
}
if (todos.length) {
main = (
<section id="main">
<input
id="toggle-all"
type="checkbox"
onChange={this.toggleAll}
checked={activeTodoCount === 0}
/>
<ul id="todo-list">
{todoItems}
</ul>
</section>
);
}
return (
<div>
<header id="header">
<h1>todos</h1>
<input
ref="newField"
id="new-todo"
placeholder="What needs to be done?"
onKeyDown={this.handleNewTodoKeyDown}
/>
</header>
{main}
{footer}
</div>
);
}
});
React.renderComponent(
<TodoApp todos={app.todos} />,
document.getElementById('todoapp')
);
})();
/**
* @jsx React.DOM
*/
/*jshint quotmark:false */
/*jshint white:false */
/*jshint trailing:false */
/*jshint newcap:false */
/*global React */
var app = app || {};
(function () {
'use strict';
app.TodoFooter = React.createClass({
render: function () {
var activeTodoWord = this.props.count === 1 ? 'item' : 'items';
var clearButton = null;
if (this.props.completedCount > 0) {
clearButton = (
<button
id="clear-completed"
onClick={this.props.onClearCompleted}>
{''}Clear completed ({this.props.completedCount}){''}
</button>
);
}
// React idiom for shortcutting to `classSet` since it'll be used often
var cx = React.addons.classSet;
var nowShowing = this.props.nowShowing;
return (
<footer id="footer">
<span id="todo-count">
<strong>{this.props.count}</strong>
{' '}{activeTodoWord}{' '}left{''}
</span>
<ul id="filters">
<li>
<a
href="#/"
className={cx({selected: nowShowing === app.ALL_TODOS})}>
All
</a>
</li>
{' '}
<li>
<a
href="#/active"
className={cx({selected: nowShowing === app.ACTIVE_TODOS})}>
Active
</a>
</li>
{' '}
<li>
<a
href="#/completed"
className={cx({selected: nowShowing === app.COMPLETED_TODOS})}>
Completed
</a>
</li>
</ul>
{clearButton}
</footer>
);
}
});
})();
/*global Backbone */
var app = app || {};
(function () {
'use strict';
// Todo Model
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
app.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')
});
}
});
})();
/**
* @jsx React.DOM
*/
/*jshint quotmark: false */
/*jshint white: false */
/*jshint trailing: false */
/*jshint newcap: false */
/*global React */
var app = app || {};
(function () {
'use strict';
var ESCAPE_KEY = 27;
var ENTER_KEY = 13;
app.TodoItem = React.createClass({
getInitialState: function () {
return {editText: this.props.todo.get('title')};
},
handleSubmit: function () {
var val = this.state.editText.trim();
if (val) {
this.props.onSave(val);
this.setState({editText: val});
} else {
this.props.onDestroy();
}
return false;
},
handleEdit: function () {
// react optimizes renders by batching them. This means you can't call
// parent's `onEdit` (which in this case triggeres a re-render), and
// immediately manipulate the DOM as if the rendering's over. Put it as a
// callback. Refer to app.jsx' `edit` method
this.props.onEdit(function () {
var node = this.refs.editField.getDOMNode();
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}.bind(this));
this.setState({editText: this.props.todo.get('title')});
},
handleKeyDown: function (event) {
if (event.which === ESCAPE_KEY) {
this.setState({editText: this.props.todo.get('title')});
this.props.onCancel();
} else if (event.which === ENTER_KEY) {
this.handleSubmit();
}
},
handleChange: function (event) {
this.setState({editText: event.target.value});
},
render: function () {
return (
<li className={React.addons.classSet({
completed: this.props.todo.get('completed'),
editing: this.props.editing
})}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={this.props.todo.get('completed')}
onChange={this.props.onToggle}
/>
<label onDoubleClick={this.handleEdit}>
{this.props.todo.get('title')}
</label>
<button className="destroy" onClick={this.props.onDestroy} />
</div>
<input
ref="editField"
className="edit"
value={this.state.editText}
onBlur={this.handleSubmit}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
</li>
);
}
});
})();
/*global Backbone */
var app = app || {};
(function () {
'use strict';
// Todo Collection
// ---------------
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var Todos = Backbone.Collection.extend({
// Reference to this collection's model.
model: app.Todo,
// Save all of the todo items under the `"todos"` namespace.
localStorage: new Backbone.LocalStorage('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');
});
},
// Filter down the list to only todo items that are still not finished.
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');
}
});
// Create our global collection of **Todos**.
app.todos = new Todos();
})();
# React TodoMVC Example With Backbone Integration
This React example integrates Backbone for its model and router. Note that this is simply a showcase of third-party library integration; Backbone isn't the best candidate because of its mutative nature, which makes it hard to take advantage of some of React's more advanced performance tuning, i.e. [`shouldComponentUpdate`](http://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate). The main React example uses a simple, custom immutable model.
> React is a JavaScript library for creating user interfaces. Its core principles are declarative code, efficiency, and flexibility. Simply specify what your component looks like and React will keep it up-to-date when the underlying data changes.
> _[React - facebook.github.io/react](http://facebook.github.io/react)_
## Learning React
The [React getting started documentation](http://facebook.github.io/react/docs/getting-started.html) is a great way to get started.
Here are some links you may find helpful:
* [Documentation](http://facebook.github.io/react/docs/getting-started.html)
* [API Reference](http://facebook.github.io/react/docs/reference.html)
* [Blog](http://facebook.github.io/react/blog/)
* [React on GitHub](https://github.com/facebook/react)
* [Support](http://facebook.github.io/react/support.html)
Articles and guides from the community:
* [Philosophy](http://www.quora.com/Pete-Hunt/Posts/React-Under-the-Hood)
* [How is Facebook's React JavaScript library](http://www.quora.com/React-JS-Library/How-is-Facebooks-React-JavaScript-library)
* [React: Under the hood](http://www.quora.com/Pete-Hunt/Posts/React-Under-the-Hood)
Get help from other React users:
* [React on StackOverflow](http://stackoverflow.com/questions/tagged/reactjs)
* [Mailing list on Google Groups](https://groups.google.com/forum/#!forum/reactjs)
*
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Running
The app is built with [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) and compiled at runtime for a lighter and more fun code reading experience. As stated in the link, JSX is not mandatory.
To run the app, spin up an HTTP server (e.g. `python -m SimpleHTTPServer`) and visit http://localhost/.../myexample/.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment