ReactJS + RefluxJS • TodoMVC
================================================
FILE: js/actions.js
================================================
(function(Reflux, global) {
'use strict';
// Each action is like an event channel for one specific event. Actions are called by components.
// The store is listening to all actions, and the components in turn are listening to the store.
// Thus the flow is: User interaction -> component calls action -> store reacts and triggers -> components update
global.TodoActions = Reflux.createActions([
"toggleItem", // called by button in TodoItem
"toggleAllItems", // called by button in TodoMain (even though you'd think TodoHeader)
"addItem", // called by hitting enter in field in TodoHeader
"removeItem", // called by button in TodoItem
"clearCompleted", // called by button in TodoFooter
"editItem" // called by finishing edit in TodoItem
]);
})(window.Reflux, window);
================================================
FILE: js/components.jsx.js
================================================
/** @jsx React.DOM */
(function(React, ReactRouter, Reflux, TodoActions, todoListStore, global) {
// Renders a single Todo item in the list
// Used in TodoMain
var TodoItem = React.createClass({
propTypes: {
label: React.PropTypes.string.isRequired,
isComplete: React.PropTypes.bool.isRequired,
id: React.PropTypes.number
},
mixins: [React.addons.LinkedStateMixin], // exposes this.linkState used in render
getInitialState: function() {
return {};
},
handleToggle: function(evt) {
TodoActions.toggleItem(this.props.id);
},
handleEditStart: function(evt) {
evt.preventDefault();
// because of linkState call in render, field will get value from this.state.editValue
this.setState({
isEditing: true,
editValue: this.props.label
}, function() {
this.refs.editInput.getDOMNode().focus();
});
},
handleValueChange: function(evt) {
var text = this.state.editValue; // because of the linkState call in render, this is the contents of the field
// we pressed enter, if text isn't empty we blur the field which will cause a save
if (evt.which === 13 && text) {
this.refs.editInput.getDOMNode().blur();
}
// pressed escape. set editing to false before blurring so we won't save
else if (evt.which === 27) {
this.setState({ isEditing: false },function(){
this.refs.editInput.getDOMNode().blur();
});
}
},
handleBlur: function() {
var text = this.state.editValue; // because of the linkState call in render, this is the contents of the field
// unless we're not editing (escape was pressed) or text is empty, save!
if (this.state.isEditing && text) {
TodoActions.editItem(this.props.id, text);
}
// whatever the outcome, if we left the field we're not editing anymore
this.setState({isEditing:false});
},
handleDestroy: function() {
TodoActions.removeItem(this.props.id);
},
render: function() {
var classes = React.addons.classSet({
'completed': this.props.isComplete,
'editing': this.state.isEditing
});
return (
);
}
});
// Renders the todo list as well as the toggle all button
// Used in TodoApp
var TodoMain = React.createClass({
mixins: [ ReactRouter.State ],
propTypes: {
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
},
toggleAll: function(evt) {
TodoActions.toggleAllItems(evt.target.checked);
},
render: function() {
var filteredList;
switch(this.getPath()){
case '/completed':
filteredList = _.filter(this.props.list,function(item){ return item.isComplete; });
break;
case '/active':
filteredList = _.filter(this.props.list,function(item){ return !item.isComplete; });
break;
default:
filteredList = this.props.list;
}
var classes = React.addons.classSet({
"hidden": this.props.list.length < 1
});
return (
{ filteredList.map(function(item){
return ;
})}
);
}
});
// Renders the headline and the form for creating new todos.
// Used in TodoApp
// Observe that the toogleall button is NOT rendered here, but in TodoMain (it is then moved up to the header with CSS)
var TodoHeader = React.createClass({
handleValueChange: function(evt) {
var text = evt.target.value;
if (evt.which === 13 && text) { // hit enter, create new item if field isn't empty
TodoActions.addItem(text);
evt.target.value = '';
} else if (evt.which === 27) { // hit escape, clear without creating
evt.target.value = '';
}
},
render: function() {
return (
todos
);
}
});
// Renders the bottom item count, navigation bar and clearallcompleted button
// Used in TodoApp
var TodoFooter = React.createClass({
propTypes: {
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
},
render: function() {
var nbrcompleted = _.filter(this.props.list, "isComplete").length,
nbrtotal = this.props.list.length,
nbrincomplete = nbrtotal-nbrcompleted,
clearButtonClass = React.addons.classSet({hidden: nbrcompleted < 1}),
footerClass = React.addons.classSet({hidden: !nbrtotal }),
completedLabel = "Clear completed (" + nbrcompleted + ")",
itemsLeftLabel = nbrincomplete === 1 ? " item left" : " items left";
return (
);
}
});
// Renders the full application
// RouteHandler will always be TodoMain, but with different 'showing' prop (all/completed/active)
var TodoApp = React.createClass({
// this will cause setState({list:updatedlist}) whenever the store does trigger(updatedlist)
mixins: [Reflux.connect(todoListStore,"list")],
render: function() {
return (
);
}
});
var routes = (
);
ReactRouter.run(routes, function(Handler) {
React.render(, document.getElementById('todoapp'));
});
})(window.React, window.ReactRouter, window.Reflux, window.TodoActions, window.todoListStore, window);
================================================
FILE: js/store.js
================================================
(function(Reflux, TodoActions, global) {
'use strict';
// some variables and helpers for our fake database stuff
var todoCounter = 0,
localStorageKey = "todos";
function getItemByKey(list,itemKey){
return _.find(list, function(item) {
return item.key === itemKey;
});
}
global.todoListStore = Reflux.createStore({
// this will set up listeners to all publishers in TodoActions, using onKeyname (or keyname) as callbacks
listenables: [TodoActions],
onEditItem: function(itemKey, newLabel) {
var foundItem = getItemByKey(this.list,itemKey);
if (!foundItem) {
return;
}
foundItem.label = newLabel;
this.updateList(this.list);
},
onAddItem: function(label) {
this.updateList([{
key: todoCounter++,
created: new Date(),
isComplete: false,
label: label
}].concat(this.list));
},
onRemoveItem: function(itemKey) {
this.updateList(_.filter(this.list,function(item){
return item.key!==itemKey;
}));
},
onToggleItem: function(itemKey) {
var foundItem = getItemByKey(this.list,itemKey);
if (foundItem) {
foundItem.isComplete = !foundItem.isComplete;
this.updateList(this.list);
}
},
onToggleAllItems: function(checked) {
this.updateList(_.map(this.list, function(item) {
item.isComplete = checked;
return item;
}));
},
onClearCompleted: function() {
this.updateList(_.filter(this.list, function(item) {
return !item.isComplete;
}));
},
// called whenever we change a list. normally this would mean a database API call
updateList: function(list){
localStorage.setItem(localStorageKey, JSON.stringify(list));
// if we used a real database, we would likely do the below in a callback
this.list = list;
this.trigger(list); // sends the updated list to all listening components (TodoApp)
},
// this will be called by all listening components as they register their listeners
getInitialState: function() {
var loadedList = localStorage.getItem(localStorageKey);
if (!loadedList) {
// If no list is in localstorage, start out with a default one
this.list = [{
key: todoCounter++,
created: new Date(),
isComplete: false,
label: 'Rule the web'
}];
} else {
this.list = _.map(JSON.parse(loadedList), function(item) {
// just resetting the key property for each todo item
item.key = todoCounter++;
return item;
});
}
return this.list;
}
});
})(window.Reflux, window.TodoActions, window);
================================================
FILE: package.json
================================================
{
"name": "reflux-todo",
"version": "0.0.1",
"description": "Todo example for the reflux project",
"main": "index.js",
"scripts": {
"prepublish": "bower install",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-connect": "^0.8.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-open": "^0.2.3",
"matchdep": "^0.3.0"
}
}
================================================
FILE: readme.md
================================================
# ReactJS w. RefluxJS TodoMVC Example
> A simple library for uni-directional dataflow application architecture inspired by ReactJS [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html)
> _[RefluxJS](https://github.com/spoike/refluxjs)_
## Implementation
TODO
## Running
Install dependencies with bower and npm. You'll first need to have [bower](http://bower.io/) and [npm](npmjs.org) installed to do so. Then run the following:
```
bower install && npm install
```
This project comes with a grunt task to runs a [`connect`](https://github.com/gruntjs/grunt-contrib-connect) web server and opens up the web browser for you. Just run:
```
grunt
```
## Credit
This TodoMVC application was created by [Mikael Brassman](https://github.com/spoike/refluxjs).
================================================
FILE: readme_template.md
================================================
# Template • [TodoMVC](http://todomvc.com)
## Getting Started
Read the [App Specification](https://github.com/tastejs/todomvc/blob/master/app-spec.md) before touching the template.
## Need help?
Feel free to contact [Sindre](https://github.com/sindresorhus) or [Pascal](https://github.com/passy) if you have any questions or need help with the template.
## Credit
Created by [Sindre Sorhus](http://sindresorhus.com)