TodoMVC Flux Example

This commit is contained in:
fisherwebdev 2014-03-14 10:37:56 -07:00
parent 308c9a0752
commit 85339bfdae
22 changed files with 1953 additions and 0 deletions

View File

@ -0,0 +1,25 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* base.css overrides
*/
/**
* We are not changing from display:none, but rather re-rendering instead.
* Therefore this needs to be displayed normally by default.
*/
#todo-list li .edit {
display: inline;
}

View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flux • TodoMVC</title>
<link rel="stylesheet" href="todomvc-common/base.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section id="todoapp" />
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://facebook.com/bill.fisher.771">Bill Fisher</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="todomvc-common/base.js"></script>
<script src="js/bundle.js"></script>
</body>
</html>

View File

@ -0,0 +1,95 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* TodoActions
*/
var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');
var TodoActions = {
/**
* @param {string} text
*/
create: function(text) {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_CREATE,
text: text
});
},
/**
* @param {string} id The ID of the ToDo item
* @param {string} text
*/
updateText: function(id, text) {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_UPDATE_TEXT,
id: id,
text: text
});
},
/**
* Toggle whether a single ToDo is complete
* @param {object} todo
*/
toggleComplete: function(todo) {
var id = todo.id;
if (todo.complete) {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_UNDO_COMPLETE,
id: id
});
} else {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_COMPLETE,
id: id
});
}
},
/**
* Mark all ToDos as complete
*/
toggleCompleteAll: function() {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
});
},
/**
* @param {string} id
*/
destroy: function(id) {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_DESTROY,
id: id
});
},
/**
* Delete all the completed ToDos
*/
destroyCompleted: function() {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_DESTROY_COMPLETED
});
}
};
module.exports = TodoActions;

View File

@ -0,0 +1,26 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
*/
var React = require('react');
var TodoApp = require('./components/TodoApp.react');
React.renderComponent(
<TodoApp />,
document.getElementById('todoapp')
);

View File

@ -0,0 +1,84 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
*/
var React = require('react');
var ReactPropTypes = React.PropTypes;
var TodoActions = require('../actions/TodoActions');
var Footer = React.createClass({
propTypes: {
allTodos: ReactPropTypes.object.isRequired
},
/**
* @return {object}
*/
render: function() {
var allTodos = this.props.allTodos;
var total = Object.keys(allTodos).length;
if (total === 0) {
return <noscript />;
}
var completed = 0;
for (var key in allTodos) {
if (allTodos[key].complete) {
completed++;
}
}
var itemsLeft = total - completed;
var itemsLeftPhrase = itemsLeft === 1 ? ' item ' : ' items ';
itemsLeftPhrase += 'left';
// Undefined and thus not rendered if no completed items are left.
var clearCompletedButton;
if (completed) {
clearCompletedButton =
<button
id="clear-completed"
onClick={this._onClearCompletedClick}>
Clear completed ({completed})
</button>;
}
return (
<footer id="footer">
<span id="todo-count">
<strong>
{itemsLeft}
</strong>
{itemsLeftPhrase}
</span>
{clearCompletedButton}
</footer>
);
},
/**
* Event handler to delete all completed TODOs
*/
_onClearCompletedClick: function() {
TodoActions.destroyCompleted();
}
});
module.exports = Footer;

View File

@ -0,0 +1,53 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
*/
var React = require('react');
var TodoActions = require('../actions/TodoActions');
var TodoTextInput = require('./TodoTextInput.react');
var Header = React.createClass({
/**
* @return {object}
*/
render: function() {
return (
<header id="header">
<h1>todos</h1>
<TodoTextInput
id="new-todo"
placeholder="What needs to be done?"
onSave={this._onSave}
/>
</header>
);
},
/**
* Event handler called within TodoTextInput.
* Defining this here allows TodoTextInput to be used in multiple places
* in different ways.
* @param {string} text
*/
_onSave: function(text) {
TodoActions.create(text);
}
});
module.exports = Header;

View File

@ -0,0 +1,71 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
*/
var React = require('react');
var ReactPropTypes = React.PropTypes;
var TodoActions = require('../actions/TodoActions');
var TodoItem = require('./TodoItem.react');
var MainSection = React.createClass({
propTypes: {
allTodos: ReactPropTypes.object.isRequired,
areAllComplete: ReactPropTypes.bool.isRequired
},
/**
* @return {object}
*/
render: function() {
// This section should be hidden by default
// and shown when there are todos.
if (Object.keys(this.props.allTodos).length < 1) {
return <noscript />;
}
var allTodos = this.props.allTodos;
var todos = [];
for (var key in allTodos) {
todos.push(<TodoItem key={key} todo={allTodos[key]} />);
}
return (
<section id="main">
<input
id="toggle-all"
type="checkbox"
onChange={this._onToggleCompleteAll}
checked={this.props.areAllComplete ? 'checked' : ''}
/>
<label htmlFor="toggle-all">Mark all as complete</label>
<ul id="todo-list">{todos}</ul>
</section>
);
},
/**
* Event handler to mark all TODOs as complete
*/
_onToggleCompleteAll: function() {
TodoActions.toggleCompleteAll();
}
});
module.exports = MainSection;

View File

@ -0,0 +1,79 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
*/
/**
* This component operates as a "Controller-View". It listens for changes in
* the TodoStore and passes the new data to its children.
*/
var Footer = require('./Footer.react');
var Header = require('./Header.react');
var MainSection = require('./MainSection.react');
var React = require('react');
var TodoStore = require('../stores/TodoStore');
/**
* Retrieve the current TODO data from the TodoStore
*/
function getTodoState() {
return {
allTodos: TodoStore.getAll(),
areAllComplete: TodoStore.areAllComplete()
};
}
var TodoApp = React.createClass({
getInitialState: function() {
return getTodoState();
},
componentDidMount: function() {
TodoStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
TodoStore.removeChangeListener(this._onChange);
},
/**
* @return {object}
*/
render: function() {
return (
<div>
<Header />
<MainSection
allTodos={this.state.allTodos}
areAllComplete={this.state.areAllComplete}
/>
<Footer allTodos={this.state.allTodos} />
</div>
);
},
/**
* Event handler for 'change' events coming from the TodoStore
*/
_onChange: function() {
this.setState(getTodoState());
}
});
module.exports = TodoApp;

View File

@ -0,0 +1,108 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
*/
var React = require('react');
var ReactPropTypes = React.PropTypes;
var TodoActions = require('../actions/TodoActions');
var TodoTextInput = require('./TodoTextInput.react');
var cx = require('react/lib/cx');
var TodoItem = React.createClass({
propTypes: {
todo: ReactPropTypes.object.isRequired
},
getInitialState: function() {
return {
isEditing: false
};
},
/**
* @return {object}
*/
render: function() {
var todo = this.props.todo;
var input;
if (this.state.isEditing) {
input =
<TodoTextInput
className="edit"
onSave={this._onSave}
value={todo.text}
/>;
}
// List items should get the class 'editing' when editing
// and 'completed' when marked as completed.
// Note that 'completed' is a classification while 'complete' is a state.
// This differentiation between classification and state becomes important
// in the naming of view actions toggleComplete() vs. destroyCompleted().
return (
<li
className={cx({
'completed': todo.complete,
'editing': this.state.isEditing
})}
key={todo.id}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.complete}
onChange={this._onToggleComplete}
/>
<label onDoubleClick={this._onDoubleClick}>
{todo.text}
</label>
<button className="destroy" onClick={this._onDestroyClick} />
</div>
{input}
</li>
);
},
_onToggleComplete: function() {
TodoActions.toggleComplete(this.props.todo);
},
_onDoubleClick: function() {
this.setState({isEditing: true});
},
/**
* Event handler called within TodoTextInput.
* Defining this here allows TodoTextInput to be used in multiple places
* in different ways.
* @param {string} text
*/
_onSave: function(text) {
TodoActions.updateText(this.props.todo.id, text);
this.setState({isEditing: false});
},
_onDestroyClick: function() {
TodoActions.destroy(this.props.todo.id);
}
});
module.exports = TodoItem;

View File

@ -0,0 +1,89 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @jsx React.DOM
*/
var React = require('react');
var ReactPropTypes = React.PropTypes;
var ENTER_KEY_CODE = 13;
var TodoTextInput = React.createClass({
propTypes: {
className: ReactPropTypes.string,
id: ReactPropTypes.string,
placeholder: ReactPropTypes.string,
onSave: ReactPropTypes.func.isRequired,
value: ReactPropTypes.string
},
getInitialState: function() {
return {
value: this.props.value || ''
};
},
/**
* @return {object}
*/
render: function() /*object*/ {
return (
<input
className={this.props.className}
id={this.props.id}
placeholder={this.props.placeholder}
onBlur={this._save}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
value={this.state.value}
autoFocus={true}
/>
);
},
/**
* Invokes the callback passed in as onSave, allowing this component to be
* used in different ways.
*/
_save: function() {
this.props.onSave(this.state.value);
this.setState({
value: ''
});
},
/**
* @param {object} event
*/
_onChange: function(/*object*/ event) {
this.setState({
value: event.target.value
});
},
/**
* @param {object} event
*/
_onKeyDown: function(event) {
if (event.keyCode === ENTER_KEY_CODE) {
this._save();
}
}
});
module.exports = TodoTextInput;

View File

@ -0,0 +1,29 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* TodoConstants
*/
var keyMirror = require('react/lib/keyMirror');
module.exports = keyMirror({
TODO_CREATE: null,
TODO_COMPLETE: null,
TODO_DESTROY: null,
TODO_DESTROY_COMPLETED: null,
TODO_TOGGLE_COMPLETE_ALL: null,
TODO_UNDO_COMPLETE: null,
TODO_UPDATE_TEXT: null
});

View File

@ -0,0 +1,41 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* AppDispatcher
*
* A singleton that operates as the central hub for application updates.
*/
var Dispatcher = require('./Dispatcher');
var merge = require('react/lib/merge');
var AppDispatcher = merge(Dispatcher.prototype, {
/**
* A bridge function between the views and the dispatcher, marking the action
* as a view action. Another variant here could be handleServerAction.
* @param {object} action The data coming from the view.
*/
handleViewAction: function(action) {
this.dispatch({
source: 'VIEW_ACTION',
action: action
});
}
});
module.exports = AppDispatcher;

View File

@ -0,0 +1,125 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Dispatcher
*
* The Dispatcher is capable of registering callbacks and invoking them.
* More robust implementations than this would include a way to order the
* callbacks for dependent Stores, and to guarantee that no two stores
* created circular dependencies.
*/
var Promise = require('es6-promise').Promise;
var merge = require('react/lib/merge');
var _callbacks = [];
var _promises = [];
/**
* Add a promise to the queue of callback invocation promises.
* @param {function} callback The Store's registered callback.
* @param {object} payload The data from the Action.
*/
var _addPromise = function(callback, payload) {
_promises.push(new Promise(function(resolve, reject) {
if (callback(payload)) {
resolve(payload);
} else {
reject(new Error('Dispatcher callback unsuccessful'));
}
}));
};
/**
* Empty the queue of callback invocation promises.
*/
var _clearPromises = function() {
_promises = [];
};
/**
* Used below in waitFor().
* @param {number} index The index within the _promises array
*/
var _getPromise = function(index) {
return _promises[index];
};
var Dispatcher = function() {};
Dispatcher.prototype = merge(Dispatcher.prototype, {
/**
* Register a Store's callback so that it may be invoked by an action.
* @param {function} callback The callback to be registered.
* @return {number} The index of the callback within the _callbacks array.
*/
register: function(callback) {
_callbacks.push(callback);
return _callbacks.length - 1; // index
},
/**
* dispatch
* @param {object} payload The data from the action.
*/
dispatch: function(payload) {
_callbacks.forEach(function(callback) {
_addPromise(callback, payload);
});
Promise.all(_promises).then(_clearPromises);
},
/**
* Allows a store to wait for the registered callbacks of other stores
* to get invoked before its own does.
* This function is not used by this TodoMVC example application, but
* it is very useful in a larger, more complex application.
*
* Example usage where StoreB waits for StoreA:
*
* var StoreA = merge(EventEmitter.prototype, {
* // other methods omitted
*
* dispatchIndex: Dispatcher.register(function(payload) {
* // switch statement with lots of cases
* })
* }
*
* var StoreB = merge(EventEmitter.prototype, {
* // other methods omitted
*
* dispatchIndex: Dispatcher.register(function(payload) {
* switch(payload.action.actionType) {
*
* case MyConstants.FOO_ACTION:
* Dispatcher.waitFor([StoreA.dispatchIndex], function() {
* // Do stuff only after StoreA's callback returns.
* });
* }
* })
* }
*
* It should be noted that if StoreB waits for StoreA, and StoreA waits for
* StoreB, a circular dependency will occur, but no error will be thrown.
* A more robust Dispatcher would issue a warning in this scenario.
*/
waitFor: function(/*array*/ promiseIndexes, /*function*/ callback) {
var selectedPromises = promiseIndexes.filter(_getPromise);
Promise.all(selectedPromises).then(callback);
}
});
module.exports = Dispatcher;

View File

