Skip to content

Instantly share code, notes, and snippets.

@rossipedia
Last active January 14, 2016 18:42
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rossipedia/04256d1c40c595d83bbb to your computer and use it in GitHub Desktop.
Save rossipedia/04256d1c40c595d83bbb to your computer and use it in GitHub Desktop.
Single HTML file version of the Redux TodoMVC example
<!DOCTYPE html>
<html>
<head>
<title>Redux TodoMVC example</title>
<link rel="stylesheet" href="https://npmcdn.com/todomvc-app-css@2.0.4/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/classnames@2.2.3"></script>
<script src="https://npmcdn.com/redux-logger@2.3.1/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>
@kole
Copy link

kole commented Jan 12, 2016

God this is freakin' gold man, thanks! Now I can ditch babel and go back to using Coffeescript!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment