Last active
January 14, 2016 18:42
-
-
Save rossipedia/04256d1c40c595d83bbb to your computer and use it in GitHub Desktop.
Single HTML file version of the Redux TodoMVC example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Redux TodoMVC example</title> | |
<link rel="stylesheet" href="https://npmcdn.com/[email protected]/index.css" type="text/css" /> | |
</head> | |
<body> | |
<div class="todoapp" id="root"> | |
</div> | |
<script src="https://fb.me/react-0.14.6.js"></script> | |
<script src="https://fb.me/react-dom-0.14.6.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.0.5/redux.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.0.6/react-redux.js"></script> | |
<script src="https://npmcdn.com/[email protected]"></script> | |
<script src="https://npmcdn.com/[email protected]/dist/index.js"></script> | |
<script> | |
// Polyfill for Object.assign | |
var _extends = Object.assign || function (target) { | |
'use strict'; | |
if (target === undefined || target === null) { | |
throw new TypeError('Cannot convert undefined or null to object'); | |
} | |
var output = Object(target); | |
for (var index = 1; index < arguments.length; index++) { | |
var source = arguments[index]; | |
if (source !== undefined && source !== null) { | |
for (var nextKey in source) { | |
if (source.hasOwnProperty(nextKey)) { | |
output[nextKey] = source[nextKey]; | |
} | |
} | |
} | |
} | |
return output; | |
}; | |
// ==================================================== | |
// Constants | |
// ==================================================== | |
// Action Types | |
var ADD_TODO = 'ADD_TODO'; | |
var DELETE_TODO = 'DELETE_TODO'; | |
var EDIT_TODO = 'EDIT_TODO'; | |
var COMPLETE_TODO = 'COMPLETE_TODO'; | |
var COMPLETE_ALL = 'COMPLETE_ALL'; | |
var CLEAR_COMPLETED = 'CLEAR_COMPLETED'; | |
// Filters | |
var SHOW_ALL = 'show_all'; | |
var SHOW_COMPLETED = 'show_completed'; | |
var SHOW_ACTIVE = 'show_active'; | |
// ==================================================== | |
// ==================================================== | |
// Imports | |
// ==================================================== | |
var render = ReactDOM.render; | |
var PropTypes = React.PropTypes; | |
var Component = React.Component; | |
var combineReducers = Redux.combineReducers; | |
var createStore = Redux.createStore; | |
var applyMiddleware = Redux.applyMiddleware; | |
var bindActionCreators = Redux.bindActionCreators; | |
var Provider = ReactRedux.Provider; | |
var connect = ReactRedux.connect; | |
// ==================================================== | |
// ==================================================== | |
// Reducers | |
// ==================================================== | |
var todos = (function () { | |
var initialState = [ | |
{ | |
text: 'Use Redux', | |
completed: false, | |
id: 0 | |
} | |
]; | |
return function todos(state, action) { | |
if (typeof state === 'undefined') { | |
state = initialState; | |
} | |
switch (action.type) { | |
case ADD_TODO: | |
return [ | |
{ | |
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, | |
completed: false, | |
text: action.text | |
}, | |
].concat(state); | |
case DELETE_TODO: | |
return state.filter(function (todo) { return todo.id !== action.id; }); | |
case EDIT_TODO: | |
return state.map(function (todo) { | |
return todo.id === action.id | |
? _extends({}, todo, { text: action.text }) | |
: todo; | |
}); | |
case COMPLETE_TODO: | |
return state.map(function (todo) { | |
return todo.id === action.id | |
? _extends({}, todo, { completed: !todo.completed }) | |
: todo; | |
}); | |
case COMPLETE_ALL: | |
var areAllMarked = state.every(function (todo) { return todo.completed; }); | |
return state.map(function (todo) { | |
return _extends({}, todo, { completed: !areAllMarked }); | |
}); | |
case CLEAR_COMPLETED: | |
return state.filter(function (todo) { return todo.completed === false; }); | |
default: | |
return state; | |
} | |
} | |
})(); | |
var rootReducer = combineReducers({ | |
todos: todos | |
}); | |
// ==================================================== | |
// ==================================================== | |
// Actions | |
// ==================================================== | |
var TodoActions = { | |
addTodo: function (text) { | |
return { type: ADD_TODO, text }; | |
}, | |
deleteTodo: function (id) { | |
return { type: DELETE_TODO, id }; | |
}, | |
editTodo: function (id, text) { | |
return { type: EDIT_TODO, id, text }; | |
}, | |
completeTodo: function (id) { | |
return { type: COMPLETE_TODO, id }; | |
}, | |
completeAll: function () { | |
return { type: COMPLETE_ALL }; | |
}, | |
clearCompleted: function () { | |
return { type: CLEAR_COMPLETED }; | |
} | |
}; | |
// ==================================================== | |
// ==================================================== | |
// Factories | |
// ==================================================== | |
var createFactory = React.createFactory; | |
var a = React.DOM.a; | |
var button = React.DOM.button; | |
var div = React.DOM.div; | |
var footer = React.DOM.footer; | |
var h1 = React.DOM.h1; | |
var header = React.DOM.header; | |
var input = React.DOM.input; | |
var label = React.DOM.label; | |
var li = React.DOM.li; | |
var section = React.DOM.section; | |
var span = React.DOM.span; | |
var strong = React.DOM.strong; | |
var ul = React.DOM.ul; | |
// ==================================================== | |
// ==================================================== | |
// Components | |
// ==================================================== | |
// Display | |
// ---------------------------------------------------- | |
// TodoTextInput | |
// ---------------------------------------------------- | |
var TodoTextInput = React.createClass({ | |
getInitialState : function () { | |
return { text: this.props.text || '' }; | |
}, | |
propTypes: { | |
onSave : PropTypes.func.isRequired, | |
text : PropTypes.string, | |
placeholder : PropTypes.string, | |
editing : PropTypes.bool, | |
newTodo : PropTypes.bool | |
}, | |
// handleSubmit(e) { | |
handleSubmit: function (e) { | |
var text = e.target.value.replace(/^\s+|\s+$/g, ''); | |
if (e.which === 13) { | |
this.props.onSave(text) | |
if (this.props.newTodo) { | |
this.setState({ text: '' }) | |
} | |
} | |
}, | |
handleChange: function(e) { | |
this.setState({ text: e.target.value }) | |
}, | |
handleBlur: function(e) { | |
if (!this.props.newTodo) { | |
this.props.onSave(e.target.value) | |
} | |
}, | |
render: function() { | |
return input({ | |
className: classNames({ | |
edit : this.props.editing, | |
'new-todo' : this.props.newTodo | |
}), | |
type : 'text', | |
placeholder : this.props.placeholder, | |
autoFocus : true, | |
value : this.state.text, | |
onBlur : this.handleBlur, | |
onChange : this.handleChange, | |
onKeyDown : this.handleSubmit | |
}); | |
} | |
}); | |
TodoTextInput = createFactory(TodoTextInput); | |
// ---------------------------------------------------- | |
// TodoItem | |
// ---------------------------------------------------- | |
// import React, { Component, PropTypes } from 'react' | |
// import classnames from 'classnames' | |
// import TodoTextInput from './TodoTextInput' | |
var TodoItem = React.createClass({ | |
displayName: 'TodoItem', | |
propTypes: { | |
todo : PropTypes.object.isRequired, | |
editTodo : PropTypes.func.isRequired, | |
deleteTodo : PropTypes.func.isRequired, | |
completeTodo : PropTypes.func.isRequired | |
}, | |
getInitialState: function () { | |
return { editing: false }; | |
}, | |
handleDoubleClick: function () { | |
this.setState({ editing: true }) | |
}, | |
handleSave: function (id, text) { | |
if (text.length === 0) { | |
this.props.deleteTodo(id) | |
} else { | |
this.props.editTodo(id, text) | |
} | |
this.setState({ editing: false }) | |
}, | |
render: function () { | |
var todo = this.props.todo; | |
var completeTodo = this.props.completeTodo; | |
var deleteTodo = this.props.deleteTodo; | |
var element = null; | |
if (this.state.editing) { | |
element = TodoTextInput({ | |
text : todo.text, | |
editing : this.state.editing, | |
onSave : (function (text) { return this.handleSave(todo.id, text); }).bind(this) | |
}); | |
} else { | |
element = div( | |
{ className:"view" }, | |
input({ | |
className : "toggle", | |
type : "checkbox", | |
checked : todo.completed, | |
onChange : function() { completeTodo(todo.id); } | |
}), | |
label({ onDoubleClick:this.handleDoubleClick},todo.text), | |
button({ | |
className : "destroy", | |
onClick : function () { deleteTodo(todo.id); } | |
}) | |
); | |
} | |
return li( | |
{ | |
className: classNames({ | |
completed: todo.completed, | |
editing: this.state.editing | |
}) | |
}, | |
element | |
); | |
} | |
}); | |
TodoItem = createFactory(TodoItem); | |
// ---------------------------------------------------- | |
// Containers | |
// ---------------------------------------------------- | |
// Header | |
// ---------------------------------------------------- | |
var Header = React.createClass({ | |
displayName: 'Header', | |
propTypes: { | |
addTodo: PropTypes.func.isRequired | |
}, | |
handleSave: function (text) { | |
if (text.length !== 0) { | |
this.props.addTodo(text); | |
} | |
}, | |
render: function () { | |
return header( | |
{ className: "header" }, | |
h1(null, 'todos'), | |
TodoTextInput({ | |
newTodo : true, | |
onSave : this.handleSave, | |
placeholder : "What needs to be done?" | |
}) | |
); | |
} | |
}); | |
Header = createFactory(Header); | |
// ---------------------------------------------------- | |
// Footer | |
// ---------------------------------------------------- | |
var FILTER_TITLES = {}; | |
FILTER_TITLES[SHOW_ALL] = 'All'; | |
FILTER_TITLES[SHOW_ACTIVE] = 'Active'; | |
FILTER_TITLES[SHOW_COMPLETED] = 'Completed'; | |
var Footer = React.createClass({ | |
displayname: 'Footer', | |
propTypes: { | |
completedCount : PropTypes.number.isRequired, | |
activeCount : PropTypes.number.isRequired, | |
filter : PropTypes.string.isRequired, | |
onClearCompleted : PropTypes.func.isRequired, | |
onShow : PropTypes.func.isRequired | |
}, | |
renderTodoCount: function () { | |
var activeCount = this.props.activeCount; | |
var itemWord = activeCount === 1 ? 'item' : 'items' | |
return span( | |
{ className:'todo-count' }, | |
strong({}, activeCount || 'No'), | |
' ', | |
itemWord, | |
' left' | |
); | |
}, | |
renderFilterLink: function (filter) { | |
var title = FILTER_TITLES[filter]; | |
var selectedFilter = this.props.filter; | |
var onShow = this.props.onShow; | |
return a( | |
{ | |
className: classNames({ selected: filter === selectedFilter }), | |
style: {cursor: 'pointer'}, | |
onClick: function () { onShow(filter); } | |
}, | |
title | |
); | |
}, | |
renderClearButton: function () { | |
var completedCount = this.props.completedCount; | |
var onClearCompleted = this.props.onClearCompleted; | |
if (completedCount > 0) { | |
return button( | |
{ | |
className:"clear-completed", | |
onClick:onClearCompleted | |
}, | |
'Clear completed' | |
); | |
} | |
}, | |
render: function () { | |
return footer( | |
{ | |
className: 'footer' | |
}, | |
this.renderTodoCount(), | |
ul({ className: 'filters' }, | |
[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(function (filter) { | |
return li({ key: filter }, this.renderFilterLink(filter)); | |
}, this), | |
this.renderClearButton() | |
) | |
); | |
} | |
}); | |
Footer = createFactory(Footer); | |
// ---------------------------------------------------- | |
// MainSection | |
// ---------------------------------------------------- | |
var TODO_FILTERS = {}; | |
TODO_FILTERS[SHOW_ALL] = function() { return true }; | |
TODO_FILTERS[SHOW_ACTIVE] = function(todo) { return !todo.completed; }; | |
TODO_FILTERS[SHOW_COMPLETED] = function (todo) { return todo.completed; }; | |
var MainSection = React.createClass({ | |
displayName: 'MainSection', | |
propTypes: { | |
todos: PropTypes.array.isRequired, | |
actions: PropTypes.object.isRequired | |
}, | |
getInitialState: function () { | |
return { filter: SHOW_ALL }; | |
}, | |
handleClearCompleted: function() { | |
var atLeastOneCompleted = this.props.todos.some(todo => todo.completed) | |
if (atLeastOneCompleted) { | |
this.props.actions.clearCompleted(); | |
} | |
}, | |
handleShow: function (filter) { | |
this.setState({ filter: filter }); | |
}, | |
renderToggleAll: function (completedCount) { | |
var todos = this.props.todos; | |
var actions = this.props.actions; | |
if (todos.length > 0) { | |
return input({ | |
className : "toggle-all", | |
type : "checkbox", | |
checked : completedCount === todos.length, | |
onChange : actions.completeAll | |
}); | |
} | |
}, | |
renderFooter: function (completedCount) { | |
var todos = this.props.todos; | |
var filter = this.state.filter; | |
var activeCount = todos.length - completedCount; | |
if (todos.length) { | |
return Footer({ | |
completedCount : completedCount, | |
activeCount : activeCount, | |
filter : filter, | |
onClearCompleted : this.handleClearCompleted, | |
onShow : this.handleShow | |
}); | |
} | |
}, | |
render: function () { | |
var todos = this.props.todos; | |
var actions = this.props.actions; | |
var filter = this.state.filter; | |
var filteredTodos = todos.filter(TODO_FILTERS[filter]); | |
var completedCount = todos.reduce(function (count, todo) { | |
return todo.completed ? count + 1 : count; | |
}, 0); | |
return section( | |
{ className: 'main' }, | |
this.renderToggleAll(completedCount), | |
ul({ className: 'todo-list' }, | |
filteredTodos.map(function (todo) { | |
return TodoItem(_extends({}, { key: todo.id, todo: todo }, actions)); | |
}) | |
), | |
this.renderFooter(completedCount) | |
); | |
} | |
}); | |
MainSection = createFactory(MainSection); | |
// ---------------------------------------------------- | |
// App | |
// ---------------------------------------------------- | |
var App = React.createClass({ | |
displayName: 'App', | |
propTypes: { | |
todos : PropTypes.array.isRequired, | |
actions : PropTypes.object.isRequired | |
}, | |
render: function () { | |
var todos = this.props.todos; | |
var actions = this.props.actions; | |
return div( | |
null, | |
Header({ addTodo: actions.addTodo }), | |
MainSection({todos:todos, actions:actions}) | |
); | |
} | |
}); | |
function mapStateToProps(state) { | |
return { | |
todos: state.todos | |
} | |
} | |
function mapDispatchToProps(dispatch) { | |
return { | |
actions: bindActionCreators(TodoActions, dispatch) | |
} | |
} | |
App = createFactory(connect( | |
mapStateToProps, | |
mapDispatchToProps | |
)(App)); | |
// ---------------------------------------------------- | |
// ==================================================== | |
// ==================================================== | |
// Init the store | |
// ==================================================== | |
function configureStore(initialState) { | |
var createStoreWithMiddleware = applyMiddleware( | |
reduxLogger() | |
)(createStore); | |
return createStoreWithMiddleware(rootReducer, initialState); | |
} | |
var store = configureStore() | |
// ==================================================== | |
Provider = createFactory(Provider); | |
render(Provider({store:store}, App()), document.getElementById('root')); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
God this is freakin' gold man, thanks! Now I can ditch babel and go back to using Coffeescript!