├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── client ├── index.html ├── index.js ├── main.tsx ├── main │ ├── components │ │ └── App.tsx │ └── reducer.ts └── todos │ ├── __spec__ │ ├── actions-spec.ts │ └── reducer-spec.ts │ ├── actions.ts │ ├── components │ ├── Footer.tsx │ ├── Header.tsx │ ├── MainSection.tsx │ ├── TodoItem.tsx │ └── TodoTextInput.tsx │ ├── constants │ ├── ActionTypes.ts │ └── TodoFilters.ts │ ├── index.ts │ ├── model.ts │ └── reducer.ts ├── internals └── webpack │ ├── webpack.dev.config.js │ ├── webpack.prod.config.js │ └── webpack.shared.config.js ├── package-lock.json ├── package.json ├── server ├── index.js └── setup.js ├── test-setup.js ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "latest", { "modules": false} ], 4 | "stage-2", 5 | "react" 6 | ] 7 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "ecmaFeatures": { 8 | "arrowFunctions": true, 9 | "blockBindings": true, 10 | "classes": true, 11 | "defaultParams": true, 12 | "destructuring": true, 13 | "forOf": true, 14 | "modules": true, 15 | "objectLiteralComputedProperties": true, 16 | "objectLiteralShorthandMethods": true, 17 | "objectLiteralShorthandProperties": true, 18 | "spread": true, 19 | "superInFunctions": true, 20 | "templateStrings": true, 21 | "unicodeCodePointEscapes": true, 22 | "jsx": true 23 | }, 24 | "rules": { 25 | "react/jsx-boolean-value": 2, 26 | "react/jsx-quotes": 2, 27 | "react/jsx-no-undef": 2, 28 | "react/jsx-sort-props": 0, 29 | "react/jsx-sort-prop-types": 0, 30 | "react/jsx-uses-react": 2, 31 | "react/jsx-uses-vars": 2, 32 | "react/no-did-mount-set-state": 2, 33 | "react/no-did-update-set-state": 2, 34 | "react/no-multi-comp": 2, 35 | "react/no-unknown-property": 1, 36 | "react/prop-types": 1, 37 | "react/react-in-jsx-scope": 2, 38 | "react/self-closing-comp": 2, 39 | "react/wrap-multilines": 0 40 | }, 41 | "plugins": [ 42 | "react" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | build/ 4 | node_modules/ 5 | typings/ 6 | npm-debug.log 7 | tmp/ 8 | dist/ 9 | .sw[a-z] 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![https://travis-ci.org/jaysoo/todomvc-redux-react-typescript](https://api.travis-ci.org/jaysoo/todomvc-redux-react-typescript.svg) 2 | 3 | This is an implementation of [TodoMVC](http://todomvc.com/) built using: 4 | 5 | - [React & ReactDOM](http://facebook.github.io/react/) 15.4.2 6 | - [Redux](https://github.com/rackt/redux) 3.6.0 7 | - [TypeScript](http://www.typescriptlang.org/) 2.2.1 8 | 9 | It is adapted from the [redux TodoMVC example](https://github.com/rackt/redux/tree/master/examples/todomvc). 10 | 11 | Read more about it in my blog post: http://jaysoo.ca/2015/09/26/typed-react-and-redux/ 12 | 13 | ## Getting Started 14 | 15 | Requirement: 16 | 17 | - NodeJS 6+ 18 | 19 | Install dependencies: 20 | 21 | ``` 22 | npm install 23 | ``` 24 | 25 | ## Running development server 26 | 27 | Run webpack dev server (for assets): 28 | 29 | ``` 30 | npm start 31 | ``` 32 | 33 | Visit [http://localhost:3000/](http://localhost:3000/). 34 | 35 | ## Running production server 36 | 37 | ``` 38 | npm run start:prod 39 | ``` 40 | 41 | Visit [http://localhost:3000/](http://localhost:3000/). 42 | 43 | This will build the assets for you on the first run. For subsequent starts, you should run: 44 | 45 | ``` 46 | npm run build 47 | ``` 48 | 49 | ### Testing 50 | 51 | To run tests, use: 52 | 53 | ``` 54 | npm test 55 | ``` 56 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TodoMVC 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import 'todomvc-app-css/index.css'; 2 | import './main'; 3 | -------------------------------------------------------------------------------- /client/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Store, createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import App from './main/components/App'; 7 | import rootReducer from './main/reducer'; 8 | 9 | const initialState = {}; 10 | 11 | const store: Store = createStore(rootReducer, initialState); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | document.getElementById('app') 18 | ); -------------------------------------------------------------------------------- /client/main/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as React from 'react'; 4 | 5 | import { 6 | Header, 7 | MainSection, 8 | model, 9 | addTodo, 10 | editTodo, 11 | clearCompleted, 12 | completeAll, 13 | completeTodo, 14 | deleteTodo 15 | } from '../../todos'; 16 | 17 | interface AppProps { 18 | todos: model.Todo[]; 19 | dispatch: Dispatch<{}>; 20 | } 21 | 22 | class App extends React.Component { 23 | render() { 24 | const { todos, dispatch } = this.props; 25 | 26 | return ( 27 |
28 |
dispatch(addTodo(text))} /> 29 | dispatch(editTodo(t, s))} 32 | deleteTodo={(t: model.Todo) => dispatch(deleteTodo(t))} 33 | completeTodo={(t: model.Todo) => dispatch(completeTodo(t))} 34 | clearCompleted={() => dispatch(clearCompleted())} 35 | completeAll={() => dispatch(completeAll())}/> 36 |
37 | ); 38 | } 39 | } 40 | 41 | const mapStateToProps = state => ({ 42 | todos: state.todos 43 | }); 44 | 45 | export default connect(mapStateToProps)(App); 46 | -------------------------------------------------------------------------------- /client/main/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import todos from '../todos'; 4 | 5 | const rootReducer = combineReducers({ 6 | todos 7 | }); 8 | 9 | export default rootReducer; 10 | -------------------------------------------------------------------------------- /client/todos/__spec__/actions-spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { expect } from 'chai'; 4 | 5 | import * as actions from '../actions'; 6 | 7 | describe('actions', () => { 8 | it('creates new todo', () => { 9 | const { payload: todo } = actions.addTodo('hello'); 10 | 11 | expect(todo.text).to.eql('hello'); 12 | }); 13 | 14 | it('deletes todo', () => { 15 | const { payload: todo } = actions.deleteTodo({ 16 | id: 999, 17 | text: '', 18 | completed: false 19 | }); 20 | 21 | expect(todo.id).to.eql(999); 22 | }); 23 | 24 | it('edits todo', () => { 25 | const { payload: todo } = actions.editTodo({ 26 | id: 999, 27 | text: 'hi', 28 | completed: false 29 | }, 'bye'); 30 | expect(todo).to.eql({ id: 999, text: 'bye', completed: false}); 31 | }); 32 | 33 | it('completes todo', () => { 34 | const { payload: todo } = actions.completeTodo({ 35 | id: 999, 36 | text: '', 37 | completed: false 38 | }); 39 | 40 | expect(todo.id).to.eql(999); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /client/todos/__spec__/reducer-spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { expect } from 'chai'; 4 | 5 | import reducer from '../reducer'; 6 | import { Todo } from '../model'; 7 | 8 | import { 9 | ADD_TODO, 10 | DELETE_TODO, 11 | EDIT_TODO, 12 | COMPLETE_TODO, 13 | COMPLETE_ALL, 14 | CLEAR_COMPLETED 15 | } from '../constants/ActionTypes'; 16 | 17 | describe('todo reducer', () => { 18 | it('handles add', () => { 19 | let state: Todo[] = [{ id: 0, text: '', completed: true }]; 20 | 21 | state = reducer(state, { 22 | type: ADD_TODO, 23 | payload: { text: 'hello', completed: false } 24 | }); 25 | 26 | expect(state[0]).to.eql( 27 | { id: 1, text: 'hello', completed: false } 28 | ); 29 | }); 30 | 31 | it('handles delete', () => { 32 | let state: Todo[] = [{ id: 1, text: '', completed: false }]; 33 | 34 | state = reducer(state, { 35 | type: DELETE_TODO, 36 | payload: { id: 1 } as Todo 37 | }); 38 | 39 | expect(state).to.eql([]); 40 | }); 41 | 42 | it('handles edit', () => { 43 | let state: Todo[] = [{ id: 1, text: '', completed: false }]; 44 | 45 | state = reducer(state, { 46 | type: EDIT_TODO, 47 | payload: { id: 1, text: 'hello' } as Todo 48 | }); 49 | 50 | expect(state[0]).to.eql( 51 | { id: 1, text: 'hello', completed: false } 52 | ); 53 | }); 54 | 55 | it('handles complete all', () => { 56 | 57 | let state: Todo[] = [ 58 | { id: 1, text: '', completed: false } 59 | ]; 60 | 61 | state = reducer(state, { 62 | type: COMPLETE_TODO, 63 | payload: { id: 1 } as Todo 64 | }); 65 | 66 | expect(state[0]).to.eql( 67 | { id: 1, text: '', completed: true } 68 | ); 69 | }); 70 | 71 | it('handles complete all', () => { 72 | let state: Todo[] = [ 73 | { id: 1, text: '', completed: false }, 74 | { id: 2, text: '', completed: true }, 75 | { id: 3, text: '', completed: false } 76 | ]; 77 | 78 | state = reducer(state, { 79 | type: COMPLETE_ALL, 80 | payload: {} as Todo 81 | }); 82 | 83 | expect(state).to.eql([ 84 | { id: 1, text: '', completed: true }, 85 | { id: 2, text: '', completed: true }, 86 | { id: 3, text: '', completed: true } 87 | ]); 88 | 89 | state = reducer(state, { 90 | type: COMPLETE_ALL, 91 | payload: {} as Todo 92 | }); 93 | 94 | expect(state).to.eql([ 95 | { id: 1, text: '', completed: false }, 96 | { id: 2, text: '', completed: false }, 97 | { id: 3, text: '', completed: false } 98 | ]); 99 | }); 100 | 101 | it('handles clear completed', () => { 102 | let state: Todo[] = [ 103 | { id: 1, text: '', completed: false }, 104 | { id: 2, text: '', completed: true } 105 | ]; 106 | 107 | state = reducer(state, { 108 | type: CLEAR_COMPLETED, 109 | payload: {} as Todo 110 | }); 111 | 112 | expect(state).to.eql([ 113 | { id: 1, text: '', completed: false } 114 | ]); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /client/todos/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | import { Todo } from './model'; 4 | 5 | import { 6 | ADD_TODO, 7 | DELETE_TODO, 8 | EDIT_TODO, 9 | COMPLETE_TODO, 10 | COMPLETE_ALL, 11 | CLEAR_COMPLETED 12 | } from './constants/ActionTypes'; 13 | 14 | const addTodo = createAction( 15 | ADD_TODO, 16 | (text: string) => ({ text, completed: false }) 17 | ); 18 | 19 | const deleteTodo = createAction( 20 | DELETE_TODO, 21 | (todo: Todo) => todo 22 | ); 23 | 24 | const editTodo = createAction( 25 | EDIT_TODO, 26 | (todo: Todo, newText: string) => ({ ...todo, text: newText }) 27 | ); 28 | 29 | const completeTodo = createAction( 30 | COMPLETE_TODO, 31 | (todo: Todo) => todo 32 | ) 33 | 34 | const completeAll = createAction( 35 | COMPLETE_ALL, 36 | () => { } 37 | ) 38 | 39 | const clearCompleted = createAction( 40 | CLEAR_COMPLETED, 41 | () => { } 42 | ); 43 | 44 | export { 45 | addTodo, 46 | deleteTodo, 47 | editTodo, 48 | completeTodo, 49 | completeAll, 50 | clearCompleted 51 | } 52 | -------------------------------------------------------------------------------- /client/todos/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as classNames from 'classnames'; 3 | 4 | import { 5 | SHOW_ALL, 6 | SHOW_COMPLETED, 7 | SHOW_ACTIVE 8 | } from '../constants/TodoFilters'; 9 | 10 | const FILTER_TITLES = { 11 | [SHOW_ALL]: 'All', 12 | [SHOW_ACTIVE]: 'Active', 13 | [SHOW_COMPLETED]: 'Completed' 14 | }; 15 | 16 | 17 | interface FooterProps { 18 | completedCount: number; 19 | activeCount: number; 20 | filter: string; 21 | onClearCompleted: ()=>void; 22 | onShow: (filter:string)=>void; 23 | } 24 | 25 | class Footer extends React.Component { 26 | renderTodoCount() { 27 | const { activeCount } = this.props; 28 | const itemWord = activeCount === 1 ? 'item' : 'items'; 29 | 30 | return ( 31 | 32 | {activeCount || 'No'} {itemWord} left 33 | 34 | ); 35 | } 36 | 37 | renderFilterLink(filter) { 38 | const title = FILTER_TITLES[filter]; 39 | const { filter: selectedFilter, onShow } = this.props; 40 | 41 | return ( 42 | onShow(filter)}> 45 | {title} 46 | 47 | ); 48 | } 49 | 50 | renderClearButton() { 51 | const { completedCount, onClearCompleted } = this.props; 52 | if (completedCount > 0) { 53 | return ( 54 | 58 | ); 59 | } 60 | } 61 | 62 | render() { 63 | return ( 64 |
65 | {this.renderTodoCount()} 66 |
    67 | {[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter => 68 |
  • 69 | {this.renderFilterLink(filter)} 70 |
  • 71 | )} 72 |
73 | {this.renderClearButton()} 74 |
75 | ); 76 | } 77 | } 78 | 79 | export default Footer; 80 | -------------------------------------------------------------------------------- /client/todos/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import TodoTextInput from './TodoTextInput'; 4 | 5 | interface HeaderProps { 6 | addTodo: (text:string)=> any; 7 | }; 8 | 9 | class Header extends React.Component { 10 | handleSave(text: string) { 11 | if (text.length !== 0) { 12 | this.props.addTodo(text); 13 | } 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 |

todos

20 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | export default Header; 30 | -------------------------------------------------------------------------------- /client/todos/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Todo } from '../model'; 4 | import TodoItem from './TodoItem'; 5 | import Footer from './Footer'; 6 | import { 7 | SHOW_ALL, 8 | SHOW_COMPLETED, 9 | SHOW_ACTIVE 10 | } from '../constants/TodoFilters'; 11 | 12 | const TODO_FILTERS = { 13 | [SHOW_ALL]: () => true, 14 | [SHOW_ACTIVE]: todo => !todo.completed, 15 | [SHOW_COMPLETED]: todo => todo.completed 16 | }; 17 | 18 | interface MainSectionProps { 19 | todos: Todo[]; 20 | clearCompleted: ()=>void; 21 | completeAll: ()=>void; 22 | editTodo: (todo:Todo, text:string)=>void; 23 | completeTodo: (todo:Todo)=>void; 24 | deleteTodo: (todo:Todo)=>void; 25 | }; 26 | interface MainSectionState { 27 | filter: string; 28 | }; 29 | 30 | class MainSection extends React.Component { 31 | constructor(props, context) { 32 | super(props, context); 33 | this.state = { filter: SHOW_ALL }; 34 | } 35 | 36 | handleClearCompleted() { 37 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed); 38 | if (atLeastOneCompleted) { 39 | this.props.clearCompleted(); 40 | } 41 | } 42 | 43 | handleShow(filter) { 44 | this.setState({ filter }); 45 | } 46 | 47 | renderToggleAll(completedCount) { 48 | const { todos, completeAll } = this.props; 49 | if (todos.length > 0) { 50 | return ( 51 | completeAll()} /> 55 | ); 56 | } 57 | } 58 | 59 | renderFooter(completedCount) { 60 | const { todos } = this.props; 61 | const { filter } = this.state; 62 | const activeCount = todos.length - completedCount; 63 | 64 | if (todos.length) { 65 | return ( 66 |