├── .babelrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── client ├── index.js └── styles.js ├── common ├── css │ ├── base │ │ ├── _fonts.scss │ │ ├── _overrides.scss │ │ ├── _styles.scss │ │ └── index.scss │ └── resources │ │ ├── _colors.scss │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── _vendors.scss ├── fonts │ └── .gitkeep ├── images │ └── favicon.png └── js │ ├── actions │ └── todos.js │ ├── components │ ├── common │ │ ├── Footer │ │ │ └── index.js │ │ ├── Header │ │ │ └── index.js │ │ ├── Loading │ │ │ └── index.js │ │ ├── RouteWithSubRoutes │ │ │ └── index.js │ │ └── index.js │ └── todos │ │ ├── TodoForm │ │ ├── index.js │ │ ├── index.scss │ │ └── spec │ │ │ ├── TodoForm.test.js │ │ │ └── __snapshots__ │ │ │ └── TodoForm.test.js.snap │ │ ├── TodoItem │ │ ├── index.js │ │ ├── index.scss │ │ └── spec │ │ │ ├── TodoItem.test.js │ │ │ └── __snapshots__ │ │ │ └── TodoItem.test.js.snap │ │ ├── TodoList │ │ ├── index.js │ │ ├── index.scss │ │ └── spec │ │ │ ├── TodoList.test.js │ │ │ └── __snapshots__ │ │ │ └── TodoList.test.js.snap │ │ └── index.js │ ├── constants │ └── index.js │ ├── containers │ ├── App │ │ └── index.js │ └── Todos │ │ ├── index.js │ │ └── index.scss │ ├── lib │ ├── api.js │ └── generateActionCreator.js │ ├── middleware │ └── .gitkeep │ ├── pages │ ├── Error │ │ └── index.js │ ├── Home │ │ ├── index.js │ │ └── index.scss │ └── Todos │ │ └── index.js │ ├── reducers │ ├── index.js │ └── todos.js │ ├── routes │ └── index.js │ └── store │ ├── index.dev.js │ ├── index.js │ └── index.prod.js ├── config └── index.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── postinstall.js ├── server ├── .node-dev.json ├── api │ ├── index.js │ └── todos │ │ ├── index.js │ │ ├── spec │ │ └── todos.controller.test.js │ │ ├── todos.controller.js │ │ └── todos.fixture.js ├── index.js ├── lib │ └── .gitkeep ├── middleware │ ├── httpsRedirect.js │ └── index.js ├── registerAliases.js ├── renderer │ ├── handler.js │ ├── index.js │ └── render.js ├── server.js └── templates │ └── layouts │ └── application.html ├── test └── support │ ├── jest.config.js │ ├── jest.globalSetup.js │ └── jest.setup.js └── webpack ├── babel.config.client.js ├── babel.config.ssr.js ├── base.js ├── development.client.babel.js ├── production.client.babel.js └── production.ssr.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current", 8 | "uglify": false 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | "transform-es2015-modules-commonjs", 15 | "transform-class-properties", 16 | "transform-export-extensions", 17 | "transform-object-rest-spread" 18 | ], 19 | "env": { 20 | "test": { 21 | "presets": [ 22 | "react" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server-side application settings 2 | # The main port to run the server on 3 | APPLICATION_PORT=3000 4 | 5 | # The absolute URL to the application. 6 | APPLICATION_BASE_URL=http://localhost:3000 7 | 8 | # The output path of server and client files built by webpack and babel. 9 | OUTPUT_PATH=dist 10 | PUBLIC_OUTPUT_PATH=dist/public 11 | 12 | # Settings for webpack-dev-server. 13 | DEV_SERVER_PORT=3001 14 | DEV_SERVER_HOSTNAME=localhost 15 | DEV_SERVER_HOST_URL=http://localhost:3001 16 | 17 | # The primary asset path. Can be changed to be a CDN URL. 18 | PUBLIC_ASSET_PATH=http://localhost:3001/assets/ 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/dist/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "max-len": [ 5 | "error", 6 | { "code": 80, "tabWidth": 2 } 7 | ], 8 | "indent": [ 9 | 1, 10 | 2, 11 | { "SwitchCase": 1 } 12 | ], 13 | "quotes": [ 14 | 2, 15 | "single" 16 | ], 17 | "react/jsx-uses-react": 1, 18 | "react/jsx-uses-vars": 1, 19 | "linebreak-style": [ 20 | 2, 21 | "unix" 22 | ], 23 | "no-console": 0, 24 | "no-unused-vars": [1], 25 | "semi": [ 26 | 2, 27 | "always" 28 | ] 29 | }, 30 | "env": { 31 | "browser": true, 32 | "es6": true, 33 | "jest": true, 34 | "node": true 35 | }, 36 | "globals": { 37 | "expect": true, 38 | "__non_webpack_require__": true 39 | }, 40 | "extends": [ 41 | "eslint:recommended", 42 | "plugin:react/recommended" 43 | ], 44 | "parserOptions": { 45 | "sourceType": "module", 46 | "ecmaFeatures": { 47 | "experimentalObjectRestSpread": true, 48 | "jsx": true 49 | }, 50 | "ecmaVersion": 6 51 | }, 52 | "plugins": [ 53 | "babel", 54 | "react" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-error.log 2 | .env 3 | node_modules 4 | npm-debug.log* 5 | .DS_Store 6 | 7 | # ignore built static files 8 | /dist 9 | /webpack-assets.json 10 | /webpack-stats.json 11 | /react-loadable.json 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal React Redux Boilerplate 2 | 3 | A universal React/Redux boilerplate with sensible defaults. Out of the box, this 4 | boilerplate comes with: 5 | 6 | - Server-side rendering with Express 7 | - Code splitting with [dynamic imports](https://webpack.js.org/guides/code-splitting/#dynamic-imports) and [react-loadable](https://github.com/thejameskyle/react-loadable) 8 | - Sane [webpack configurations](webpack/) 9 | - JS hot reloading with [react-hot-loader (@next)](https://github.com/gaearon/react-hot-loader) and [webpack-dev-server](https://github.com/webpack/webpack-dev-server) 10 | - CSS, SASS and [css-modules](https://github.com/css-modules/css-modules) support with hot reloading and no [flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) ([css-hot-loader](https://github.com/shepherdwind/css-hot-loader)) 11 | - Routing with [react-router-v4](https://github.com/ReactTraining/react-router) 12 | - Full production builds that do not rely on `babel-node`. 13 | - Pre-configured testing tools with `jest` and `enzyme` to work with css modules, static files, and aliased module paths. 14 | 15 | ## Philosophy 16 | 17 | The JavaScript ecosystem is brimming with open source libraries. With advances 18 | in ES6 and commitments by the big tech companies to invest in JavaScript, the 19 | last several years have arguably turned web development into what was once a 20 | huge pain in the ass, to a pretty decently enjoyable experience. 21 | 22 | With so many different packages now available, we now have the freedom and the 23 | choice to craft applications to our exact specifications, reducing bloat and 24 | minimizing the number of code we need to support cross-platform apps. It really 25 | is a new world. 26 | 27 | However, with so many different developers working on different libraries, 28 | things are constantly in flux, and breaking changes are often introduced. It can 29 | be hard to keep up with the latest and greatest since they're always changing. 30 | 31 | To help alleviate this, we've collected some of the best practices and features 32 | from the React ecosystem and put them in one place. Although this boilerplate is 33 | fully production-capable as is, its main goal is to serve as an example of how 34 | to bring an application together using the latest tools in the ecosystem. 35 | 36 | ## Development Mode 37 | 38 | Copy environment variables and edit them if necessary: 39 | 40 | ``` 41 | cp .env.example .env 42 | ``` 43 | 44 | Then: 45 | 46 | ``` 47 | npm install 48 | npm start 49 | ``` 50 | 51 | Direct your browser to `http://localhost:3000`. 52 | 53 | ## Production Builds 54 | 55 | Add environment variables the way you normally would on your production system. 56 | 57 | ``` 58 | npm run prod:build 59 | npm run serve 60 | ``` 61 | 62 | Or simply: 63 | 64 | ``` 65 | npm run prod 66 | ``` 67 | 68 | If using Heroku, simply add a `Procfile` in the root directory. The 69 | [postinstall](postinstall.js) script will do the rest. 70 | 71 | ``` 72 | web: npm run serve 73 | ``` 74 | 75 | ## Path Aliases 76 | 77 | In `package.json`, there is a property named `_moduleAliases`. This object 78 | defines the require() aliases used by both webpack and node. 79 | 80 | Aliased paths are prefixed with one of two symbols, which denote different 81 | things: 82 | 83 | `@` - component and template paths, e.g. `@components` 84 | 85 | `$` - server paths that are built by babel, e.g. `server/api` 86 | 87 | Aliases are nice to use for convenience, and lets us avoid using relative paths 88 | in our components: 89 | 90 | ``` 91 | // This sucks 92 | import SomeComponent from '../../../components/SomeComponent'; 93 | 94 | // This is way better 95 | import SomeComponent from '@components/SomeComponent'; 96 | ``` 97 | 98 | You can add additional aliases in `package.json` to your own liking. 99 | 100 | ## Environment Variables 101 | 102 | In development mode, environment variables are loaded by `dotenv` off the `.env` 103 | file in your root directory. In production, you'll have to manage these 104 | yourself. 105 | 106 | An example with Heroku: 107 | 108 | ``` 109 | heroku config:set FOO=bar 110 | ``` 111 | 112 | ## CSS Modules 113 | 114 | This project uses [CSS Modules](https://github.com/css-modules/css-modules). 115 | Class names should be in `camelCase`. Simply import the .scss file into your 116 | component, for example: 117 | 118 | ``` 119 | ├── components 120 | │   ├── Header.js 121 | │   ├── Header.scss 122 | ``` 123 | 124 | ``` 125 | // Header.scss 126 | .headerContainer { 127 | height: 100px; 128 | width: 100%; 129 | } 130 | ``` 131 | 132 | ``` 133 | // Header.js 134 | import css from './Header.scss'; 135 | 136 | const Header = (props) => { 137 | return ( 138 |
139 | {...} 140 |
141 | ); 142 | } 143 | 144 | ``` 145 | 146 | ## Redux Devtools 147 | 148 | This project supports the awesome [Redux Devtools Extension](https://github.com/zalmoxisus/redux-devtools-extension). 149 | Install the Chrome or Firefox extension and it should just work. 150 | 151 | ## Pre-fetching Data for Server Side Rendering (SSR) 152 | 153 | When rendering components on the server, you'll find that you may need to fetch 154 | some data before it can be rendered. The [component renderer](server/renderer/handler.js) 155 | looks for a `fetchData` method on the container component and its child 156 | components, then executes all of them and only renders after the promises have 157 | all been resolved. 158 | 159 | ``` 160 | // As an ES6 class 161 | 162 | class TodosContainer extends React.Component { 163 | static fetchData = ({ store }) => { 164 | return store.dispatch(fetchTodos()); 165 | }; 166 | } 167 | 168 | // As a functional stateless component 169 | 170 | const TodosContainer = (props) => { 171 | const { todos } = props; 172 | return ( 173 | // ...component code 174 | ); 175 | } 176 | 177 | TodosContainer.fetchData = ({ store }) => { 178 | return store.dispatch(fetchTodos()); 179 | } 180 | ``` 181 | 182 | ## Async / Await 183 | 184 | This project uses `async/await`, available by default in Node.js v8.x.x or 185 | higher. If you experience errors, please upgrade your version of Node.js. 186 | 187 | ## Testing 188 | 189 | The default testing framework is Jest, though you can use whatever you want. 190 | 191 | Tests and their corresponding files such as Jest snapshots, should be co-located 192 | alongside the modules they are testing, in a `spec/` folder. For example: 193 | 194 | ``` 195 | ├── components 196 | │   ├── todos 197 | │   │   ├── TodoForm 198 | │   │   │   ├── spec 199 | │   │   │   │   ├── TodoForm.test.js 200 | │   │   │   ├── index.js 201 | │   │   │   ├── index.scss 202 | ``` 203 | 204 | Tests can be written with ES2015, since it passes through `babel-register`. 205 | 206 | ## Running Tests 207 | 208 | To run a single test: 209 | 210 | ``` 211 | npm test /path/to/single.test.js 212 | 213 | // Or, to watch for changes 214 | npm run test:watch /path/to/single.test.js 215 | ``` 216 | 217 | To run all tests: 218 | 219 | ``` 220 | npm run test:all 221 | 222 | // Or, to watch for changes 223 | npm run test:all:watch 224 | ``` 225 | 226 | ## Running ESLint 227 | 228 | ``` 229 | npm run lint 230 | ``` 231 | 232 | Check the `.eslintignore` file for directories excluded from linting. 233 | 234 | ## Changing the public asset path 235 | 236 | By default, assets are built into `dist/public`. This path is then served by 237 | express under the path `assets`. This is the public asset path. In a production 238 | scenario, you may want your assets to be hosted on a CDN. To do so, just change 239 | the `PUBLIC_ASSET_PATH` environment variant. 240 | 241 | Example using Heroku, if serving via CDN: 242 | 243 | ``` 244 | heroku config:set PUBLIC_ASSET_PATH=https://my.cdn.com 245 | ``` 246 | 247 | Example using Heroku, if serving locally: 248 | 249 | ``` 250 | heroku config:set PUBLIC_ASSET_PATH=/assets 251 | ``` 252 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import './styles'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | import { ConnectedRouter } from 'react-router-redux'; 6 | import createHistory from 'history/createBrowserHistory'; 7 | import configureStore from '@store'; 8 | import App from '@containers/App'; 9 | import Loadable from 'react-loadable'; 10 | 11 | // Hydrate the redux store from server state. 12 | const initialState = window.__INITIAL_STATE__; 13 | const history = createHistory(); 14 | const store = configureStore(initialState, history); 15 | 16 | // Render the application 17 | window.main = () => { 18 | Loadable.preloadReady().then(() => { 19 | ReactDOM.hydrate( 20 | 21 | 22 | 23 | 24 | , 25 | document.getElementById('app') 26 | ); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /client/styles.js: -------------------------------------------------------------------------------- 1 | /* Style Loader 2 | * 3 | * Anything imported in here will either be added to the vendor CSS chunk, or 4 | * the main app CSS chunk. Where they will go depends on its location or its 5 | * extension. 6 | * 7 | * Files will be added to the vendor.css chunk if: 8 | * - they are located inside `node_modules`, or 9 | * - they are plain .css files. 10 | * Otherwise, files will be added to the main app.css chunk. 11 | */ 12 | 13 | // Pre-built Semantic-UI css. If you want to customize this, you can build your 14 | // own distribution of it and include it here. 15 | // See https: *semantic-ui.com/introduction/build-tools.html 16 | import 'semantic-ui-css/semantic.min.css'; 17 | 18 | // Include initial base styles. 19 | import '@css/base/index.scss'; 20 | -------------------------------------------------------------------------------- /common/css/base/_fonts.scss: -------------------------------------------------------------------------------- 1 | // Import fonts here, from google, or whereever. 2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600'); 3 | -------------------------------------------------------------------------------- /common/css/base/_overrides.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/css/base/_overrides.scss -------------------------------------------------------------------------------- /common/css/base/_styles.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | font-family: 'Open Sans', sans-serif; 4 | 5 | p { 6 | font-size: 16px; 7 | line-height: 24px; 8 | } 9 | } 10 | 11 | body { 12 | background-color: $white; 13 | } 14 | 15 | #app { 16 | height: 100%; 17 | width: 100%; 18 | margin: 0 auto; 19 | } 20 | -------------------------------------------------------------------------------- /common/css/base/index.scss: -------------------------------------------------------------------------------- 1 | // Resource files (.e.g. variables, mixins, etc) 2 | @import '../resources/variables'; 3 | @import '../resources/mixins'; 4 | 5 | // Add custom fonts, overrides, and base styles. 6 | @import 'fonts'; 7 | @import 'overrides'; 8 | @import 'styles'; 9 | -------------------------------------------------------------------------------- /common/css/resources/_colors.scss: -------------------------------------------------------------------------------- 1 | $white: #fff; 2 | $dark-gray: #333; 3 | -------------------------------------------------------------------------------- /common/css/resources/_mixins.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/css/resources/_mixins.scss -------------------------------------------------------------------------------- /common/css/resources/_variables.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/css/resources/_variables.scss -------------------------------------------------------------------------------- /common/css/resources/_vendors.scss: -------------------------------------------------------------------------------- 1 | /* Import or define any sass mixins, functions, and vendor mixins below */ 2 | 3 | @import "~include-media/dist/include-media"; 4 | -------------------------------------------------------------------------------- /common/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/fonts/.gitkeep -------------------------------------------------------------------------------- /common/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/images/favicon.png -------------------------------------------------------------------------------- /common/js/actions/todos.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TODO, 3 | REMOVE_TODO, 4 | TOGGLE_TODO, 5 | FETCH_TODOS_REQUEST, 6 | FETCH_TODOS_SUCCESS, 7 | FETCH_TODOS_FAILURE 8 | } from '@constants/index'; 9 | import api from '@lib/api'; 10 | import generateActionCreator from '@lib/generateActionCreator'; 11 | 12 | export const addTodo = generateActionCreator(ADD_TODO, 'text'); 13 | export const removeTodo = generateActionCreator(REMOVE_TODO, 'id'); 14 | export const toggleTodo = generateActionCreator(TOGGLE_TODO, 'id'); 15 | 16 | export const fetchTodosRequest = generateActionCreator(FETCH_TODOS_REQUEST); 17 | export const fetchTodosSuccess = generateActionCreator( 18 | FETCH_TODOS_SUCCESS, 19 | 'todos' 20 | ); 21 | export const fetchTodosFailure = generateActionCreator( 22 | FETCH_TODOS_FAILURE, 23 | 'error' 24 | ); 25 | 26 | export const fetchTodos = () => { 27 | return dispatch => { 28 | dispatch(fetchTodosRequest()); 29 | 30 | return api 31 | .get('/api/todos') 32 | .then(todos => { 33 | dispatch(fetchTodosSuccess(todos)); 34 | 35 | return Promise.resolve(todos); 36 | }) 37 | .catch(error => { 38 | dispatch(fetchTodosFailure(error)); 39 | 40 | return Promise.reject(error); 41 | }); 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /common/js/components/common/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Footer extends Component { 4 | render() { 5 | return ( 6 | 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /common/js/components/common/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { Header, Menu } from 'semantic-ui-react'; 4 | 5 | const menuItems = [ 6 | { name: 'Home', to: '/', exact: true }, 7 | { name: 'Todos', to: '/todos' } 8 | ]; 9 | 10 | class HeaderView extends Component { 11 | render() { 12 | return ( 13 |
14 | 15 | {menuItems.map(item => ( 16 | 17 | {item.name} 18 | 19 | ))} 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default HeaderView; 27 | -------------------------------------------------------------------------------- /common/js/components/common/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Loading = (props) => { 5 | const { error, timedOut, pastDelay } = props; 6 | 7 | if (error) { 8 | // When the loader has errored 9 | return ( 10 |
Error!
11 | ); 12 | } else if (timedOut) { 13 | // When the loader has taken longer than the timeout 14 | return ( 15 |
Taking a long time...
16 | ); 17 | } else if (pastDelay) { 18 | // When the loader has taken longer than the delay 19 | return ( 20 |
Loading...
21 | ); 22 | } 23 | 24 | return null; 25 | }; 26 | 27 | Loading.propTypes = { 28 | error: PropTypes.bool, 29 | timedOut: PropTypes.bool, 30 | pastDelay: PropTypes.bool 31 | }; 32 | 33 | export default Loading; 34 | -------------------------------------------------------------------------------- /common/js/components/common/RouteWithSubRoutes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | const RouteWithSubRoutes = props => { 5 | const { 6 | path, 7 | computedMatch, 8 | component: Component, 9 | routes, 10 | restProps 11 | } = props; 12 | 13 | return ( 14 | { 17 | // pass the sub-routes down to keep nesting 18 | return ( 19 | 25 | ); 26 | }} 27 | /> 28 | ); 29 | }; 30 | 31 | export default RouteWithSubRoutes; 32 | -------------------------------------------------------------------------------- /common/js/components/common/index.js: -------------------------------------------------------------------------------- 1 | export { default as Footer } from './Footer'; 2 | export { default as Header } from './Header'; 3 | export { default as Loading } from './Loading'; 4 | export { default as RouteWithSubRoutes } from './RouteWithSubRoutes'; 5 | -------------------------------------------------------------------------------- /common/js/components/todos/TodoForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form } from 'semantic-ui-react'; 4 | import classnames from 'classnames/bind'; 5 | import css from './index.scss'; 6 | 7 | class TodoForm extends Component { 8 | static propTypes = { 9 | onSubmit: PropTypes.func, 10 | className: PropTypes.string 11 | }; 12 | 13 | state = { todoText: '' }; 14 | 15 | submitTodo = ev => { 16 | ev.preventDefault(); 17 | 18 | const { onSubmit } = this.props; 19 | const { todoText } = this.state; 20 | 21 | if (todoText && todoText !== '' && typeof onSubmit === 'function') { 22 | onSubmit(todoText); 23 | this.setState({ todoText: '' }); 24 | } 25 | }; 26 | 27 | onTodoChange = ev => { 28 | this.setState({ todoText: ev.target.value }); 29 | }; 30 | 31 | render() { 32 | const { className } = this.props; 33 | const { todoText } = this.state; 34 | 35 | return ( 36 |
40 | 41 | 47 | 48 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | export default TodoForm; 55 | -------------------------------------------------------------------------------- /common/js/components/todos/TodoForm/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/combine/universal-react-redux/e13316c9eb88e1eb8b77059def52422b518c409b/common/js/components/todos/TodoForm/index.scss -------------------------------------------------------------------------------- /common/js/components/todos/TodoForm/spec/TodoForm.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoForm from '../index'; 3 | import { mount } from 'enzyme'; 4 | 5 | describe('TodoForm', () => { 6 | it('renders correctly', () => { 7 | const component = mount(); 8 | expect(component).toMatchSnapshot(); 9 | }); 10 | 11 | describe('clicking on submit button', () => { 12 | test('calls the onSubmit prop', () => { 13 | const mockSubmit = jest.fn(); 14 | const component = mount(); 15 | 16 | // test form submission 17 | component.setState({ todoText: 'Foobar' }); 18 | component.find('form').simulate('submit'); 19 | 20 | expect(mockSubmit.mock.calls.length).toEqual(1); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /common/js/components/todos/TodoForm/spec/__snapshots__/TodoForm.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TodoForm renders correctly 1`] = ` 4 | 5 |
10 | 14 | 15 |
18 | 26 | 33 |
36 | 42 |
45 | 52 |
53 | 54 |
55 |
56 |
57 | 63 | 68 |
71 |