@ -0,0 +1,186 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* TodoStore
*/
var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var TodoConstants = require('../constants/TodoConstants');
var merge = require('react/lib/merge');
var CHANGE_EVENT = 'change';
var _todos = {};
/**
* Create a TODO item.
* @param {string} text The content of the TODO
*/
function create(text) {
// Hand waving here -- not showing how this interacts with XHR or persistent
// server-side storage.
// Using the current timestamp in place of a real id.
var id = Date.now();
_todos[id] = {
id: id,
complete: false,
text: text
};
}
/**
* Update a TODO item.
* @param {string} id
* @param {object} updates An object literal containing only the data to be
* updated.
*/
function update(id, updates) {
_todos[id] = merge(_todos[id], updates);
}
/**
* Update all of the TODO items with the same object.
* the data to be updated. Used to mark all TODOs as completed.
* @param {object} updates An object literal containing only the data to be
* updated.
*/
function updateAll(updates) {
for (var id in _todos) {
update(id, updates);
}
}
/**
* Delete a TODO item.
* @param {string} id
*/
function destroy(id) {
delete _todos[id];
}
/**
* Delete all the completed TODO items.
*/
function destroyCompleted() {
for (var id in _todos) {
if (_todos[id].complete) {
destroy(id);
}
}
}
var TodoStore = merge(EventEmitter.prototype, {
/**
* Tests whether all the remaining TODO items are marked as completed.
* @return {booleam}
*/
areAllComplete: function() {
for (id in _todos) {
if (!_todos[id].complete) {
return false;
break;
}
}
return true;
},
/**
* Get the entire collection of TODOs.
* @return {object}
*/
getAll: function() {
return _todos;
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
/**
* @param {function} callback
*/
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
/**
* @param {function} callback
*/
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
// Register to handle all updates
AppDispatcher.register(function(payload) {
var action = payload.action;
var text;
switch(action.actionType) {
case TodoConstants.TODO_CREATE:
text = action.text.trim();
if (text !== '') {
create(text);
}
break;
case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:
if (TodoStore.areAllComplete()) {
updateAll({complete: false});
} else {
updateAll({complete: true});
}
break;
case TodoConstants.TODO_UNDO_COMPLETE:
update(action.id, {complete: false});
break;
case TodoConstants.TODO_COMPLETE:
update(action.id, {complete: true});
break;
case TodoConstants.TODO_UPDATE_TEXT:
text = action.text.trim();
if (text !== '') {
update(action.id, {text: text});
}
break;
case TodoConstants.TODO_DESTROY:
destroy(action.id);
break;
case TodoConstants.TODO_DESTROY_COMPLETED:
destroyCompleted();
break;
default:
return true;
}
// This often goes in each case that should trigger a UI change. This store
// needs to trigger a UI change after every view action, so we can make the
// code less repetitive by putting it here. We need the default case,
// however, to make sure this only gets called after one of the cases above.
TodoStore.emitChange();
return true; // No errors. Needed by promise in Dispatcher.
})
module.exports = TodoStore;

View File

@ -0,0 +1,32 @@
{
"name": "todomvc-flux",
"version": "0.0.1",
"description": "Example Flux architecture.",
"main": "js/app.js",
"dependencies": {
"es6-promise": "~0.1.1",
"react": "~0.9"
},
"devDependencies": {
"browserify": "~2.36.0",
"envify": "~1.2.0",
"reactify": "~0.4.0",
"statics": "~0.1.0",
"uglifyjs": "~2.3.6",
"watchify": "~0.4.1"
},
"scripts": {
"start": "STATIC_ROOT=./static watchify -o js/bundle.js -v -d .",
"build": "STATIC_ROOT=./static NODE_ENV=production browserify . | uglifyjs -cm > js/bundle.min.js",
"collect-static": "collect-static . ./static",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Bill Fisher",
"license": "Apache 2",
"browserify": {
"transform": [
"reactify",
"envify"
]
}
}

View File

@ -0,0 +1,100 @@
# Flux TodoMVC Example
> An application architecture for React utilizing a unidirectional data flow.
## Learning Flux
The [React website](http://facebook.github.io/react) is a great resource for getting started.
A post on the [React Blog](http://facebook.github.io/react/blog/) is forthcoming to describe the Flux architecture in more detail.
## Implementation
Flux applications have three major parts: the Dispatcher, the Stores, and the Views (React components). These should not be confused with Model-View-Controller. Controllers do exist in a Flux application, but they are Controller-Views -- top level views that retrieve data from the Stores and pass this data down to their children.
Data in a Flux application flows in a single direction, in a cycle:
<pre>
Views ---> (actions) ----> Dispatcher ---> (registerd callback) ---> Stores --------+
Ʌ |
| V
+-- (Controller-Views "change" event handlers) ---- (Stores emit "change" events) --+
</pre>
All data flows through the Dispatcher as a central hub. Actions most often originate from user interactions with the Views, and are nothing more than a call into the Dispatcher. The Dispatcher then calls the callbacks that the Stores have registered with it, effectively dispatching the data contained in the actions to all Stores. Within their registered callbacks, Stores determine which actions they are interested in, and respond accordingly. The stores then emit a "change" event to alert the Views that a change to the data layer has occurred. Controller-Views listen for these events and retrieve data from the Stores in an event handler. The View-Controllers call their own render() method via setState() or forceUpdate(), updating themselves and all of their children.
In this TodoMVC example application, we can see these elements in our directory structure. Views here are referred to as "components" as they are React components.
<pre>
./
index.html
js/
actions/
TodoActions.js
app.js
bundle.js
dispatcher/
AppDispatcher.js
Dispatcher.js
components/
Footer.react.js
Header.react.js
MainSection.react.js
TodoApp.react.js
TodoItem.react.js
TodoTextInput.react.js
stores/
TodoStore.js
</pre>
The primary entry point into the application is app.js. This file bootstraps the React rendering inside of index.html. TodoApp.js is our Controller-View and it passes all data down into its child React components.
TodoActions.js is a collection of actions that views may call from within their event handlers, in response to user interactions. They are nothing more than helpers that call into the AppDispatcher.
Dispatcher.js is a base class for AppDispatcher.js which extends it with a small amount of application-specific code. This Dispatcher is a naive implementation based on promises, but much more robust implementations are possible.
TodoStore.js is our only Store. It provides all of the application logic and in-memory storage. Based on EventEmitter from Node.js, it emits "change" events after responding to actions in the callback it registers with the Dispatcher.
The bundle.js file is automatically genenerated by the build process, explained below.
## Running
You must have [npm](https://www.npmjs.org/) installed on your computer.
From the root project directory run these commands from the command line:
npm install
This will install all dependencies.
To build the project, first run this command:
npm start
This will perform an initial build and start a watcher process that will update build.js with any changes you wish to make. This watcher is based on [Browserify](http://browserify.org/) and [Watchify](https://github.com/substack/watchify), and it transforms React's JSX syntax into standard JavaScript with [Reactify](https://github.com/andreypopp/reactify).
To run the app, spin up an HTTP server and visit http://localhost/.../todomvc-flux/.
## Credit
This TodoMVC application was created by [Bill Fisher](https://www.facebook.com/bill.fisher.771).
## License
> Copyright 2013-2014 Facebook, Inc.
>
> Licensed under the Apache License, Version 2.0 (the "License");
> you may not use this file except in compliance with the License.
> You may obtain a copy of the License at
>
> http://www.apache.org/licenses/LICENSE-2.0
>
> Unless required by applicable law or agreed to in writing, software
> distributed under the License is distributed on an "AS IS" BASIS,
> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> See the License for the specific language governing permissions and
> limitations under the License.

View File

@ -0,0 +1,14 @@
{
"name": "todomvc-common",
"version": "0.1.9",
"homepage": "https://github.com/tastejs/todomvc-common",
"_release": "0.1.9",
"_resolution": {
"type": "version",
"tag": "v0.1.9",
"commit": "7dd61b0ebf56c020e719a69444442cc7ae7242ff"
},
"_source": "git://github.com/tastejs/todomvc-common.git",
"_target": "~0.1.4",
"_originalSource": "todomvc-common"
}

View File

@ -0,0 +1,556 @@
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
button,
input[type="checkbox"] {
outline: none;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
/* Mobile Safari */
border: none;
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-ms-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden {
display: none;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}

View File

@ -0,0 +1,209 @@
(function () {
'use strict';
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
}
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base;
[/labs/, /\w*-examples/].forEach(function (href) {
var match = location.href.match(href);
if (!base && match) {
base = location.href.indexOf(match);
}
});
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').getAttribute('data-framework');
}
if (template && learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.template = template;
this.append();
}
}
Learn.prototype.append = function () {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
});
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
redirect();
getFile('learn.json', Learn);
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,4 @@
{
"name": "todomvc-common",
"version": "0.1.9"
}

View File

@ -0,0 +1,8 @@
# todomvc-common
> Bower component for some common utilities we use in every app
## License
MIT