├── .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 | 
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 |
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 |
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 |
71 | );
72 | }
73 | }
74 |
75 | render() {
76 | const { todos, completeTodo, deleteTodo, editTodo } = this.props;
77 | const { filter } = this.state;
78 |
79 | const filteredTodos = todos.filter(TODO_FILTERS[filter]);
80 | const completedCount = todos.reduce((count: number, todo): number =>
81 | todo.completed ? count + 1 : count,
82 | 0
83 | );
84 |
85 | return (
86 |
87 | {this.renderToggleAll(completedCount)}
88 |
89 | {filteredTodos.map(todo =>
90 |
96 | )}
97 |
98 | {this.renderFooter(completedCount)}
99 |
100 | );
101 | }
102 | }
103 |
104 | export default MainSection;
105 |
--------------------------------------------------------------------------------
/client/todos/components/TodoItem.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as classNames from 'classnames';
3 |
4 | import { Todo } from '../model';
5 | import TodoTextInput from './TodoTextInput';
6 |
7 | interface TodoItemProps {
8 | todo: Todo;
9 | editTodo: (todo:Todo, text:string)=>void;
10 | deleteTodo: (todo:Todo)=>void;
11 | completeTodo: (todo:Todo)=>void;
12 | key?: any;
13 | }
14 | interface TodoItemState {
15 | editing: boolean;
16 | };
17 |
18 | class TodoItem extends React.Component {
19 | constructor(props, context) {
20 | super(props, context);
21 | this.state = {
22 | editing: false
23 | };
24 | }
25 |
26 | handleDoubleClick() {
27 | this.setState({ editing: true });
28 | }
29 |
30 | handleSave(todo:Todo, text:string) {
31 | if (text.length === 0) {
32 | this.props.deleteTodo(todo);
33 | } else {
34 | this.props.editTodo(todo, text);
35 | }
36 | this.setState({ editing: false });
37 | }
38 |
39 | render() {
40 | const {todo, completeTodo, deleteTodo} = this.props;
41 |
42 | let element;
43 | if (this.state.editing) {
44 | element = (
45 | this.handleSave(todo, text)}/>
48 | );
49 | } else {
50 | element = (
51 |
52 | completeTodo(todo)} />
56 |
59 |
62 | );
63 | }
64 |
65 | return (
66 |
70 | {element}
71 |
72 | );
73 | }
74 | }
75 |
76 | export default TodoItem;
77 |
--------------------------------------------------------------------------------
/client/todos/components/TodoTextInput.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as classNames from 'classnames';
3 |
4 | interface TodoTextInputProps {
5 | onSave: (text:string)=>void;
6 | text?: string;
7 | placeholder?: string,
8 | editing?: boolean;
9 | newTodo?: boolean;
10 | }
11 | interface TodoTextInputState {
12 | text: string;
13 | }
14 |
15 | class TodoTextInput extends React.Component {
16 | constructor(props, context) {
17 | super(props, context);
18 | this.state = {
19 | text: this.props.text || ''
20 | };
21 | }
22 |
23 | handleSubmit(e) {
24 | const text = e.target.value.trim();
25 | if (e.which === 13) {
26 | this.props.onSave(text);
27 | if (this.props.newTodo) {
28 | this.setState({ text: '' });
29 | }
30 | }
31 | }
32 |
33 | handleChange(e) {
34 | this.setState({ text: e.target.value });
35 | }
36 |
37 | handleBlur(e) {
38 | if (!this.props.newTodo) {
39 | this.props.onSave(e.target.value);
40 | }
41 | }
42 |
43 | render() {
44 | return (
45 |
57 | );
58 | }
59 | }
60 |
61 |
62 | export default TodoTextInput;
63 |
--------------------------------------------------------------------------------
/client/todos/constants/ActionTypes.ts:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO';
2 | export const DELETE_TODO = 'DELETE_TODO';
3 | export const EDIT_TODO = 'EDIT_TODO';
4 | export const COMPLETE_TODO = 'COMPLETE_TODO';
5 | export const COMPLETE_ALL = 'COMPLETE_ALL';
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
--------------------------------------------------------------------------------
/client/todos/constants/TodoFilters.ts:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all';
2 | export const SHOW_COMPLETED = 'show_completed';
3 | export const SHOW_ACTIVE = 'show_active';
--------------------------------------------------------------------------------
/client/todos/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Footer } from './components/Footer';
2 | export { default as Header } from './components/Header';
3 | export { default as MainSection } from './components/MainSection';
4 | export { default as TodoItem } from './components/TodoItem';
5 | export { default as TodoTextInput } from './components/TodoTextInput';
6 | export * from './actions';
7 | import * as model from './model';
8 | export { model };
9 | import reducer from './reducer';
10 | export default reducer;
11 |
--------------------------------------------------------------------------------
/client/todos/model.ts:
--------------------------------------------------------------------------------
1 | export type Todo = {
2 | id?: number;
3 | text: string;
4 | completed: boolean;
5 | };
6 |
7 | export type IState = Todo[];
8 |
--------------------------------------------------------------------------------
/client/todos/reducer.ts:
--------------------------------------------------------------------------------
1 | import { handleActions, Action } from 'redux-actions';
2 |
3 | import { Todo, IState } from './model';
4 | import {
5 | ADD_TODO,
6 | DELETE_TODO,
7 | EDIT_TODO,
8 | COMPLETE_TODO,
9 | COMPLETE_ALL,
10 | CLEAR_COMPLETED
11 | } from './constants/ActionTypes';
12 |
13 | const initialState: IState = [{
14 | text: 'Use Redux with TypeScript',
15 | completed: false,
16 | id: 0
17 | }];
18 |
19 | export default handleActions({
20 | [ADD_TODO]: (state: IState, action: Action): IState => {
21 | return [{
22 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
23 | completed: action.payload.completed,
24 | text: action.payload.text
25 | }, ...state];
26 | },
27 |
28 | [DELETE_TODO]: (state: IState, action: Action): IState => {
29 | return state.filter(todo =>
30 | todo.id !== action.payload.id
31 | );
32 | },
33 |
34 | [EDIT_TODO]: (state: IState, action: Action): IState => {
35 | return state.map(todo =>
36 | todo.id === action.payload.id
37 | ? { ...todo, text: action.payload.text }
38 | : todo
39 | );
40 | },
41 |
42 | [COMPLETE_TODO]: (state: IState, action: Action): IState => {
43 | return state.map(todo =>
44 | todo.id === action.payload.id ?
45 | { ...todo, completed: !todo.completed } :
46 | todo
47 | );
48 | },
49 |
50 | [COMPLETE_ALL]: (state: IState, action: Action): IState => {
51 | const areAllMarked = state.every(todo => todo.completed);
52 | return state.map(todo => ({ ...todo,
53 | completed: !areAllMarked
54 | }));
55 | },
56 |
57 | [CLEAR_COMPLETED]: (state: IState, action: Action): IState => {
58 | return state.filter(todo => todo.completed === false);
59 | }
60 | }, initialState);
61 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const webpack = require('webpack');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const plugins = [
7 | new webpack.HotModuleReplacementPlugin(),
8 | new webpack.NoEmitOnErrorsPlugin(),
9 | new HtmlWebpackPlugin({
10 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js
11 | templateContent: templateContent(),
12 | })
13 | ];
14 |
15 | module.exports = require('./webpack.shared.config')({
16 | // Add hot reloading in development
17 | entry: [
18 | 'webpack-hot-middleware/client',
19 | path.join(process.cwd(), 'client/index.js'),
20 | ],
21 |
22 | output: {
23 | filename: '[name].js',
24 | chunkFilename: '[name].chunk.js',
25 | },
26 |
27 | resolve: {
28 | modules: ['app', 'src', 'node_modules'],
29 | alias: {
30 | 'src': path.join(process.cwd(), 'src')
31 | }
32 | },
33 |
34 | plugins: plugins,
35 |
36 | babelQuery: {
37 | presets: ['react-hmre'],
38 | },
39 |
40 | devtool: 'cheap-module-eval-source-map'
41 | });
42 |
43 | function templateContent() {
44 | return fs.readFileSync(
45 | path.resolve(process.cwd(), 'client/index.html')
46 | ).toString();
47 | }
48 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | module.exports = require('./webpack.shared.config')({
6 | entry: [
7 | path.join(process.cwd(), 'client/index.js'),
8 | ],
9 |
10 | output: {
11 | filename: '[name].[chunkhash].js',
12 | chunkFilename: '[name].[chunkhash].chunk.js',
13 | },
14 |
15 | plugins: [
16 | new webpack.optimize.CommonsChunkPlugin({
17 | name: 'vendor',
18 | children: true,
19 | minChunks: 2,
20 | async: true,
21 | }),
22 |
23 | new HtmlWebpackPlugin({
24 | template: 'client/index.html',
25 | minify: {
26 | removeComments: true,
27 | collapseWhitespace: true,
28 | removeRedundantAttributes: true,
29 | useShortDoctype: true,
30 | removeEmptyAttributes: true,
31 | removeStyleLinkTypeAttributes: true,
32 | keepClosingSlash: true,
33 | minifyJS: true,
34 | minifyCSS: true,
35 | minifyURLs: true,
36 | },
37 | inject: true,
38 | })
39 | ]
40 | });
41 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.shared.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = (options) => ({
5 | entry: options.entry,
6 | output: Object.assign({
7 | path: path.resolve(process.cwd(), 'build'),
8 | publicPath: '/',
9 | }, options.output),
10 | module: {
11 | loaders: [{
12 | test: /\.js$/,
13 | loader: 'babel-loader',
14 | exclude: /node_modules/,
15 | query: options.babelQuery,
16 | }, {
17 | test: /\.tsx?$/,
18 | loader: 'awesome-typescript-loader'
19 | }, {
20 | test: /\.css$/,
21 | exclude: /node_modules/,
22 | loaders: ['style-loader', 'css-loader'],
23 | }, {
24 | test: /\.css$/,
25 | include: /node_modules/,
26 | loaders: ['style-loader', 'css-loader'],
27 | }, {
28 | test: /\.(eot|svg|ttf|woff|woff2)$/,
29 | loader: 'file-loader',
30 | }, {
31 | test: /\.(jpg|png|gif)$/,
32 | loaders: [
33 | 'file-loader',
34 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}',
35 | ],
36 | }, {
37 | test: /\.html$/,
38 | loader: 'html-loader',
39 | }, {
40 | test: /\.json$/,
41 | loader: 'json-loader',
42 | }, {
43 | test: /\.(mp4|webm)$/,
44 | loader: 'url-loader?limit=10000',
45 | }],
46 | },
47 | plugins: options.plugins.concat([
48 | new webpack.NamedModulesPlugin(),
49 | ]),
50 |
51 | node: {
52 | fs: 'empty',
53 | path: 'empty',
54 | net: 'empty',
55 | fsevents: 'empty',
56 | tls: 'empty',
57 | child_process: 'empty'
58 | },
59 |
60 | resolve: Object.assign({
61 | modules: ['app', 'node_modules'],
62 | extensions: ['.ts', '.tsx', '.js'],
63 | mainFields: [
64 | 'browser',
65 | 'jsnext:main',
66 | 'main',
67 | ],
68 | }, options.resolve),
69 |
70 | devtool: options.devtool,
71 |
72 | target: 'web',
73 | });
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todomvc-redux-react-ts",
3 | "version": "2.1.0",
4 | "description": "TodoMVC example using Redux, React, and Typescript ",
5 | "repository": {
6 | "type": "git",
7 | "url": "git@github.com:jaysoo/todomvc-redux-react-typescript.git"
8 | },
9 | "scripts": {
10 | "build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.config.js --progress --profile --colors",
11 | "start": "cross-env NODE_ENV=development node server",
12 | "start:prod": "cross-env NODE_ENV=production node server",
13 | "test": "tsc && mocha --require test-setup --recursive ./dist/**/__spec__/**/*-spec.js"
14 | },
15 | "author": "Jack Hsu ",
16 | "license": "ISC",
17 | "dependencies": {
18 | "classnames": "2.2.5",
19 | "concurrently": "3.4.0",
20 | "express": "4.15.2",
21 | "extract-text-webpack-plugin": "2.1.0",
22 | "react": "16.0.0",
23 | "react-dom": "16.0.0",
24 | "react-redux": "5.0.6",
25 | "redux": "3.7.2",
26 | "redux-actions": "2.2.1",
27 | "serve-static": "1.12.1",
28 | "todomvc-app-css": "2.0.6",
29 | "yargs": "7.0.1"
30 | },
31 | "devDependencies": {
32 | "@types/assertion-error": "1.0.30",
33 | "@types/chai": "3.4.35",
34 | "@types/classnames": "0.0.32",
35 | "@types/mocha": "2.2.39",
36 | "@types/react": "16.0.10",
37 | "@types/react-dom": "16.0.1",
38 | "@types/react-redux": "5.0.10",
39 | "@types/redux-actions": "2.2.2",
40 | "awesome-typescript-loader": "3.2.3",
41 | "babel-core": "6.26.0",
42 | "babel-eslint": "8.0.1",
43 | "babel-loader": "7.1.2",
44 | "babel-polyfill": "6.26.0",
45 | "babel-preset-es2015": "6.24.1",
46 | "babel-preset-latest": "6.24.1",
47 | "babel-preset-react": "6.24.1",
48 | "babel-preset-react-hmre": "1.1.1",
49 | "babel-preset-stage-2": "6.24.1",
50 | "babel-runtime": "6.26.0",
51 | "chai": "3.5.0",
52 | "chalk": "2.1.0",
53 | "cross-env": "5.0.5",
54 | "css-loader": "0.28.7",
55 | "eslint": "4.8.0",
56 | "eslint-plugin-react": "7.4.0",
57 | "file-loader": "1.1.5",
58 | "html-loader": "0.5.1",
59 | "html-webpack-plugin": "2.30.1",
60 | "jsdom": "9.11.0",
61 | "mocha": "4.0.1",
62 | "redux-devtools": "3.4.0",
63 | "socket.io": "1.7.3",
64 | "stats-webpack-plugin": "0.6.1",
65 | "style-loader": "0.19.0",
66 | "typescript": "2.5.3",
67 | "webpack": "3.7.1",
68 | "webpack-dev-server": "2.9.1",
69 | "webpack-hot-middleware": "2.19.1"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const chalk = require('chalk');
4 | const cp = require('child_process');
5 | const buildDir = path.resolve(process.cwd(), 'build');
6 | const isProd = process.env.NODE_ENV === 'production'
7 |
8 | if (!fs.existsSync(buildDir)) {
9 | console.log(chalk.yellow('Build folder not found. Running webpack for you...'));
10 | cp.execSync(`npm run build`);
11 | } else {
12 | isProd && console.log(chalk.yellow('Starting from previous build. If you want to create a new build, run `npm run build` first.'));
13 | }
14 |
15 | require("./setup")({
16 | outputPath: buildDir,
17 | publicPath: '/',
18 | });
19 |
--------------------------------------------------------------------------------
/server/setup.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const compression = require('compression');
3 | const express = require('express');
4 | const http = require('http');
5 | const chalk = require('chalk');
6 |
7 | const isProd = process.env.NODE_ENV === 'production';
8 | const port = process.env.PORT || 3000;
9 |
10 | module.exports = function (options) {
11 | const app = express();
12 |
13 | if (isProd) {
14 | addProdMiddlewares(app, options);
15 | } else {
16 | const webpackConfig = require('../internals/webpack/webpack.dev.config');
17 | addDevMiddlewares(app, webpackConfig);
18 | }
19 |
20 | // serve the static assets
21 | app.use("/_assets", express.static(path.join(__dirname, "..", "build", "public"), {
22 | maxAge: "200d" // We can cache them as they include hashes
23 | }));
24 | app.use("/", express.static(path.join(__dirname, "..", "public"), {
25 | }));
26 |
27 | app.get("/*", function(req, res) {
28 | renderer.render(
29 | req.path,
30 | function(err, html) {
31 | if(err) {
32 | res.statusCode = 500;
33 | res.contentType = "text; charset=utf8";
34 | res.end(err.message);
35 | return;
36 | }
37 | res.contentType = "text/html; charset=utf8";
38 | res.end(html);
39 | }
40 | );
41 | });
42 |
43 | const server = http.createServer(app);
44 |
45 | server.listen(port, function () {
46 | console.log(chalk.green('Server started at http://localhost:' + port + '\n'));
47 | });
48 | };
49 |
50 |
51 | // Dev middleware
52 | function addDevMiddlewares(app, webpackConfig) {
53 | const webpack = require('webpack');
54 | const webpackDevMiddleware = require('webpack-dev-middleware');
55 | const webpackHotMiddleware = require('webpack-hot-middleware');
56 | const compiler = webpack(webpackConfig);
57 | const middleware = webpackDevMiddleware(compiler, {
58 | noInfo: true,
59 | publicPath: webpackConfig.output.publicPath,
60 | silent: true,
61 | stats: 'errors-only',
62 | });
63 |
64 | app.use(middleware);
65 | app.use(webpackHotMiddleware(compiler));
66 |
67 | const fs = middleware.fileSystem;
68 |
69 | app.get('*', (req, res) => {
70 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => {
71 | if (err) {
72 | res.sendStatus(404);
73 | } else {
74 | res.send(file.toString());
75 | }
76 | });
77 | });
78 | }
79 |
80 | function addProdMiddlewares(app, options) {
81 | const publicPath = options.publicPath || '/';
82 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build');
83 |
84 | app.use(compression());
85 | app.use(publicPath, express.static(outputPath));
86 |
87 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html')));
88 | }
--------------------------------------------------------------------------------
/test-setup.js:
--------------------------------------------------------------------------------
1 | var jsdom = require('jsdom');
2 | require('babel-polyfill');
3 |
4 | ['.wav', '.css', 'sass', '.scss'].forEach(function (ext) {
5 | require.extensions[ext] = function (module, filename) {
6 | };
7 | });
8 |
9 | // Setup browser environment so that we can test React components.
10 | global.document = jsdom.jsdom('');
11 | global.window = document.parentWindow;
12 | global.navigator = {
13 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36'
14 | };
15 | global.console.debug = function () {}; // NodeJS does not have console.debug, but React uses it.
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.5.3",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "target": "es5",
6 | "jsx": "react",
7 | "sourceMap": true,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "strictNullChecks": false,
11 | "outDir": "dist"
12 | },
13 | "exclude": [
14 | "dist",
15 | "node_modules"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./make-webpack-config")({
2 |
3 | });
--------------------------------------------------------------------------------