├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .lgtm ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── app ├── .htaccess ├── .nginx.conf ├── app.js ├── components │ ├── A │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── AppBar │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── AutoComplete │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── Avatar │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── Badge │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── BottomNavigation │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── BottomNavigationItem │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── Card │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── CardActions │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── CardHeader │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── CardMedia │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── CardText │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ ├── CardTitle │ │ ├── index.js │ │ ├── tests │ │ │ └── index.test.js │ │ └── view.js │ └── H1 │ │ ├── index.js │ │ └── tests │ │ └── index.test.js ├── containers │ ├── App │ │ ├── constants.js │ │ ├── index.js │ │ ├── selectors.js │ │ ├── tests │ │ │ └── selectors.test.js │ │ └── view.js │ ├── HomePage │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ ├── selectors.js │ │ ├── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ ├── sagas.test.js │ │ │ └── selectors.test.js │ │ └── view.js │ ├── LanguageProvider │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ └── view.js │ └── NotFoundPage │ │ ├── index.js │ │ ├── messages.js │ │ └── view.js ├── favicon.ico ├── global-styles.js ├── i18n.js ├── index.html ├── manifest.json ├── reducers.js ├── routes.js ├── store.js ├── tests │ └── store.test.js ├── translations │ └── en.json └── utils │ ├── asyncInjectors.js │ └── tests │ └── asyncInjectors.test.js ├── appveyor.yml ├── docs ├── README.md ├── css │ ├── README.md │ ├── remove.md │ ├── sanitize.md │ ├── sass.md │ └── styled-componets.md ├── general │ ├── README.md │ ├── commands.md │ ├── deployment.md │ ├── faq.md │ ├── files.md │ ├── gotchas.md │ ├── remove.md │ ├── server-configs.md │ ├── webstorm-debug.png │ └── webstorm-eslint.png ├── js │ ├── README.md │ ├── i18n.md │ ├── immutablejs.md │ ├── redux-saga.md │ ├── redux.md │ ├── remove.md │ ├── reselect.md │ └── routing.md └── testing │ ├── README.md │ ├── component-testing.md │ ├── remote-testing.md │ └── unit-testing.md ├── internals ├── config.js ├── generators │ ├── component │ │ ├── es6.js.hbs │ │ ├── es6.pure.js.hbs │ │ ├── index.js │ │ ├── messages.js.hbs │ │ ├── test.js.hbs │ │ └── view.js.hbs │ ├── container │ │ ├── actions.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── constants.js.hbs │ │ ├── index.js │ │ ├── index.js.hbs │ │ ├── messages.js.hbs │ │ ├── reducer.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── sagas.js.hbs │ │ ├── sagas.test.js.hbs │ │ ├── selectors.js.hbs │ │ ├── selectors.test.js.hbs │ │ ├── test.js.hbs │ │ └── view.js.hbs │ ├── index.js │ ├── language │ │ ├── add-locale-data.hbs │ │ ├── app-locale.hbs │ │ ├── format-translation-messages.hbs │ │ ├── index.js │ │ ├── intl-locale-data.hbs │ │ ├── polyfill-intl-locale.hbs │ │ ├── translation-messages.hbs │ │ └── translations-json.hbs │ ├── route │ │ ├── index.js │ │ ├── route.hbs │ │ └── routeWithReducer.hbs │ └── utils │ │ └── componentExists.js ├── scripts │ ├── analyze.js │ ├── dependencies.js │ ├── extract-intl.js │ ├── helpers │ │ ├── checkmark.js │ │ └── progress.js │ ├── npmcheckversion.js │ └── pagespeed.js ├── testing │ ├── karma.conf.js │ └── test-bundler.js └── webpack │ ├── webpack.base.babel.js │ ├── webpack.dev.babel.js │ ├── webpack.dll.babel.js │ ├── webpack.prod.babel.js │ └── webpack.test.babel.js ├── package.json ├── server ├── index.js ├── logger.js └── middlewares │ └── frontendMiddleware.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "latest", 5 | { 6 | "es2015": { 7 | "modules": false 8 | } 9 | } 10 | ], 11 | "react", 12 | "stage-0" 13 | ], 14 | "env": { 15 | "production": { 16 | "only": [ 17 | "app" 18 | ], 19 | "plugins": [ 20 | "transform-react-remove-prop-types", 21 | "transform-react-constant-elements", 22 | "transform-react-inline-elements" 23 | ] 24 | }, 25 | "test": { 26 | "plugins": [ 27 | "istanbul" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "es6": true 9 | }, 10 | "plugins": [ 11 | "redux-saga", 12 | "react", 13 | "jsx-a11y" 14 | ], 15 | "parserOptions": { 16 | "ecmaVersion": 6, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "rules": { 23 | "arrow-parens": [ 24 | "error", 25 | "always" 26 | ], 27 | "arrow-body-style": [ 28 | 2, 29 | "as-needed" 30 | ], 31 | "comma-dangle": [ 32 | 2, 33 | "always-multiline" 34 | ], 35 | "import/imports-first": 0, 36 | "import/newline-after-import": 0, 37 | "import/no-dynamic-require": 0, 38 | "import/no-extraneous-dependencies": 0, 39 | "import/no-named-as-default": 0, 40 | "import/no-unresolved": 2, 41 | "import/prefer-default-export": 0, 42 | "indent": [ 43 | 2, 44 | 2, 45 | { 46 | "SwitchCase": 1 47 | } 48 | ], 49 | "jsx-a11y/aria-props": 2, 50 | "jsx-a11y/heading-has-content": 0, 51 | "jsx-a11y/href-no-hash": 2, 52 | "jsx-a11y/label-has-for": 2, 53 | "jsx-a11y/mouse-events-have-key-events": 2, 54 | "jsx-a11y/role-has-required-aria-props": 2, 55 | "jsx-a11y/role-supports-aria-props": 2, 56 | "max-len": 0, 57 | "newline-per-chained-call": 0, 58 | "no-console": 1, 59 | "no-use-before-define": 0, 60 | "prefer-template": 2, 61 | "class-methods-use-this": 0, 62 | "react/forbid-prop-types": 0, 63 | "react/jsx-first-prop-new-line": [ 64 | 2, 65 | "multiline" 66 | ], 67 | "react/jsx-filename-extension": 0, 68 | "react/jsx-no-target-blank": 0, 69 | "react/prefer-stateless-function": 0, 70 | "react/require-extension": 0, 71 | "react/self-closing-comp": 0, 72 | "redux-saga/no-yield-in-race": 2, 73 | "redux-saga/yield-effects": 2, 74 | "require-yield": 0 75 | }, 76 | "settings": { 77 | "import/resolver": { 78 | "webpack": { 79 | "config": "./internals/webpack/webpack.test.babel.js" 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | .gitconfig text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | 73 | # Heroku 74 | Procfile text 75 | .slugignore text 76 | 77 | # Documentation 78 | *.md text 79 | LICENSE text 80 | AUTHORS text 81 | 82 | 83 | # 84 | ## These files are binary and should be left untouched 85 | # 86 | 87 | # (binary is a macro for -text -diff) 88 | *.png binary 89 | *.jpg binary 90 | *.jpeg binary 91 | *.gif binary 92 | *.ico binary 93 | *.mov binary 94 | *.mp4 binary 95 | *.mp3 binary 96 | *.flv binary 97 | *.fla binary 98 | *.swf binary 99 | *.gz binary 100 | *.zip binary 101 | *.7z binary 102 | *.ttf binary 103 | *.eot binary 104 | *.woff binary 105 | *.pyc binary 106 | *.pdf binary 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /.lgtm: -------------------------------------------------------------------------------- 1 | approvals = 2 2 | pattern = "(?i):shipit:|LGTM" 3 | self_approval_off = true 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | - 5 6 | - 4 7 | 8 | script: npm run build 9 | 10 | install: 11 | - npm i -g npm@latest 12 | - npm install 13 | 14 | before_install: 15 | - export CHROME_BIN=chromium-browser 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | 19 | notifications: 20 | email: 21 | on_failure: change 22 | 23 | after_success: 'npm run coveralls' 24 | 25 | cache: 26 | directories: 27 | - node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Kelson Adams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start:prod 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React/Redux Boilerplate with Material-UI wrapper components 2 | 3 | This is my main react redux boilerplate with pre-built [material-ui](http://material-ui.com) wrapper components. 4 | 5 | ## Quick start 6 | 7 | 1. Clone this repo using `git clone https://github.com/kelsonic/react-redux-material-ui-boilerplate.git` 8 | 2. Run either `npm install` or `yarn` (**We auto-detect `yarn` on your machine). 9 | 3. Start your development server by running `npm start`. Your server should be running on `localhost: 3000`. 10 | 11 | ## Documentation 12 | 13 | - [Intro](docs/general): What's included and why 14 | - [**Commands**](docs/general/commands.md): Getting the most out of this boilerplate 15 | - [Testing](docs/testing): How to work with the built-in test harness 16 | - [Styling](docs/css): How to work with the CSS tooling 17 | - [Your app](docs/js): Supercharging your app with Routing, Redux, simple 18 | asynchronicity helpers, etc. 19 | 20 | ## Another React-Redux boilerplate? 21 | 22 | A large inspiration came from Max's [React-Redux boilerplate](https://github.com/mxstbr/react-boilerplate), which has been battle-tested and is production-ready. 23 | 24 | *So why create another React-Redux boilerplate?* A few reasons: 25 | 26 | - I needed a separate `view.js` for each component/container. 27 | - [PureComponents are preferred (ie. optimized) over stateless components](https://medium.com/modus-create-front-end-development/component-rendering-performance-in-react-df859b474adc#.8sz7sopg0). 28 | - I needed material-ui wrapper components that could be easily used in containers. 29 | 30 | There are more reasons, though I figured introducing pre-built material-ui wrapper components was reason enough to open-source this boilerplate. 31 | 32 | ## License 33 | 34 | [MIT License](LICENSE). 35 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/.nginx.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have line 'include /etc/nginx/conf.d/*.conf;' 4 | # in your main nginx configuration file 5 | ## 6 | 7 | ## 8 | # Redirect to the same URL with https:// 9 | ## 10 | 11 | server { 12 | 13 | listen 80; 14 | 15 | # Type your domain name below 16 | server_name example.com; 17 | 18 | return 301 https://$server_name$request_uri; 19 | 20 | } 21 | 22 | ## 23 | # HTTPS configurations 24 | ## 25 | 26 | server { 27 | 28 | listen 443; 29 | 30 | # Type your domain name below 31 | server_name example.com; 32 | 33 | ssl on; 34 | ssl_certificate /path/to/certificate.crt; 35 | ssl_certificate_key /path/to/server.key; 36 | 37 | # Use only TSL protocols for more secure 38 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 39 | 40 | # Always serve index.html for any request 41 | location / { 42 | # Set path 43 | root /var/www/; 44 | try_files $uri /index.html; 45 | } 46 | 47 | ## 48 | # If you want to use Node/Rails/etc. API server 49 | # on the same port (443) config Nginx as a reverse proxy. 50 | # For security reasons use a firewall like ufw in Ubuntu 51 | # and deny port 3000/tcp. 52 | ## 53 | 54 | # location /api/ { 55 | # 56 | # proxy_pass http://localhost:3000; 57 | # proxy_http_version 1.1; 58 | # proxy_set_header X-Forwarded-Proto https; 59 | # proxy_set_header Upgrade $http_upgrade; 60 | # proxy_set_header Connection 'upgrade'; 61 | # proxy_set_header Host $host; 62 | # proxy_cache_bypass $http_upgrade; 63 | # 64 | # } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * This is the entry file for the application, only setup and boilerplate 5 | * code. 6 | */ 7 | import 'babel-polyfill'; 8 | 9 | /* eslint-disable import/no-unresolved, import/extensions */ 10 | // Load the manifest.json file and the .htaccess file 11 | import '!file?name=[name].[ext]!./manifest.json'; 12 | import 'file?name=[name].[ext]!./.htaccess'; 13 | /* eslint-enable import/no-unresolved, import/extensions */ 14 | 15 | // Import all the third party stuff 16 | import React from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import { Provider } from 'react-redux'; 19 | import { applyRouterMiddleware, Router, browserHistory } from 'react-router'; 20 | import { syncHistoryWithStore } from 'react-router-redux'; 21 | import { useScroll } from 'react-router-scroll'; 22 | import LanguageProvider from 'containers/LanguageProvider'; 23 | import configureStore from './store'; 24 | 25 | // Import i18n messages 26 | import { translationMessages } from './i18n'; 27 | 28 | // Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder 29 | import 'sanitize.css/sanitize.css'; 30 | 31 | // Needed for onTouchTap 32 | // http://stackoverflow.com/a/34015469/988941 33 | import injectTapEventPlugin from 'react-tap-event-plugin'; 34 | injectTapEventPlugin(); 35 | 36 | // Create redux store with history 37 | // this uses the singleton browserHistory provided by react-router 38 | // Optionally, this could be changed to leverage a created history 39 | // e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` 40 | const initialState = {}; 41 | const store = configureStore(initialState, browserHistory); 42 | 43 | // Sync history and store, as the react-router-redux reducer 44 | // is under the non-default key ("routing"), selectLocationState 45 | // must be provided for resolving how to retrieve the "route" in the state 46 | import { selectLocationState } from 'containers/App/selectors'; 47 | const history = syncHistoryWithStore(browserHistory, store, { 48 | selectLocationState: selectLocationState(), 49 | }); 50 | 51 | // Set up the router, wrapping all Routes in the App component 52 | import App from 'containers/App'; 53 | import createRoutes from './routes'; 54 | const rootRoute = { 55 | component: App, 56 | childRoutes: createRoutes(store), 57 | }; 58 | 59 | 60 | const render = (translatedMessages) => { 61 | ReactDOM.render( 62 | 63 | 64 | 73 | 74 | , 75 | document.getElementById('app') 76 | ); 77 | }; 78 | 79 | 80 | // Hot reloadable translation json files 81 | if (module.hot) { 82 | // modules.hot.accept does not accept dynamic dependencies, 83 | // have to be constants at compile-time 84 | module.hot.accept('./i18n', () => { 85 | render(translationMessages); 86 | }); 87 | } 88 | 89 | // Chunked polyfill for browsers without Intl support 90 | if (!window.Intl) { 91 | (new Promise((resolve) => { 92 | resolve(System.import('intl')); 93 | })) 94 | .then(() => Promise.all([ 95 | System.import('intl/locale-data/jsonp/de.js'), 96 | ])) 97 | .then(() => render(translationMessages)) 98 | .catch((err) => { 99 | throw err; 100 | }); 101 | } else { 102 | render(translationMessages); 103 | } 104 | 105 | // Install ServiceWorker and AppCache in the end since 106 | // it's not most important operation and if main code fails, 107 | // we do not want it installed 108 | import { install } from 'offline-plugin/runtime'; 109 | install(); 110 | -------------------------------------------------------------------------------- /app/components/A/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A link to a certain page, an anchor tag 3 | */ 4 | 5 | import styled from 'styled-components'; 6 | 7 | const A = styled.a` 8 | color: #41addd; 9 | 10 | &:hover { 11 | color: #6cc0e5; 12 | } 13 | `; 14 | 15 | export default A; 16 | -------------------------------------------------------------------------------- /app/components/A/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing our link component 3 | */ 4 | 5 | import A from '../index'; 6 | 7 | import expect from 'expect'; 8 | import { shallow } from 'enzyme'; 9 | import React from 'react'; 10 | 11 | const href = 'http://mxstbr.com/'; 12 | const children = (

Test

); 13 | const renderComponent = (props = {}) => shallow( 14 | 15 | {children} 16 | 17 | ); 18 | 19 | describe('', () => { 20 | it('should render an tag', () => { 21 | const renderedComponent = renderComponent(); 22 | expect(renderedComponent.type()).toEqual('a'); 23 | }); 24 | 25 | it('should have an href attribute', () => { 26 | const renderedComponent = renderComponent(); 27 | expect(renderedComponent.prop('href')).toEqual(href); 28 | }); 29 | 30 | it('should have children', () => { 31 | const renderedComponent = renderComponent(); 32 | expect(renderedComponent.contains(children)).toEqual(true); 33 | }); 34 | 35 | it('should have a className attribute', () => { 36 | const className = 'test'; 37 | const renderedComponent = renderComponent({ className }); 38 | expect(renderedComponent.find('a').hasClass(className)).toEqual(true); 39 | }); 40 | 41 | it('should adopt a target attribute', () => { 42 | const target = '_blank'; 43 | const renderedComponent = renderComponent({ target }); 44 | expect(renderedComponent.prop('target')).toEqual(target); 45 | }); 46 | 47 | it('should adopt a type attribute', () => { 48 | const type = 'text/html'; 49 | const renderedComponent = renderComponent({ type }); 50 | expect(renderedComponent.prop('type')).toEqual(type); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /app/components/AppBar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AppBar 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class AppBar extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | className: PropTypes.string, 14 | iconClassNameLeft: PropTypes.string, 15 | iconClassNameRight: PropTypes.string, 16 | iconElementLeft: PropTypes.node, 17 | iconElementRight: PropTypes.node, 18 | iconStyleLeft: PropTypes.object, 19 | iconStyleRight: PropTypes.object, 20 | onLeftIconButtonTouchTap: PropTypes.func, 21 | onRightIconButtonTouchTap: PropTypes.func, 22 | onTitleTouchTap: PropTypes.func, 23 | showMenuIconButton: PropTypes.bool, 24 | style: PropTypes.object, 25 | title: PropTypes.node, 26 | titleStyle: PropTypes.object, 27 | zDepth: PropTypes.number, 28 | }; 29 | 30 | static defaultProps = { 31 | children: undefined, 32 | className: undefined, 33 | iconClassNameLeft: undefined, 34 | iconClassNameRight: undefined, 35 | iconElementLeft: undefined, 36 | iconElementRight: undefined, 37 | iconStyleLeft: undefined, 38 | iconStyleRight: undefined, 39 | onLeftIconButtonTouchTap: () => {}, 40 | onRightIconButtonTouchTap: () => {}, 41 | onTitleTouchTap: () => {}, 42 | showMenuIconButton: true, 43 | style: undefined, 44 | title: '', 45 | titleStyle: undefined, 46 | zDepth: 1, 47 | }; 48 | } 49 | 50 | AppBar.prototype.render = view; 51 | 52 | export default AppBar; 53 | -------------------------------------------------------------------------------- /app/components/AppBar/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import AppBar from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/AppBar/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AppBar view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import AppBar from 'material-ui/AppBar'; 9 | 10 | export default function render() { 11 | return ( 12 | 29 | {this.props.children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/components/AutoComplete/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AutoComplete 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class AutoComplete extends PureComponent { 11 | static propTypes = { 12 | anchorOrigin: PropTypes.object, 13 | animated: PropTypes.bool, 14 | animation: PropTypes.func, 15 | dataSource: PropTypes.array, 16 | dataSourceConfig: PropTypes.object, 17 | disableFocusRipple: PropTypes.bool, 18 | errorStyle: PropTypes.object, 19 | errorText: PropTypes.node, 20 | filter: PropTypes.func, 21 | floatingLabelText: PropTypes.node, 22 | fullWidth: PropTypes.bool, 23 | hintText: PropTypes.node, 24 | listStyle: PropTypes.object, 25 | maxSearchResults: PropTypes.number, 26 | menuCloseDelay: PropTypes.number, 27 | menuProps: PropTypes.object, 28 | menuStyle: PropTypes.object, 29 | onClose: PropTypes.func, 30 | onNewRequest: PropTypes.func, 31 | onUpdateInput: PropTypes.func, 32 | open: PropTypes.bool, 33 | openOnFocus: PropTypes.bool, 34 | popoverProps: PropTypes.object, 35 | searchText: PropTypes.string, 36 | style: PropTypes.object, 37 | targetOrigin: PropTypes.object, 38 | textFieldStyle: PropTypes.object, 39 | }; 40 | 41 | static defaultProps = { 42 | anchorOrigin: { 43 | vertical: 'bottom', 44 | horizontal: 'left', 45 | }, 46 | animated: true, 47 | animation: () => {}, 48 | dataSource: undefined, 49 | dataSourceConfig: { 50 | text: 'text', 51 | value: 'value', 52 | }, 53 | disableFocusRipple: true, 54 | errorStyle: undefined, 55 | errorText: undefined, 56 | filter: (searchText, key) => ( 57 | searchText !== '' && key.indexOf(searchText) !== -1 58 | ), 59 | floatingLabelText: undefined, 60 | fullWidth: false, 61 | hintText: undefined, 62 | listStyle: undefined, 63 | maxSearchResults: undefined, 64 | menuCloseDelay: 300, 65 | menuProps: undefined, 66 | menuStyle: undefined, 67 | onClose: () => {}, 68 | onNewRequest: () => {}, 69 | onUpdateInput: () => {}, 70 | open: false, 71 | openOnFocus: false, 72 | popoverProps: undefined, 73 | searchText: '', 74 | style: undefined, 75 | targetOrigin: { 76 | vertical: 'top', 77 | horizontal: 'left', 78 | }, 79 | textFieldStyle: undefined, 80 | }; 81 | } 82 | 83 | AutoComplete.prototype.render = view; 84 | 85 | export default AutoComplete; 86 | -------------------------------------------------------------------------------- /app/components/AutoComplete/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import AutoComplete from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/AutoComplete/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AutoComplete view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import AutoComplete from 'material-ui/AutoComplete'; 9 | 10 | export default function render() { 11 | return ( 12 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Avatar 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class Avatar extends PureComponent { 11 | static propTypes = { 12 | backgroundColor: PropTypes.string, 13 | children: PropTypes.node, 14 | className: PropTypes.string, 15 | color: PropTypes.string, 16 | icon: PropTypes.node, 17 | size: PropTypes.number, 18 | src: PropTypes.string, 19 | style: PropTypes.object, 20 | }; 21 | 22 | static defaultProps = { 23 | backgroundColor: undefined, 24 | children: undefined, 25 | className: undefined, 26 | color: undefined, 27 | icon: undefined, 28 | size: 40, 29 | src: undefined, 30 | style: undefined, 31 | }; 32 | } 33 | 34 | Avatar.prototype.render = view; 35 | 36 | export default Avatar; 37 | -------------------------------------------------------------------------------- /app/components/Avatar/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Avatar from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/Avatar/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Avatar view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import Avatar from 'material-ui/Avatar'; 9 | 10 | export default function render() { 11 | return ( 12 | 21 | {this.props.children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/Badge/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Badge 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class Badge extends PureComponent { 11 | static propTypes = { 12 | badgeContent: PropTypes.node, 13 | badgeStyle: PropTypes.object, 14 | children: PropTypes.node, 15 | className: PropTypes.string, 16 | primary: PropTypes.bool, 17 | secondary: PropTypes.bool, 18 | style: PropTypes.object, 19 | }; 20 | 21 | static defaultProps = { 22 | badgeContent: undefined, 23 | badgeStyle: undefined, 24 | children: undefined, 25 | className: undefined, 26 | primary: false, 27 | secondary: false, 28 | style: undefined, 29 | }; 30 | } 31 | 32 | Badge.prototype.render = view; 33 | 34 | export default Badge; 35 | -------------------------------------------------------------------------------- /app/components/Badge/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Badge from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/Badge/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Badge view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import Badge from 'material-ui/Badge'; 9 | 10 | export default function render() { 11 | return ( 12 | 20 | {this.props.children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/components/BottomNavigation/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigation 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class BottomNavigation extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | selectedIndex: PropTypes.number, 14 | }; 15 | 16 | static defaultProps = { 17 | children: undefined, 18 | selectedIndex: undefined, 19 | }; 20 | } 21 | 22 | BottomNavigation.prototype.render = view; 23 | 24 | export default BottomNavigation; 25 | -------------------------------------------------------------------------------- /app/components/BottomNavigation/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import BottomNavigation from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/BottomNavigation/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigation view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { BottomNavigation } from 'material-ui/BottomNavigation'; 9 | 10 | export default function render() { 11 | return ( 12 | 15 | {this.props.children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/components/BottomNavigationItem/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigationItem 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class BottomNavigationItem extends PureComponent { 11 | static propTypes = { 12 | label: PropTypes.string, 13 | icon: PropTypes.node, 14 | onTouchTap: PropTypes.func, 15 | }; 16 | 17 | static defaultProps = { 18 | label: undefined, 19 | icon: undefined, 20 | onTouchTap: () => {}, 21 | }; 22 | } 23 | 24 | BottomNavigationItem.prototype.render = view; 25 | 26 | export default BottomNavigationItem; 27 | -------------------------------------------------------------------------------- /app/components/BottomNavigationItem/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import BottomNavigationItem from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/BottomNavigationItem/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigationItem view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { BottomNavigationItem } from 'material-ui/BottomNavigation'; 9 | 10 | export default function render() { 11 | return ( 12 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/components/Card/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Card 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class Card extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | containerStyle: PropTypes.object, 14 | expandable: PropTypes.bool, 15 | expanded: PropTypes.bool, 16 | initiallyExpanded: PropTypes.bool, 17 | onExpandChange: PropTypes.function, 18 | showExpandableButton: PropTypes.bool, 19 | style: PropTypes.object, 20 | }; 21 | 22 | static defaultProps = { 23 | children: undefined, 24 | containerStyle: undefined, 25 | expandable: false, 26 | expanded: null, 27 | initiallyExpanded: false, 28 | onExpandChange: undefined, 29 | showExpandableButton: undefined, 30 | style: undefined, 31 | }; 32 | } 33 | 34 | Card.prototype.render = view; 35 | 36 | export default Card; 37 | -------------------------------------------------------------------------------- /app/components/Card/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Card from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/Card/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Card view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Card } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 21 | {this.props.children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/CardActions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardActions 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardActions extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | expandable: PropTypes.bool, 15 | showExpandableButton: PropTypes.bool, 16 | style: PropTypes.object, 17 | }; 18 | 19 | static defaultProps = { 20 | actAsExpander: undefined, 21 | children: undefined, 22 | expandable: undefined, 23 | showExpandableButton: undefined, 24 | style: undefined, 25 | }; 26 | } 27 | 28 | CardActions.prototype.render = view; 29 | 30 | export default CardActions; 31 | -------------------------------------------------------------------------------- /app/components/CardActions/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardActions from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardActions/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardActions view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardActions } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 18 | {this.props.children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/CardHeader/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardHeader 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardHeader extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | avatar: PropTypes.node, 14 | children: PropTypes.node, 15 | closeIcon: PropTypes.node, 16 | expandable: PropTypes.bool, 17 | openIcon: PropTypes.node, 18 | showExpandableButton: PropTypes.bool, 19 | style: PropTypes.object, 20 | subtitle: PropTypes.node, 21 | subtitleColor: PropTypes.string, 22 | subtitleStyle: PropTypes.object, 23 | textStyle: PropTypes.object, 24 | title: PropTypes.node, 25 | titleColor: PropTypes.string, 26 | titleStyle: PropTypes.object, 27 | }; 28 | 29 | static defaultProps = { 30 | actAsExpander: undefined, 31 | avatar: null, 32 | children: undefined, 33 | closeIcon: undefined, 34 | expandable: undefined, 35 | openIcon: undefined, 36 | showExpandableButton: undefined, 37 | style: undefined, 38 | subtitle: undefined, 39 | subtitleColor: undefined, 40 | subtitleStyle: undefined, 41 | textStyle: undefined, 42 | title: undefined, 43 | titleColor: undefined, 44 | titleStyle: undefined, 45 | }; 46 | } 47 | 48 | CardHeader.prototype.render = view; 49 | 50 | export default CardHeader; 51 | -------------------------------------------------------------------------------- /app/components/CardHeader/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardHeader from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardHeader/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardHeader view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardHeader } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/components/CardMedia/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardMedia 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardMedia extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | expandable: PropTypes.bool, 15 | mediaStyle: PropTypes.object, 16 | overlay: PropTypes.node, 17 | overlayContainerStyle: PropTypes.object, 18 | overlayContentStyle: PropTypes.object, 19 | overlayStyle: PropTypes.object, 20 | style: PropTypes.object, 21 | }; 22 | 23 | static defaultProps = { 24 | actAsExpander: undefined, 25 | children: undefined, 26 | expandable: undefined, 27 | mediaStyle: undefined, 28 | overlay: undefined, 29 | overlayContainerStyle: undefined, 30 | overlayContentStyle: undefined, 31 | overlayStyle: undefined, 32 | style: undefined, 33 | }; 34 | } 35 | 36 | CardMedia.prototype.render = view; 37 | 38 | export default CardMedia; 39 | -------------------------------------------------------------------------------- /app/components/CardMedia/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardMedia from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardMedia/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardMedia view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardMedia } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 22 | {this.props.children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/components/CardText/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardText 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardText extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | color: PropTypes.string, 15 | expandable: PropTypes.bool, 16 | style: PropTypes.object, 17 | }; 18 | 19 | static defaultProps = { 20 | actAsExpander: undefined, 21 | children: undefined, 22 | color: undefined, 23 | expandable: undefined, 24 | style: undefined, 25 | }; 26 | } 27 | 28 | CardText.prototype.render = view; 29 | 30 | export default CardText; 31 | -------------------------------------------------------------------------------- /app/components/CardText/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardText from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardText/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardText view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardText } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 18 | {this.props.children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/CardTitle/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardTitle 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardTitle extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | expandable: PropTypes.bool, 15 | showExpandableButton: PropTypes.bool, 16 | style: PropTypes.object, 17 | subtitle: PropTypes.node, 18 | subtitleColor: PropTypes.string, 19 | subtitleStyle: PropTypes.object, 20 | title: PropTypes.node, 21 | titleColor: PropTypes.string, 22 | titleStyle: PropTypes.object, 23 | }; 24 | 25 | static defaultProps = { 26 | actAsExpander: undefined, 27 | children: undefined, 28 | expandable: undefined, 29 | showExpandableButton: undefined, 30 | style: undefined, 31 | subtitle: undefined, 32 | subtitleColor: undefined, 33 | subtitleStyle: undefined, 34 | title: undefined, 35 | titleColor: undefined, 36 | titleStyle: undefined, 37 | }; 38 | } 39 | 40 | CardTitle.prototype.render = view; 41 | 42 | export default CardTitle; 43 | -------------------------------------------------------------------------------- /app/components/CardTitle/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardTitle from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardTitle/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardTitle view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardTitle } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 24 | {this.props.children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/components/H1/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const H1 = styled.h1` 4 | font-size: 2em; 5 | margin-bottom: 0.25em; 6 | `; 7 | 8 | export default H1; 9 | -------------------------------------------------------------------------------- /app/components/H1/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import H1 from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('

', () => { 8 | it('should render a prop', () => { 9 | const id = 'testId'; 10 | const renderedComponent = shallow( 11 |

12 | ); 13 | expect(renderedComponent.prop('id')).toEqual(id); 14 | }); 15 | 16 | it('should render its text', () => { 17 | const children = 'Text'; 18 | const renderedComponent = shallow( 19 |

{children}

20 | ); 21 | expect(renderedComponent.contains(children)).toEqual(true); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /app/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 3 | */ 4 | 5 | export const DEFAULT_LOCALE = 'en'; 6 | -------------------------------------------------------------------------------- /app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | */ 6 | 7 | import { PureComponent, PropTypes } from 'react'; 8 | import view from './view'; 9 | 10 | export default class App extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | }; 14 | } 15 | 16 | App.prototype.render = view; 17 | -------------------------------------------------------------------------------- /app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | // selectLocationState expects a plain JS object for the routing state 2 | const selectLocationState = () => { 3 | let prevRoutingState; 4 | let prevRoutingStateJS; 5 | 6 | return (state) => { 7 | const routingState = state.get('route'); // or state.route 8 | 9 | if (!routingState.equals(prevRoutingState)) { 10 | prevRoutingState = routingState; 11 | prevRoutingStateJS = routingState.toJS(); 12 | } 13 | 14 | return prevRoutingStateJS; 15 | }; 16 | }; 17 | 18 | export { 19 | selectLocationState, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/App/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import expect from 'expect'; 3 | 4 | import { selectLocationState } from 'containers/App/selectors'; 5 | 6 | describe('selectLocationState', () => { 7 | it('should select the route as a plain JS object', () => { 8 | const route = fromJS({ 9 | locationBeforeTransitions: null, 10 | }); 11 | const mockedState = fromJS({ 12 | route, 13 | }); 14 | expect(selectLocationState()(mockedState)).toEqual(route.toJS()); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/containers/App/view.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | // Refer to http://www.material-ui.com/#/get-started/server-rendering for information on material-ui's Mui. 3 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | import { 6 | green100, 7 | green500, 8 | green700, 9 | } from 'material-ui/styles/colors'; 10 | 11 | const muiTheme = getMuiTheme({ 12 | palette: { 13 | primary1Color: green500, 14 | primary2Color: green700, 15 | primary3Color: green100, 16 | }, 17 | }); 18 | 19 | export default function render() { 20 | return ( 21 | 22 |
23 | {Children.toArray(this.props.children)} 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/containers/HomePage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /app/containers/HomePage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/Homepage/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /app/containers/HomePage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage 4 | * 5 | */ 6 | 7 | import { PureComponent } from 'react'; 8 | import { connect } from 'react-redux'; 9 | import view from './view'; 10 | import selectHomepage from './selectors'; 11 | 12 | class Homepage extends PureComponent {} 13 | 14 | Homepage.prototype.render = view; 15 | 16 | const mapStateToProps = selectHomepage(); 17 | 18 | function mapDispatchToProps(dispatch) { 19 | return { 20 | dispatch, 21 | }; 22 | } 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(Homepage); 25 | -------------------------------------------------------------------------------- /app/containers/HomePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Homepage Messages 3 | * 4 | * This contains all the text for the Homepage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.Homepage.header', 11 | defaultMessage: 'This is Homepage container! (This is coming from Homepage/messages.js)', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/HomePage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function homepageReducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default homepageReducer; 24 | -------------------------------------------------------------------------------- /app/containers/HomePage/sagas.js: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the homepage state domain 5 | */ 6 | const selectHomepageDomain = () => (state) => state.get('homepage'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by Homepage 15 | */ 16 | 17 | const selectHomepage = () => createSelector( 18 | selectHomepageDomain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default selectHomepage; 23 | export { 24 | selectHomepageDomain, 25 | }; 26 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('Homepage actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import { Homepage } from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import homepageReducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('homepageReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(homepageReducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/sagas.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { selectHomepageDomain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = selectHomepageDomain(); 6 | 7 | describe('selectHomepageDomain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/view.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import Helmet from 'react-helmet'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import messages from './messages'; 11 | // Extra material-ui components (don't necessitate wrappers) 12 | import FileFolder from 'material-ui/svg-icons/file/folder'; 13 | // Material-UI wrapper components 14 | import AppBar from 'components/AppBar'; 15 | import AutoComplete from 'components/AutoComplete'; 16 | import Avatar from 'components/Avatar'; 17 | import Badge from 'components/Badge'; 18 | // Other Components 19 | import A from 'components/A'; 20 | import H1 from 'components/H1'; 21 | 22 | export default function render() { 23 | return ( 24 |
25 | 31 | 32 | 33 |

AppBar

34 | 38 | 39 |

AutoComplete

40 | 44 | 45 |

Avatar

46 | } 48 | color={'#999'} 49 | backgroundColor={'#e9e9e9'} 50 | size={30} 51 | /> 52 | 53 |

Badge

54 | 58 | Company Name 59 | 60 | 61 |

BottomNavigation (TODO: example)

62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { 8 | CHANGE_LOCALE, 9 | } from './constants'; 10 | 11 | export function changeLocale(languageLocale) { 12 | return { 13 | type: CHANGE_LOCALE, 14 | locale: languageLocale, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | export const DEFAULT_LOCALE = 'en'; 9 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import { PureComponent, PropTypes } from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { selectLocale } from './selectors'; 13 | import view from './view'; 14 | 15 | class LanguageProvider extends PureComponent { 16 | static propTypes = { 17 | locale: PropTypes.string, 18 | messages: PropTypes.object, 19 | children: PropTypes.element.isRequired, 20 | }; 21 | } 22 | 23 | LanguageProvider.prototype.render = view; 24 | 25 | const mapStateToProps = createSelector( 26 | selectLocale(), 27 | (locale) => ({ locale }) 28 | ); 29 | 30 | function mapDispatchToProps(dispatch) { 31 | return { 32 | dispatch, 33 | }; 34 | } 35 | 36 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); 37 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | CHANGE_LOCALE, 10 | } from './constants'; 11 | import { 12 | DEFAULT_LOCALE, 13 | } from '../App/constants'; 14 | 15 | const initialState = fromJS({ 16 | locale: DEFAULT_LOCALE, 17 | }); 18 | 19 | function languageProviderReducer(state = initialState, action) { 20 | switch (action.type) { 21 | case CHANGE_LOCALE: 22 | return state 23 | .set('locale', action.locale); 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export default languageProviderReducer; 30 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the languageToggle state domain 5 | */ 6 | const selectLanguage = () => (state) => state.get('language'); 7 | 8 | /** 9 | * Select the language locale 10 | */ 11 | const selectLocale = () => createSelector( 12 | selectLanguage(), 13 | (languageState) => languageState.get('locale') 14 | ); 15 | 16 | export { 17 | selectLanguage, 18 | selectLocale, 19 | }; 20 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/view.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | import { IntlProvider } from 'react-intl'; 3 | 4 | export default function render() { 5 | return ( 6 | 11 | {Children.only(this.props.children)} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | */ 6 | 7 | import { PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | export default class NotFound extends PureComponent {} 11 | 12 | NotFound.prototype.render = view; 13 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | */ 4 | 5 | import { defineMessages } from 'react-intl'; 6 | 7 | export default defineMessages({ 8 | header: { 9 | id: 'app.components.NotFoundPage.header', 10 | defaultMessage: 'This is NotFoundPage component !', 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import messages from './messages'; 4 | 5 | export default function render() { 6 | return ( 7 |

8 | 9 |

10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelsonic/react-redux-material-ui-boilerplate/82d226c8f375f39d006d537213483ca9dba68bff/app/favicon.ico -------------------------------------------------------------------------------- /app/global-styles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components'; 2 | 3 | /* eslint no-unused-expressions: 0 */ 4 | injectGlobal` 5 | html, 6 | body { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 13 | } 14 | 15 | body.fontLoaded { 16 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #app { 20 | background-color: #fafafa; 21 | min-height: 100%; 22 | min-width: 100%; 23 | } 24 | 25 | p, 26 | label { 27 | font-family: Georgia, Times, 'Times New Roman', serif; 28 | line-height: 1.5em; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl'; 8 | import { DEFAULT_LOCALE } from './containers/App/constants'; // eslint-disable-line 9 | 10 | import enLocaleData from 'react-intl/locale-data/en'; 11 | 12 | export const appLocales = [ 13 | 'en', 14 | ]; 15 | 16 | import enTranslationMessages from './translations/en.json'; 17 | 18 | addLocaleData(enLocaleData); 19 | 20 | export const formatTranslationMessages = (locale, messages) => { 21 | const defaultFormattedMessages = locale !== DEFAULT_LOCALE ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) : {}; 22 | const formattedMessages = {}; 23 | const messageKeys = Object.keys(messages); 24 | for (const messageKey of messageKeys) { 25 | if (locale === DEFAULT_LOCALE) { 26 | formattedMessages[messageKey] = messages[messageKey]; 27 | } else { 28 | formattedMessages[messageKey] = messages[messageKey] || defaultFormattedMessages[messageKey]; 29 | } 30 | } 31 | 32 | return formattedMessages; 33 | }; 34 | 35 | export const translationMessages = { 36 | en: formatTranslationMessages('en', enTranslationMessages), 37 | }; 38 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React.js Boilerplate 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Boilerplate", 3 | "icons": [ 4 | { 5 | "src": "favicon.png", 6 | "sizes": "48x48", 7 | "type": "image/png", 8 | "density": 1.0 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "sizes": "96x96", 13 | "type": "image/png", 14 | "density": 2.0 15 | }, 16 | { 17 | "src": "favicon.png", 18 | "sizes": "144x144", 19 | "type": "image/png", 20 | "density": 3.0 21 | }, 22 | { 23 | "src": "favicon.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "density": 4.0 27 | } 28 | ], 29 | "start_url": "index.html", 30 | "display": "standalone", 31 | "orientation": "portrait", 32 | "background_color": "#FFFFFF" 33 | } -------------------------------------------------------------------------------- /app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { combineReducers } from 'redux-immutable'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 10 | 11 | /* 12 | * routeReducer 13 | * 14 | * The reducer merges route location changes into our immutable state. 15 | * The change is necessitated by moving to react-router-redux@4 16 | * 17 | */ 18 | 19 | // Initial routing state 20 | const routeInitialState = fromJS({ 21 | locationBeforeTransitions: null, 22 | }); 23 | 24 | /** 25 | * Merge route into the global application state 26 | */ 27 | function routeReducer(state = routeInitialState, action) { 28 | switch (action.type) { 29 | /* istanbul ignore next */ 30 | case LOCATION_CHANGE: 31 | return state.merge({ 32 | locationBeforeTransitions: action.payload, 33 | }); 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | /** 40 | * Creates the main reducer with the asynchronously loaded ones 41 | */ 42 | export default function createReducer(asyncReducers) { 43 | return combineReducers({ 44 | route: routeReducer, 45 | language: languageProviderReducer, 46 | ...asyncReducers, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | // These are the pages you can go to. 2 | // They are all wrapped in the App component, which should contain the navbar etc 3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information 4 | // about the code splitting business 5 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 6 | 7 | const errorLoading = (err) => { 8 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console 9 | }; 10 | 11 | const loadModule = (cb) => (componentModule) => { 12 | cb(null, componentModule.default); 13 | }; 14 | 15 | export default function createRoutes(store) { 16 | // Create reusable async injectors using getAsyncInjectors factory 17 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 18 | 19 | return [ 20 | { 21 | path: '/', 22 | name: 'homepage', 23 | getComponent(nextState, cb) { 24 | const importModules = Promise.all([ 25 | System.import('containers/Homepage/reducer'), 26 | System.import('containers/Homepage/sagas'), 27 | System.import('containers/Homepage'), 28 | ]); 29 | 30 | const renderRoute = loadModule(cb); 31 | 32 | importModules.then(([reducer, sagas, component]) => { 33 | injectReducer('homepage', reducer.default); 34 | injectSagas(sagas.default); 35 | renderRoute(component); 36 | }); 37 | 38 | importModules.catch(errorLoading); 39 | }, 40 | }, { 41 | path: '*', 42 | name: 'notfound', 43 | getComponent(nextState, cb) { 44 | System.import('containers/NotFoundPage') 45 | .then(loadModule(cb)) 46 | .catch(errorLoading); 47 | }, 48 | }, 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | 13 | export default function configureStore(initialState = {}, history) { 14 | // Create the store with two middlewares 15 | // 1. sagaMiddleware: Makes redux-sagas work 16 | // 2. routerMiddleware: Syncs the location/URL path to the state 17 | const middlewares = [ 18 | sagaMiddleware, 19 | routerMiddleware(history), 20 | ]; 21 | 22 | const enhancers = [ 23 | applyMiddleware(...middlewares), 24 | ]; 25 | 26 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 27 | /* eslint-disable no-underscore-dangle */ 28 | const composeEnhancers = 29 | process.env.NODE_ENV !== 'production' && 30 | typeof window === 'object' && 31 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 32 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; 33 | /* eslint-enable */ 34 | 35 | const store = createStore( 36 | createReducer(), 37 | fromJS(initialState), 38 | composeEnhancers(...enhancers) 39 | ); 40 | 41 | // Extensions 42 | store.runSaga = sagaMiddleware.run; 43 | store.asyncReducers = {}; // Async reducer registry 44 | 45 | // Make reducers hot reloadable, see http://mxs.is/googmo 46 | /* istanbul ignore next */ 47 | if (module.hot) { 48 | module.hot.accept('./reducers', () => { 49 | System.import('./reducers').then((reducerModule) => { 50 | const createReducers = reducerModule.default; 51 | const nextReducers = createReducers(store.asyncReducers); 52 | 53 | store.replaceReducer(nextReducers); 54 | }); 55 | }); 56 | } 57 | 58 | return store; 59 | } 60 | -------------------------------------------------------------------------------- /app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from '../store'; // eslint-disable-line 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /app/utils/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash'; 2 | import invariant from 'invariant'; 3 | import warning from 'warning'; 4 | import createReducer from 'reducers'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | asyncReducers: isObject, 17 | }; 18 | invariant( 19 | conformsTo(store, shape), 20 | '(app/utils...) asyncInjectors: Expected a valid redux store' 21 | ); 22 | } 23 | 24 | /** 25 | * Inject an asynchronously loaded reducer 26 | */ 27 | export function injectAsyncReducer(store, isValid) { 28 | return function injectReducer(name, asyncReducer) { 29 | if (!isValid) checkStore(store); 30 | 31 | invariant( 32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 34 | ); 35 | 36 | if (Reflect.has(store.asyncReducers, name)) return; 37 | 38 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 39 | store.replaceReducer(createReducer(store.asyncReducers)); 40 | }; 41 | } 42 | 43 | /** 44 | * Inject an asynchronously loaded saga 45 | */ 46 | export function injectAsyncSagas(store, isValid) { 47 | return function injectSagas(sagas) { 48 | if (!isValid) checkStore(store); 49 | 50 | invariant( 51 | Array.isArray(sagas), 52 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 53 | ); 54 | 55 | warning( 56 | !isEmpty(sagas), 57 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 58 | ); 59 | 60 | sagas.map(store.runSaga); 61 | }; 62 | } 63 | 64 | /** 65 | * Helper for creating injectors 66 | */ 67 | export function getAsyncInjectors(store) { 68 | checkStore(store); 69 | 70 | return { 71 | injectReducer: injectAsyncReducer(store, true), 72 | injectSagas: injectAsyncSagas(store, true), 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /app/utils/tests/asyncInjectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test async injectors 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from 'store'; 7 | import { memoryHistory } from 'react-router'; 8 | import { put } from 'redux-saga/effects'; 9 | import { fromJS } from 'immutable'; 10 | 11 | import { 12 | injectAsyncReducer, 13 | injectAsyncSagas, 14 | getAsyncInjectors, 15 | } from 'utils/asyncInjectors'; 16 | 17 | // Fixtures 18 | 19 | const initialState = fromJS({ reduced: 'soon' }); 20 | 21 | const reducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case 'TEST': 24 | return state.set('reduced', action.payload); 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | function* testSaga() { 31 | yield put({ type: 'TEST', payload: 'yup' }); 32 | } 33 | 34 | const sagas = [ 35 | testSaga, 36 | ]; 37 | 38 | describe('asyncInjectors', () => { 39 | let store; 40 | 41 | describe('getAsyncInjectors', () => { 42 | before(() => { 43 | store = configureStore({}, memoryHistory); 44 | }); 45 | 46 | it('given a store, should return all async injectors', () => { 47 | const { injectReducer, injectSagas } = getAsyncInjectors(store); 48 | 49 | injectReducer('test', reducer); 50 | injectSagas(sagas); 51 | 52 | const actual = store.getState().get('test'); 53 | const expected = initialState.merge({ reduced: 'yup' }); 54 | 55 | expect(actual.toJS()).toEqual(expected.toJS()); 56 | }); 57 | 58 | it('should throw if passed invalid store shape', () => { 59 | let result = false; 60 | 61 | Reflect.deleteProperty(store, 'dispatch'); 62 | 63 | try { 64 | getAsyncInjectors(store); 65 | } catch (err) { 66 | result = err.name === 'Invariant Violation'; 67 | } 68 | 69 | expect(result).toEqual(true); 70 | }); 71 | }); 72 | 73 | describe('helpers', () => { 74 | before(() => { 75 | store = configureStore({}, memoryHistory); 76 | }); 77 | 78 | describe('injectAsyncReducer', () => { 79 | it('given a store, it should provide a function to inject a reducer', () => { 80 | const injectReducer = injectAsyncReducer(store); 81 | 82 | injectReducer('test', reducer); 83 | 84 | const actual = store.getState().get('test'); 85 | const expected = initialState; 86 | 87 | expect(actual.toJS()).toEqual(expected.toJS()); 88 | }); 89 | 90 | it('should throw if passed invalid name', () => { 91 | let result = false; 92 | 93 | const injectReducer = injectAsyncReducer(store); 94 | 95 | try { 96 | injectReducer('', reducer); 97 | } catch (err) { 98 | result = err.name === 'Invariant Violation'; 99 | } 100 | 101 | try { 102 | injectReducer(999, reducer); 103 | } catch (err) { 104 | result = err.name === 'Invariant Violation'; 105 | } 106 | 107 | expect(result).toEqual(true); 108 | }); 109 | 110 | it('should throw if passed invalid reducer', () => { 111 | let result = false; 112 | 113 | const injectReducer = injectAsyncReducer(store); 114 | 115 | try { 116 | injectReducer('bad', 'nope'); 117 | } catch (err) { 118 | result = err.name === 'Invariant Violation'; 119 | } 120 | 121 | try { 122 | injectReducer('coolio', 12345); 123 | } catch (err) { 124 | result = err.name === 'Invariant Violation'; 125 | } 126 | 127 | expect(result).toEqual(true); 128 | }); 129 | }); 130 | 131 | describe('injectAsyncSagas', () => { 132 | it('given a store, it should provide a function to inject a saga', () => { 133 | const injectSagas = injectAsyncSagas(store); 134 | 135 | injectSagas(sagas); 136 | 137 | const actual = store.getState().get('test'); 138 | const expected = initialState.merge({ reduced: 'yup' }); 139 | 140 | expect(actual.toJS()).toEqual(expected.toJS()); 141 | }); 142 | 143 | it('should throw if passed invalid saga', () => { 144 | let result = false; 145 | 146 | const injectSagas = injectAsyncSagas(store); 147 | 148 | try { 149 | injectSagas({ testSaga }); 150 | } catch (err) { 151 | result = err.name === 'Invariant Violation'; 152 | } 153 | 154 | try { 155 | injectSagas(testSaga); 156 | } catch (err) { 157 | result = err.name === 'Invariant Violation'; 158 | } 159 | 160 | expect(result).toEqual(true); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Set build version format here instead of in the admin panel 4 | version: "{build}" 5 | 6 | # Do not build on gh tags 7 | skip_tags: true 8 | 9 | # Test against these versions of Node.js 10 | environment: 11 | 12 | matrix: 13 | # Node versions to run 14 | - nodejs_version: 6 15 | - nodejs_version: 5 16 | - nodejs_version: 4 17 | 18 | # Fix line endings in Windows. (runs before repo cloning) 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | # Install scripts--runs after repo cloning 23 | install: 24 | # Install chrome 25 | - choco install -y googlechrome 26 | # Install the latest stable version of Node 27 | - ps: Install-Product node $env:nodejs_version 28 | - npm -g install npm 29 | - set PATH=%APPDATA%\npm;%PATH% 30 | - npm install 31 | 32 | # Disable automatic builds 33 | build: off 34 | 35 | # Post-install test scripts 36 | test_script: 37 | # Output debugging info 38 | - node --version 39 | - npm --version 40 | # run build and run tests 41 | - npm run build 42 | 43 | # Cache node_modules for faster builds 44 | cache: 45 | - node_modules -> package.json 46 | 47 | # remove, as appveyor doesn't support secure variables on pr builds 48 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 49 | #on_success: 50 | #- npm run coveralls 51 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Table of Contents 4 | 5 | - [General](general) 6 | - [**CLI Commands**](general/commands.md) 7 | - [Tool Configuration](general/files.md) 8 | - [Server Configurations](general/server-configs.md) 9 | - [Deployment](general/deployment.md) *(currently Heroku specific)* 10 | - [FAQ](general/faq.md) 11 | - [Gotchas](general/gotchas.md) 12 | - [Remove](general/remove.md) 13 | - [Testing](testing) 14 | - [Unit Testing](testing/unit-testing.md) 15 | - [Component Testing](testing/component-testing.md) 16 | - [Remote Testing](testing/remote-testing.md) 17 | - [CSS](css) 18 | - [`styled-components`](css/styled-componets.md) 19 | - [sanitize.css](css/sanitize.md) 20 | - [JS](js) 21 | - [Redux](js/redux.md) 22 | - [ImmutableJS](js/immutablejs.md) 23 | - [reselect](js/reselect.md) 24 | - [redux-saga](js/redux-saga.md) 25 | - [i18n](js/i18n.md) 26 | - [routing](js/routing.md) 27 | 28 | ## Overview 29 | 30 | ### Development 31 | 32 | Run `npm start` to see your app at `localhost:3000` 33 | 34 | ### Building & Deploying 35 | 36 | 1. Run `npm run build`, which will compile all the necessary files to the 37 | `build` folder. 38 | 39 | 2. Upload the contents of the `build` folder to your web server's root folder. 40 | 41 | ### Structure 42 | 43 | The [`app/`](../../../tree/master/app) directory contains your entire application code, including CSS, 44 | JavaScript, HTML and tests. 45 | 46 | The rest of the folders and files only exist to make your life easier, and 47 | should not need to be touched. 48 | 49 | *(If they do have to be changed, please [submit an issue](https://github.com/kelsonic/react-redux-material-ui-boilerplate/issues)!)* 50 | 51 | ### CSS 52 | 53 | Utilising [tagged template literals](./docs/tagged-template-literals.md) 54 | (a recent addition to JavaScript) and the [power of CSS](./docs/css-we-support.md), 55 | `styled-components` allows you to write actual CSS code to style your components. 56 | It also removes the mapping between components and styles – using components as a 57 | low-level styling construct could not be easier! 58 | 59 | See the [CSS documentation](./css/README.md) for more information. 60 | 61 | ### JS 62 | 63 | We bundle all your clientside scripts and chunk them into several files using 64 | code splitting where possible. We then automatically optimize your code when 65 | building for production so you don't have to worry about that. 66 | 67 | See the [JS documentation](./js/README.md) for more information about the 68 | JavaScript side of things. 69 | 70 | ### SEO 71 | 72 | We use [react-helmet](https://github.com/nfl/react-helmet) for managing document head tags. Examples on how to 73 | write head tags can be found [here](https://github.com/nfl/react-helmet#examples). 74 | 75 | ### Testing 76 | 77 | For a thorough explanation of the testing procedure, see the 78 | [testing documentation](./testing/README.md)! 79 | 80 | #### Performance testing 81 | 82 | With the production server running (i.e. while `npm run start:production` is running in 83 | another tab), enter `npm run pagespeed` to run Google PageSpeed Insights and 84 | get a performance check right in your terminal! 85 | 86 | #### Browser testing 87 | 88 | `npm run start:tunnel` makes your locally-running app globally available on the web 89 | via a temporary URL: great for testing on different devices, client demos, etc! 90 | 91 | #### Unit testing 92 | 93 | Unit tests live in `test/` directories right next to the components being tested 94 | and are run with `npm run test`. 95 | -------------------------------------------------------------------------------- /docs/css/README.md: -------------------------------------------------------------------------------- 1 | # CSS 2 | 3 | This boilerplate uses [`styled-components`](https://github.com/styled-components/styled-components) 4 | allowing you to write your CSS in your JavaScript, 5 | removing the mapping between styles and components. 6 | 7 | `styled-components` let's us embrace component encapsulation while sanitize.css gives us 8 | data-driven cross-browser normalisation. 9 | 10 | Learn more: 11 | 12 | - [`syled-components`](styled-componets.md) 13 | - [sanitize.css](sanitize.md) 14 | - [Using Sass](sass.md) 15 | -------------------------------------------------------------------------------- /docs/css/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing `sanitize.css` 2 | 3 | Delete [lines 31 and 32 in `app.js`](../../app/app.js#L31-L32) and remove it 4 | from the `dependencies` in [`package.json`](../../package.json)! 5 | -------------------------------------------------------------------------------- /docs/css/sanitize.md: -------------------------------------------------------------------------------- 1 | # `sanitize.css` 2 | 3 | Sanitize.css makes browsers render elements more in 4 | line with developer expectations (e.g. having the box model set to a cascading 5 | `box-sizing: border-box`) and preferences (its defaults can be individually 6 | overridden). 7 | 8 | It was selected over older projects like `normalize.css` and `reset.css` due 9 | to its greater flexibility and better alignment with CSSNext features like CSS 10 | variables. 11 | 12 | See the [official documentation](https://github.com/10up/sanitize.css) for more 13 | information. 14 | 15 | --- 16 | 17 | _Don't like this feature? [Click here](remove.md)_ 18 | -------------------------------------------------------------------------------- /docs/css/sass.md: -------------------------------------------------------------------------------- 1 | # Can I use Sass with this boilerplate? 2 | 3 | Yes, although we advise against it and **do not support this**. We selected 4 | [`styled-components`](https://github.com/styled-components/styled-components) 5 | over Sass because its approach is more powerful: instead of trying to 6 | give a styling language programmatic abilities, it pulls logic and configuration 7 | out into JS where we believe those features belong. 8 | 9 | If you _really_ still want (or need) to use Sass then... 10 | 11 | 1. You will need to add a [sass-loader](https://github.com/jtangelder/sass-loader) 12 | to the loaders section in `internals/webpack/webpack.base.babel.js` so it reads something like 13 | ```javascript 14 | { 15 | test: /\.scss$/, 16 | exclude: /node_modules/, 17 | loaders: ['style', 'css', 'sass'] 18 | } 19 | ``` 20 | 21 | Then run `npm i -D sass-loader node-sass` 22 | 23 | ...and you should be good to go! 24 | -------------------------------------------------------------------------------- /docs/css/styled-componets.md: -------------------------------------------------------------------------------- 1 | # `styled-components` 2 | 3 | `styled-components` allow you to write actual CSS code in your JavaScript to style your components, 4 | removing the mapping between components and styles. 5 | 6 | See the 7 | [official documentation](https://github.com/styled-components/styled-components) 8 | for more information! 9 | 10 | ## Usage 11 | 12 | This creates two react components, `` and `<Wrapper>`: 13 | 14 | ```JSX 15 | import React from 'react'; 16 | 17 | import styled from 'styled-components'; 18 | 19 | // Create a <Title> react component that renders an <h1> which is 20 | // centered, palevioletred and sized at 1.5em 21 | const Title = styled.h1` 22 | font-size: 1.5em; 23 | text-align: center; 24 | color: palevioletred; 25 | `; 26 | 27 | // Create a <Wrapper> react component that renders a <section> with 28 | // some padding and a papayawhip background 29 | const Wrapper = styled.section` 30 | padding: 4em; 31 | background: papayawhip; 32 | `; 33 | ``` 34 | 35 | *(The CSS rules are automatically vendor prefixed, so you don't have to think about it!)* 36 | 37 | You render them like so: 38 | 39 | ```JSX 40 | // Use them like any other React component – except they're styled! 41 | <Wrapper> 42 | <Title>Hello World, this is my first styled component! 43 | 44 | ``` 45 | 46 | For further examples see the 47 | [official documentation](https://github.com/styled-components/styled-components). 48 | -------------------------------------------------------------------------------- /docs/general/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The JavaScript ecosystem evolves at incredible speed: staying current can feel 4 | overwhelming. So, instead of you having to stay on top of every new tool, 5 | feature and technique to hit the headlines, this project aims to lighten the 6 | load by providing a curated baseline of the most valuable ones. 7 | 8 | Using React Boilerplate, you get to start your app with our community's current 9 | ideas on what represents optimal developer experience, best practice, most 10 | efficient tooling and cleanest project structure. 11 | 12 | - [**CLI Commands**](commands.md) 13 | - [Tool Configuration](files.md) 14 | - [Server Configurations](server-configs.md) 15 | - [Deployment](deployment.md) *(currently Heroku specific)* 16 | - [FAQ](faq.md) 17 | - [Gotchas](gotchas.md) 18 | 19 | # Feature overview 20 | 21 | ## Quick scaffolding 22 | 23 | Automate the creation of components, containers, routes, selectors and sagas - 24 | and their tests - right from the CLI! 25 | 26 | Run `npm run generate` in your terminal and choose one of the parts you want 27 | to generate. They'll automatically be imported in the correct places and have 28 | everything set up correctly. 29 | 30 | > We use [plop] to generate new components, you can find all the logic and 31 | templates for the generation in `internals/generators`. 32 | 33 | [plop]: https://github.com/amwmedia/plop 34 | 35 | ## Instant feedback 36 | 37 | Enjoy the best DX and code your app at the speed of thought! Your saved changes 38 | to the CSS and JS are reflected instantaneously without refreshing the page. 39 | Preserve application state even when you update something in the underlying code! 40 | 41 | ## Predictable state management 42 | 43 | We use Redux to manage our applications state. We have also added optional 44 | support for the [Chrome Redux DevTools Extension] – if you have it installed, 45 | you can see, play back and change your action history! 46 | 47 | [Chrome Redux DevTools Extension]: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd 48 | 49 | ## Next generation JavaScript 50 | 51 | Use ESNext template strings, object destructuring, arrow functions, JSX syntax 52 | and more, today. This is possible thanks to Babel with the `latest`, `stage-0` 53 | and `react` presets! 54 | 55 | ## Next generation CSS 56 | 57 | Write composable CSS that's co-located with your components using [`styled-components`] 58 | for complete modularity. Unique generated class names keep the specificity low 59 | while eliminating style clashes. Ship only the styles that are used on the 60 | visible page for the best performance. 61 | 62 | [`styled-components`]: ../css/styled-components.md 63 | 64 | ## Industry-standard routing 65 | 66 | It's natural to want to add pages (e.g. `/about`) to your application, and 67 | routing makes this possible. Thanks to [react-router] with [react-router-redux], 68 | that's as easy as pie and the url is auto-synced to your application state! 69 | 70 | [react-router]: https://github.com/reactjs/react-router 71 | [react-router-redux]: https://github.com/reactjs/react-router-redux 72 | 73 | # Optional extras 74 | 75 | _Don't like any of these features? [Click here](remove.md)_ 76 | 77 | ## Offline-first 78 | 79 | The next frontier in performant web apps: availability without a network 80 | connection from the instant your users load the app. This is done with a 81 | ServiceWorker and a fallback to AppCache, so this feature even works on older 82 | browsers! 83 | 84 | > All your files are included automatically. No manual intervention needed 85 | thanks to Webpack's [`offline-plugin`](https://github.com/NekR/offline-plugin) 86 | 87 | ### Add To Homescreen 88 | 89 | After repeat visits to your site, users will get a prompt to add your application 90 | to their homescreen. Combined with offline caching, this means your web app can 91 | be used exactly like a native application (without the limitations of an app store). 92 | 93 | The name and icon to be displayed are set in the `app/manifest.json` file. 94 | Change them to your project name and icon, and try it! 95 | 96 | ## Performant Web Font Loading 97 | 98 | If you simply use web fonts in your project, the page will stay blank until 99 | these fonts are downloaded. That means a lot of waiting time in which users 100 | could already read the content. 101 | 102 | [FontFaceObserver](https://github.com/bramstein/fontfaceobserver) adds a class 103 | to the `body` when the fonts have loaded. (see [`app.js`](../../app/app.js#L26-L36) 104 | and [`App/styles.css`](../../app/containers/App/styles.css)) 105 | 106 | ### Adding a new font 107 | 108 | 1. Either add the `@font-face` declaration to `App/styles.css` or add a `` 109 | tag to the [`index.html`](../../app/index.html). (Don't forget to remove the `` 110 | for Open Sans from the [`index.html`](../../app/index.html)!) 111 | 112 | 2. In `App/styles.css`, specify your initial `font-family` in the `body` tag 113 | with only web-save fonts. In the `body.jsFontLoaded` tag, specify your 114 | `font-family` stack with your web font. 115 | 116 | 3. In `app.js` add a `Observer` for your font. 117 | 118 | ## Image optimization 119 | 120 | Images often represent the majority of bytes downloaded on a web page, so image 121 | optimization can often be a notable performance improvement. Thanks to Webpack's 122 | [`image-loader`](https://github.com/tcoopman/image-webpack-loader), every PNG, JPEG, GIF and SVG images 123 | is optimized. 124 | 125 | See [`image-loader`](https://github.com/tcoopman/image-webpack-loader) to customize optimizations options. 126 | -------------------------------------------------------------------------------- /docs/general/commands.md: -------------------------------------------------------------------------------- 1 | # Command Line Commands 2 | 3 | ## Development 4 | 5 | ```Shell 6 | npm run start 7 | ``` 8 | 9 | Starts the development server running on `http://localhost:3000` 10 | 11 | ## Generators 12 | 13 | ```Shell 14 | npm run generate 15 | ``` 16 | 17 | Allows you to auto-generate boilerplate code for common parts of your 18 | application, specifically `component`s, `container`s, and `route`s. You can 19 | also run `npm run generate ` to skip the first selection. (e.g. `npm run 20 | generate container`) 21 | 22 | ## Server 23 | 24 | ### Development 25 | 26 | ```Shell 27 | npm start 28 | ``` 29 | 30 | Starts the development server and makes your application accessible at 31 | `localhost:3000`. Tunnels that server with `ngrok`, which means the website 32 | accessible anywhere! Changes in the application code will be hot-reloaded. 33 | 34 | ### Production 35 | 36 | ```Shell 37 | npm run start:prod 38 | ``` 39 | 40 | Starts the production server, configured for optimal performance: assets are 41 | minified and served gzipped. 42 | 43 | ### Port 44 | 45 | To change the port the app is accessible at pass the `--port` option to the command 46 | with `--`. E.g. to make the app visible at `localhost:5000`, run the following: 47 | `npm start -- --port 5000` 48 | 49 | ## Building 50 | 51 | ```Shell 52 | npm run build 53 | ``` 54 | 55 | Preps your app for deployment. Optimizes and minifies all files, piping them to 56 | a folder called `build`. Upload the contents of `build` to your web server to 57 | see your work live! 58 | 59 | ## Testing 60 | 61 | See the [testing documentation](../testing/README.md) for detailed information 62 | about our testing setup! 63 | 64 | ## Unit testing 65 | 66 | ```Shell 67 | npm run test 68 | ``` 69 | 70 | Tests your application with the unit tests specified in the `*test.js` files 71 | throughout the application. 72 | All the `test` commands allow an optional `-- --grep string` argument to filter 73 | the tests ran by Karma. Useful if you need to run a specific test only. 74 | 75 | ```Shell 76 | # Run only the Button component tests 77 | npm run test:watch -- --grep Button 78 | ``` 79 | 80 | ### Browsers 81 | 82 | To choose the browser to run your unit tests in (Chrome by default), run one of 83 | the following commands: 84 | 85 | #### Firefox 86 | 87 | ```Shell 88 | npm run test:firefox 89 | ``` 90 | 91 | #### Safari 92 | 93 | ```Shell 94 | npm run test:safari 95 | ``` 96 | 97 | #### Internet Explorer 98 | 99 | *Windows only!* 100 | 101 | ```Shell 102 | npm run test:ie 103 | ``` 104 | 105 | ### Watching 106 | 107 | ```Shell 108 | npm run test:watch 109 | ``` 110 | 111 | Watches changes to your application and reruns tests whenever a file changes. 112 | 113 | ### Remote testing 114 | 115 | ```Shell 116 | npm run start:tunnel 117 | ``` 118 | Starts the development server and tunnels it with `ngrok`, making the website 119 | available on the entire world. Useful for testing on different devices in different locations! 120 | 121 | ### Performance testing 122 | 123 | ```Shell 124 | npm run pagespeed 125 | ``` 126 | 127 | With the remote server running (i.e. while `npm run start:prod` is running in 128 | another terminal session), enter this command to run Google PageSpeed Insights 129 | and get a performance check right in your terminal! 130 | 131 | ### Dependency size test 132 | 133 | ```Shell 134 | npm run analyze 135 | ``` 136 | 137 | This command will generate a `stats.json` file from your production build, which 138 | you can upload to the [webpack analyzer](https://webpack.github.io/analyse/). This 139 | analyzer will visualize your dependencies and chunks with detailed statistics 140 | about the bundle size. 141 | 142 | ## Linting 143 | 144 | ```Shell 145 | npm run lint 146 | ``` 147 | 148 | Lints your JavaScript and CSS. 149 | 150 | ### JavaScript 151 | 152 | ```Shell 153 | npm run lint:js 154 | ``` 155 | 156 | Only lints your JavaScript. 157 | 158 | ### CSS 159 | 160 | ```Shell 161 | npm run lint:css 162 | ``` 163 | 164 | Only lints your CSS. 165 | -------------------------------------------------------------------------------- /docs/general/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## Heroku 4 | 5 | ### Easy 5-Step Deployment Process 6 | 7 | *Step 1:* Create a _Procfile_ with the following line: `web: npm run start:prod`. We do this because Heroku runs `npm run start` by default, so we need this setting to override the default run command. 8 | 9 | *Step 2:* Install the Node.js buildpack for your Heroku app by running the following command: `heroku buildpacks:set https://github.com/heroku/heroku-buildpack-nodejs#v91 -a [your app name]`. Make sure to replace `#v91` with whatever the latest buildpack is, which you can [find here](https://github.com/heroku/heroku-buildpack-nodejs/releases). 10 | 11 | *Step 3:* Add this line to your `package.json` file in the scripts area: `"heroku-postbuild": "npm run build",`. This is so Heroku can build your production assets when deploying (more of which you can [read about here](https://devcenter.heroku.com/articles/nodejs-support#heroku-specific-build-steps)). Then, adjust the _prebuild_ script in your `package.json` file so it looks like this: `"prebuild": "npm run build:clean",` to avoid having Heroku attempt to run Karma tests (which are unsupported with this buildpack). 12 | 13 | *Step 4:* Run `heroku config:set NPM_CONFIG_PRODUCTION=false` so that Heroku can compile the NPM modules included in your _devDependencies_ (since many of these packages are required for the build process). 14 | 15 | *Step 5:* Follow the standard Heroku deploy process: 16 | 17 | 1. `git add .` 18 | 2. `git commit -m 'Made some epic changes as per usual'` 19 | 3. `git push heroku master` 20 | -------------------------------------------------------------------------------- /docs/general/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Where are Babel and ESLint configured? 4 | 5 | In package.json 6 | 7 | ## Where are the files coming from when I run `npm start`? 8 | 9 | In development Webpack compiles your application runs it in-memory. Only when 10 | you run `npm run build` will it write to disk and preserve your bundled 11 | application across computer restarts. 12 | 13 | ## How do I fix `Error: listen EADDRINUSE 127.0.0.1:3000`? 14 | 15 | This simply means that there's another process already listening on port 3000. 16 | The fix is to kill the process and rerun `npm start`. 17 | 18 | ### OS X / Linux: 19 | 20 | 1. Find the process id (PID): 21 | ```Shell 22 | ps aux | grep node 23 | ``` 24 | > This will return the PID as the value following your username: 25 | > ```Shell 26 | > janedoe 29811 49.1 2.1 3394936 356956 s004 S+ 4:45pm 2:40.07 node server 27 | > ``` 28 | > Note: If nothing is listed, you can try `lsof -i tcp:3000` 29 | 30 | 1. Then run 31 | ```Shell 32 | kill -9 YOUR_PID 33 | ``` 34 | > e.g. given the output from the example above, `YOUR_PID` is `29811`, hence 35 | that would mean you would run `kill -9 29811` 36 | 37 | ### Windows 38 | 39 | 1. Find the process id (PID): 40 | ```Shell 41 | netstat -a -o -n 42 | ``` 43 | 44 | > This will return a list of running processes and the ports they're 45 | listening on: 46 | > ``` 47 | > Proto Local Address Foreign Address State PID 48 | > TCP 0.0.0.0:25 0.0.0.0:0 Listening 4196 49 | > ... 50 | > TCP 0.0.0.0:3000 0.0.0.0:0 Listening 28344 51 | ``` 52 | 53 | 1. Then run 54 | ```Shell 55 | taskkill /F /PID YOUR_PID 56 | ``` 57 | > e.g. given the output from the example above, `YOUR_PID` is `28344`, hence 58 | that would mean you would run `taskkill /F /PID 28344` 59 | 60 | ## Issue with local caching when running in production mode (F5 / ctrl+F5 / cmd+r weird behavior) 61 | 62 | Your production site isn't working? You update the code and nothing changes? It drives you insane? 63 | 64 | #### Quick fix on your local browser: 65 | 66 | To fix it on your local browser, just do the following. (Suited when you're testing the production mode locally) 67 | 68 | `Chrome dev tools > Application > Clear Storage > Clear site data` *(Chrome)* 69 | 70 | #### Full in-depth explanation 71 | 72 | Read more at https://github.com/NekR/offline-plugin/blob/master/docs/updates.md 73 | 74 | ## Local webfonts not working for development 75 | 76 | In development mode CSS sourcemaps require that styling is loaded by blob://, 77 | resulting in browsers resolving font files relative to the main document. 78 | 79 | A way to use local webfonts in development mode is to add an absolute 80 | output.publicPath in webpack.dev.babel.js, with protocol. 81 | 82 | ```javascript 83 | // webpack.dev.babel.js 84 | 85 | output: { 86 | publicPath: 'http://127.0.0.1:3000/', 87 | /* … */ 88 | }, 89 | ``` 90 | 91 | ## Non-route containers 92 | 93 | > Note: Container will always be nested somewhere below a route. Even if there's dozens of components 94 | in between, somewhere up the tree will be route. (maybe only "/", but still a route) 95 | 96 | ### Where do I put the reducer? 97 | 98 | While you can include the reducer statically in `reducers.js`, we don't recommend this as you lose 99 | the benefits of code splitting. Instead, add it as a _composed reducer_. This means that you 100 | pass actions onward to a second reducer from a lower-level route reducer like so: 101 | 102 | 103 | ```JS 104 | // Main route reducer 105 | 106 | function myReducerOfRoute(state, action) { 107 | switch (action.type) { 108 | case SOME_OTHER_ACTION: 109 | return someOtherReducer(state, action); 110 | } 111 | } 112 | ``` 113 | 114 | That way, you still get the code splitting at route level, but avoid having a static `combineReducers` 115 | call that includes all of them by default. 116 | 117 | *See [this and the following lesson](https://egghead.io/lessons/javascript-redux-reducer-composition-with-arrays?course=getting-started-with-redux) of the egghead.io Redux course for more information about reducer composition!* 118 | 119 | ### How do I run the saga? 120 | 121 | Since a container will always be within a route, one we can simply add it to the exported array in 122 | `sagas.js` of the route container somewhere up the tree: 123 | 124 | ```JS 125 | // /containers/SomeContainer/sagas.js 126 | 127 | import { someOtherSagaFromNestedContainer } from './containers/SomeNestedContainer/sagas'; 128 | 129 | function* someSaga() { /* … */ } 130 | 131 | export default [ 132 | someSaga, 133 | someOtherSagaFromNestedContainer, 134 | ]; 135 | ``` 136 | 137 | Or, if you have multiple sagas in the nested container: 138 | 139 | 140 | ```JS 141 | // /containers/SomeContainer/sagas.js 142 | 143 | import nestedContainerSagas from './containers/SomeNestedContainer/sagas'; 144 | 145 | function* someSaga() { /* … */ } 146 | 147 | export default [ 148 | someSaga, 149 | ...nestedContainerSagas, 150 | ]; 151 | ``` 152 | 153 | ## Using this boilerplate with WebStorm 154 | 155 | WebStorm is a powerful IDE, and why not also use it as debugger tool? Here is the steps 156 | 157 | 1. [Install JetBrain Chrome Extension](https://chrome.google.com/webstore/detail/jetbrains-ide-support/hmhgeddbohgjknpmjagkdomcpobmllji) 158 | 2. [Setting up the PORT](https://www.jetbrains.com/help/webstorm/2016.1/using-jetbrains-chrome-extension.html) 159 | 3. Change WebPack devtool config to `source-map` 160 | 4. Run web server (`npm run start`) 161 | 5. Create Run Configuration (Run > Edit Configurations) 162 | 6. Add new `JavaScript Debug` 163 | 7. Setting up URL 164 | 8. Start Debug (Click the green bug button) 165 | 9. Edit Run Configuration Again 166 | 10. Mapping Url as below picture 167 | * Map your `root` directory with `webpack://.` (please note the last dot) 168 | * Map your `build` directory with your root path (e.g. `http://localhost:3000`) 169 | 11. Hit OK and restart debugging session 170 | 171 | ![How to debug using WebStorm](webstorm-debug.png) 172 | 173 | ### Troubleshooting 174 | 175 | 1. You miss the last `.` (dot) in `webpack://.` 176 | 2. The port debugger is listening tool and the JetBrain extension is mismatch. 177 | 178 | ### Enable ESLint 179 | 180 | ESLint help making all developer follow the same coding format. Please also setting up in your IDE, otherwise, you will fail ESLint test. 181 | 1. Go to WebStorm Preference 182 | 2. Search for `ESLint` 183 | 3. Click `Enable` 184 | 185 | ![Setting up ESLint](webstorm-eslint.png) 186 | 187 | ## Use CI with bitbucket pipelines 188 | 189 | Your project is on bitbucket? Take advantage of the pipelines feature (Continuous Integration) by creating a 'bitbucket-pipelines.yml' file at the root of the project and use the following code to automatically test your app at each commit: 190 | 191 | ```YAML 192 | image: gwhansscheuren/bitbucket-pipelines-node-chrome-firefox 193 | 194 | pipelines: 195 | default: 196 | - step: 197 | script: 198 | - node --version 199 | - npm --version 200 | - npm install 201 | - npm test 202 | ``` 203 | 204 | ## I'm using Node v0.12 and the server doesn't work? 205 | 206 | We settled on supporting the last three major Node.js versions for the boilerplate – at the moment 207 | of this writing those are v4, v5 and v6. We **highly recommend upgrading to a newer Node.js version**! 208 | 209 | If you _have_ to use Node.js 0.12, you can hack around the server not running by using `babel-cli` to 210 | run the server: `npm install babel-cli`, and then replace all instances of `node server` in the `"scripts"` 211 | in the `package.json` with `babel server`! 212 | 213 | ## Have another question? 214 | 215 | Submit an [issue](https://github.com/kelsonic/react-redux-material-ui-boilerplate/issues), 216 | hop onto the [Gitter channel](https://gitter.im/mxstbr/react-boilerplate) 217 | -------------------------------------------------------------------------------- /docs/general/files.md: -------------------------------------------------------------------------------- 1 | # Configuration: A Glossary 2 | 3 | A guide to the configuration files for this project: where they live and what 4 | they do. 5 | 6 | ## The root folder 7 | 8 | * `.editorconfig`: Sets the default configuration for certain files across editors. (e.g. indentation) 9 | 10 | * `.gitattributes`: Normalizes how `git`, the version control system this boilerplate uses, handles certain files. 11 | 12 | * `.gitignore`: Tells `git` to ignore certain files and folders which don't need to be version controlled, like the build folder. 13 | 14 | * `.travis.yml` and `appveyor.yml`: Continuous Integration configuration
15 | This boilerplate uses [Travis CI](https://travis-ci.com) for Linux environments 16 | and [AppVeyor](https://www.appveyor.com/) for Windows platforms, but feel free 17 | to swap either out for your own choice of CI. 18 | 19 | * `package.json`: Our `npm` configuration file has three functions: 20 | 21 | 1. It's where Babel and ESLint are configured 22 | 1. It's the API for the project: a consistent interface for all its controls 23 | 1. It lists the project's package dependencies 24 | 25 | Baking the config in is a slightly unusual set-up, but it allows us to keep 26 | the project root as uncluttered and grokkable-at-a-glance as possible. 27 | 28 | ## The `./internals` folder 29 | 30 | This is where the bulk of the tooling configuration lives, broken out into 31 | recognisable units of work. 32 | 33 | Feel free to change anything you like but don't be afraid to [ask upfront](https://gitter.im/mxstbr/react-boilerplate) 34 | whether you should: build systems are easy to break! 35 | -------------------------------------------------------------------------------- /docs/general/gotchas.md: -------------------------------------------------------------------------------- 1 | # Gotchas 2 | 3 | These are some things to be aware of when using this boilerplate. 4 | 5 | ## Special images in HTML files 6 | 7 | If you specify your images in the `.html` files using the `` tag, everything 8 | will work fine. The problem comes up if you try to include images using anything 9 | except that tag, like meta tags: 10 | 11 | ```HTML 12 | 13 | ``` 14 | 15 | The webpack `html-loader` does not recognise this as an image file and will not 16 | transfer the image to the build folder. To get webpack to transfer them, you 17 | have to import them with the file loader in your JavaScript somewhere, e.g.: 18 | 19 | ```JavaScript 20 | import 'file?name=[name].[ext]!../img/yourimg.png'; 21 | ``` 22 | 23 | Then webpack will correctly transfer the image to the build folder. 24 | -------------------------------------------------------------------------------- /docs/general/remove.md: -------------------------------------------------------------------------------- 1 | ### Removing offline access 2 | 3 | **Careful** about removing this, as there is no real downside to having your 4 | application available when the users network connection isn't perfect. 5 | 6 | To remove offline capability, delete the `offline-plugin` from the 7 | [`package.json`](../../package.json), remove the import of the plugin in 8 | [`app.js`](../../app/app.js) and remove the plugin from the 9 | [`webpack.prod.babel.js`](../../internals/webpack/webpack.prod.babel.js). 10 | 11 | ### Removing add to homescreen functionality 12 | 13 | Delete [`manifest.json`](../../app/manifest.json) and remove the 14 | `` tag from the 15 | [`index.html`](../../app/index.html). 16 | 17 | ### Removing performant web font loading 18 | 19 | **Careful** about removing this, as perceived performance might be highly impacted. 20 | 21 | To remove `FontFaceObserver`, don't import it in [`app.js`](../../app/app.js) and 22 | remove it from the [`package.json`](../../package.json). 23 | 24 | ### Removing image optimization 25 | 26 | To remove image optimization, delete the `image-webpack-loader` from the 27 | [`package.json`](../../package.json), and remove the `image-loader` from [`webpack.base.babel.js`](../../internals/webpack/webpack.base.babel.js): 28 | ``` 29 | … 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 | … 38 | ``` 39 | 40 | Then replace it with classic `file-loader`: 41 | 42 | ``` 43 | … 44 | { 45 | test: /\.(jpg|png|gif)$/, 46 | loader: 'file-loader', 47 | } 48 | … 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/general/server-configs.md: -------------------------------------------------------------------------------- 1 | # Server Configurations 2 | 3 | ## Apache 4 | 5 | This boilerplate includes a `.htaccess` file that does two things: 6 | 7 | 1. Redirect all traffic to HTTPS because ServiceWorker only works for encrypted 8 | traffic. 9 | 1. Rewrite all pages (e.g. `yourdomain.com/subpage`) to `yourdomain.com/index.html` 10 | to let `react-router` take care of presenting the correct page. 11 | 12 | > Note: For performance reasons you should probably adapt it to run as a static 13 | `.conf` file (typically under `/etc/apache2/sites-enabled` or similar) so that 14 | your server doesn't have to apply its rules dynamically per request) 15 | 16 | ## Nginx 17 | 18 | Also it includes a `.nginx.conf` file that does the same on Nginx server. 19 | -------------------------------------------------------------------------------- /docs/general/webstorm-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelsonic/react-redux-material-ui-boilerplate/82d226c8f375f39d006d537213483ca9dba68bff/docs/general/webstorm-debug.png -------------------------------------------------------------------------------- /docs/general/webstorm-eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelsonic/react-redux-material-ui-boilerplate/82d226c8f375f39d006d537213483ca9dba68bff/docs/general/webstorm-eslint.png -------------------------------------------------------------------------------- /docs/js/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 2 | 3 | ## State management 4 | 5 | This boilerplate manages application state using [Redux](redux.md), makes it 6 | immutable with [`ImmutableJS`](immutablejs.md) and keeps access performant 7 | via [`reselect`](reselect.md). 8 | 9 | For managing asynchronous flows (e.g. logging in) we use [`redux-saga`](redux-saga.md). 10 | 11 | For routing, we use [`react-router` in combination with `react-router-redux`](routing.md). 12 | 13 | We include a generator for components, containers, sagas, routes and selectors. 14 | Run `npm run generate` to choose from the available generators, and automatically 15 | add new parts of your application! 16 | 17 | > Note: If you want to skip the generator selection process, 18 | `npm run generate ` also works. (e.g. `npm run generate route`) 19 | 20 | ### Learn more 21 | 22 | - [Redux](redux.md) 23 | - [ImmutableJS](immutablejs.md) 24 | - [reselect](reselect.md) 25 | - [redux-saga](redux-saga.md) 26 | - [react-intl](i18n.md) 27 | - [routing](routing.md) 28 | 29 | ## Architecture: `components` and `containers` 30 | 31 | We adopted a split between stateless, reusable components called (wait for it...) 32 | `components` and stateful parent components called `containers`. 33 | 34 | ### Learn more 35 | 36 | See [this article](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 37 | by Dan Abramov for a great introduction to this approach. 38 | -------------------------------------------------------------------------------- /docs/js/i18n.md: -------------------------------------------------------------------------------- 1 | # `i18n` 2 | 3 | `react-intl` is a library to manage internationalization and pluralization support 4 | for your react application. This involves multi-language support for both the static text but also things like variable numbers, words or names that change with application state. `react-intl` provides an incredible amount of mature facility to preform these very tasks. 5 | 6 | The complete `react-intl` docs can be found here: 7 | 8 | https://github.com/yahoo/react-intl/wiki 9 | 10 | ## Usage 11 | 12 | Below we see a `messages.js` file for the `Footer` component example. A `messages.js` file should be included in any simple or container component that wants to use internationalization. You can add this support when you scaffold your component using this boilerplates scaffolding `plop` system. 13 | 14 | All default English text for the component is contained here (e.g. `This project is licensed under the MIT license.`), and is tagged with an ID (e.g. `boilerplate.components.Footer.license.message`) in addition to it's object definition id (e.g. `licenseMessage`). 15 | 16 | This is set in `react-intl`'s `defineMessages` function which is then exported for use in the component. You can read more about `defineMessages` here: 17 | 18 | https://github.com/yahoo/react-intl/wiki/API#definemessages 19 | 20 | ```js 21 | /* 22 | * Footer Messages 23 | * 24 | * This contains all the text for the Footer component. 25 | */ 26 | import { defineMessages } from 'react-intl'; 27 | 28 | export default defineMessages({ 29 | licenseMessage: { 30 | id: 'boilerplate.components.Footer.license.message', 31 | defaultMessage: 'This project is licensed under the MIT license.', 32 | }, 33 | authorMessage: { 34 | id: 'boilerplate.components.Footer.author.message', 35 | defaultMessage: ` 36 | Made with love by {author}. 37 | `, 38 | }, 39 | }); 40 | ``` 41 | 42 | Below is the example `Footer` component. Here we see the component including the `messages.js` file, which contains all the default component text, organized with ids (and optionally descriptions). We are also importing the `FormattedMessage` component, which will display a given message from the `messages.js` file in the selected language. 43 | 44 | You will also notice a more complex use of `FormattedMessage` for the author message where alternate or variable values (i.e. `author: Kelsonic,`) are being injected, in this case it's a react component. 45 | 46 | ```js 47 | import React from 'react'; 48 | 49 | import messages from './messages'; 50 | import A from 'components/A'; 51 | import styles from './styles.css'; 52 | import { FormattedMessage } from 'react-intl'; 53 | 54 | function Footer() { 55 | return ( 56 |
57 |
58 |

59 | 60 |

61 |
62 |
63 |

64 | Kelsonic, 68 | }} 69 | /> 70 |

71 |
72 |
73 | ); 74 | } 75 | 76 | export default Footer; 77 | ``` 78 | 79 | ## Extracting i18n JSON files 80 | 81 | You can extract all i18n language within each component by running the following command: 82 | 83 | ``` 84 | npm run extract-intl 85 | ``` 86 | 87 | This will extract all language into i18n JSON files in `app/translations`. 88 | 89 | ## Adding A Language 90 | 91 | You can add a language by running the generate command: 92 | 93 | ``` 94 | npm run generate language 95 | ``` 96 | 97 | Then enter the two character i18n standard language specifier (e.g. "fr", "de", "es" - without quotes). This will add in the necessary JSON language file and import statements for the language. Note, it is up to you to fill in the translations for the language. 98 | 99 | ## Removing i18n and react-intl 100 | 101 | You can remove `react-intl` modules by first removing the `IntlProvider` object from the `app/app.js` file and by either removing or not selecting the i18n text option during component scaffolding. 102 | 103 | The packages associated with `react-intl` are: 104 | - react-intl 105 | - babel-plugin-react-intl 106 | -------------------------------------------------------------------------------- /docs/js/immutablejs.md: -------------------------------------------------------------------------------- 1 | # ImmutableJS 2 | 3 | Immutable data structures can be deeply compared in no time. This allows us to 4 | efficiently determine if our components need to rerender since we know if the 5 | `props` changed or not! 6 | 7 | Check out the [official documentation](https://facebook.github.io/immutable-js/) 8 | for a good explanation of the more intricate benefits it has. 9 | 10 | ## Usage 11 | 12 | In our reducers, we make the initial state an immutable data structure with the 13 | `fromJS` function. We pass it an object or an array, and it takes care of 14 | converting it to a immutable data structure. (Note: the conversion is performed deeply so 15 | that even arbitrarily nested arrays/objects are immutable structures too!) 16 | 17 | ```JS 18 | import { fromJS } from 'immutable'; 19 | 20 | const initialState = fromJS({ 21 | myData: { 22 | message: 'Hello World!' 23 | }, 24 | }); 25 | ``` 26 | 27 | 28 | 29 | When a reducer is subscribed to an action and needs to return the new state they can do so by using setter methods such as [`.set`](https://facebook.github.io/immutable-js/docs/#/Map/set) and [`.update`](https://facebook.github.io/immutable-js/docs/#/Map/update) and [`.merge`](https://facebook.github.io/immutable-js/docs/#/Map/merge). 30 | If the changing state data is nested, we can utilize the 'deep' versions of these setters: [`.setIn`](https://facebook.github.io/immutable-js/docs/#/Map/setIn) and [`.updateIn`](https://facebook.github.io/immutable-js/docs/#/Map/updateIn), [`.mergeIn`](https://facebook.github.io/immutable-js/docs/#/Map/mergeIn). 31 | 32 | ```JS 33 | import { SOME_ACTION, SOME_OTHER_ACTION } from './actions'; 34 | 35 | // […] 36 | 37 | function myReducer(state = initialState, action) { 38 | switch (action.type) { 39 | case SOME_ACTION: 40 | return state.set('myData', action.payload); 41 | case SOME_OTHER_ACTION: 42 | return state.setIn(['myData', 'message'], action.payload); 43 | default: 44 | return state; 45 | } 46 | } 47 | ``` 48 | 49 | We use [`reselect`](./reselect.md) to efficiently cache our computed application 50 | state. Since that state is now immutable, we need to use the [`.get`](https://facebook.github.io/immutable-js/docs/#/Iterable/get) and [`.getIn`](https://facebook.github.io/immutable-js/docs/#/Iterable/getIn) 51 | functions to select the part we want. 52 | 53 | ```JS 54 | const myDataSelector = (state) => state.get('myData'); 55 | const messageSelector = (state) => state.getIn(['myData', 'message']); 56 | 57 | export default myDataSelector; 58 | ``` 59 | 60 | To learn more, check out [`reselect.md`](reselect.md)! 61 | 62 | ## Immutable Records 63 | 64 | ImmutableJS provides a number of immutable structures such as [`Map`](https://facebook.github.io/immutable-js/docs/#/Map), [`Set`](https://facebook.github.io/immutable-js/docs/#/Set) and [`List`](https://facebook.github.io/immutable-js/docs/#/List). 65 | One drawback to these structures is that properties must be accessed via the getter methods (`.get` or `.getIn`) and cannot be accessed with dot notation as they would in a plain javascript object. 66 | For instance you'll write `map.get('property')` instead of `object.property`, and `list.get(0)` instead of `array[0]`. 67 | This can make your code a little harder to follow and requires you to be extra cautious when passing arguments or props to functions or components that try to access values with regular dot notation. 68 | ImmutableJS's [`Record`](https://facebook.github.io/immutable-js/docs/#/Record) structure offers a solution to this issue. 69 | 70 | A `Record` is similar to a `Map` but has a fixed shape, meaning it's property keys are predefined and you can't later add a new property after the record is created. Attempting to set new properties will cause an error. 71 | One benefit of `Record` is that you can now, along with other immutable read methods (.get, .set, .merge and so on), use the dot notation to access properties. 72 | 73 | The creation of a record is less simple than simply calling `.toJS()`. 74 | First, you have to define the `Record` shape. With the example above, to create your initial state, you'll write: 75 | 76 | ```JS 77 | // Defining the shape 78 | const StateRecord = Record({ 79 | myData: { 80 | message: 'Hello World!' 81 | } 82 | }); 83 | 84 | const initialState = new StateRecord({}); // initialState is now a new StateRecord instance 85 | // initialized with myData.message set by default as 'Hello World!' 86 | ``` 87 | 88 | Now, if you want to access `myData`, you can just write `state.myData` in your reducer code and to access the `message` property you can write `state.myData.message` as you would in a plain javascript object. 89 | 90 | ### Gotchas of Using Records 91 | 92 | Although dot notation can now be used to read properties the same does not apply to setting properties. Any attempts to set a property on a `Record` using dot notation will result in errors. 93 | Instead setter methods ( `.set`, `.update`, `.merge`) should be used. 94 | 95 | Certain properties can not be set on a record as they would conflict with the API. Consider the below example: 96 | ```JS 97 | const ProductRecord = Record({ 98 | type: 'tshirt', 99 | size: 'small' 100 | }); 101 | ``` 102 | 103 | Because record.size is used to return the records count (similar to array.length), the above definition would throw an error. -------------------------------------------------------------------------------- /docs/js/redux-saga.md: -------------------------------------------------------------------------------- 1 | # `redux-saga` 2 | 3 | `redux-saga` is a library to manage side effects in your application. It works 4 | beautifully for data fetching, concurrent computations and a lot more. 5 | [Sebastien Lorber](https://twitter.com/sebastienlorber) put it best: 6 | 7 | > Imagine there is widget1 and widget2. When some button on widget1 is clicked, 8 | then it should have an effect on widget2. Instead of coupling the 2 widgets 9 | together (ie widget1 dispatch an action that targets widget2), widget1 only 10 | dispatch that its button was clicked. Then the saga listen for this button 11 | click and then update widget2 by dispatching a new event that widget2 is aware of. 12 | > 13 | > This adds a level of indirection that is unnecessary for simple apps, but make 14 | it more easy to scale complex applications. You can now publish widget1 and 15 | widget2 to different npm repositories so that they never have to know about 16 | each others, without having them to share a global registry of actions. The 2 17 | widgets are now bounded contexts that can live separately. They do not need 18 | each others to be consistent and can be reused in other apps as well. **The saga 19 | is the coupling point between the two widgets that coordinate them in a 20 | meaningful way for your business.** 21 | 22 | _Note: It is well worth reading the [source](https://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34623840#34623840) 23 | of this quote in its entirety!_ 24 | 25 | To learn more about this amazing way to handle concurrent flows, start with the 26 | [official documentation](https://github.com/yelouafi/redux-saga) and explore 27 | some examples! (read [this comparison](https://stackoverflow.com/questions/34930735/pros-cons-of-using-redux-saga-with-es6-generators-vs-redux-thunk-with-es7-async/34933395) if you're used to `redux-thunk`) 28 | 29 | ## Usage 30 | 31 | Sagas are associated with a container, just like actions, constants, selectors 32 | and reducers. If your container already has a `sagas.js` file, simply add your 33 | saga to that. If your container does not yet have a `sagas.js` file, add one with 34 | this boilerplate structure: 35 | 36 | ```JS 37 | import { take, call, put, select } from 'redux-saga/effects'; 38 | 39 | // Your sagas for this container 40 | export default [ 41 | sagaName, 42 | ]; 43 | 44 | // Individual exports for testing 45 | export function* sagaName() { 46 | 47 | } 48 | ``` 49 | 50 | Then, in your `routes.js`, add injection for the newly added saga: 51 | 52 | ```JS 53 | getComponent(nextState, cb) { 54 | const importModules = Promise.all([ 55 | System.import('containers/YourComponent/reducer'), 56 | System.import('containers/YourComponent/sagas'), 57 | System.import('containers/YourComponent'), 58 | ]); 59 | 60 | const renderRoute = loadModule(cb); 61 | 62 | importModules.then(([reducer, sagas, component]) => { 63 | injectReducer('home', reducer.default); 64 | injectSagas(sagas.default); // Inject the saga 65 | 66 | renderRoute(component); 67 | }); 68 | 69 | importModules.catch(errorLoading); 70 | }, 71 | ``` 72 | 73 | Now add as many sagas to your `sagas.js` file as you want! 74 | 75 | --- 76 | 77 | _Don't like this feature? [Click here](remove.md)_ 78 | -------------------------------------------------------------------------------- /docs/js/redux.md: -------------------------------------------------------------------------------- 1 | # Redux 2 | 3 | If you haven't worked with Redux, it's highly recommended (possibly indispensable!) 4 | to read through the (amazing) [official documentation](http://redux.js.org) 5 | and/or watch this [free video tutorial series](https://egghead.io/series/getting-started-with-redux). 6 | 7 | ## Usage 8 | 9 | See above! As minimal as Redux is, the challenge it addresses - app state 10 | management - is a complex topic that is too involved to properly discuss here. 11 | 12 | ## Removing redux 13 | 14 | There are a few reasons why we chose to bundle redux with React Boilerplate, the 15 | biggest being that it is widely regarded as the current best Flux implementation 16 | in terms of architecture, support and documentation. 17 | 18 | You may feel differently! This is completely OK :) 19 | 20 | Below are a few reasons you might want to remove it: 21 | 22 | ### I'm just getting started and Flux is hard 23 | 24 | You're under no obligation to use Redux or any other Flux library! The complexity 25 | of your application will determine the point at which you need to introduce it. 26 | 27 | Here are a couple of great resources for taking a minimal approach: 28 | 29 | - [Misconceptions of Tooling in JavaScript](http://javascriptplayground.com/blog/2016/02/the-react-webpack-tooling-problem) 30 | - [Learn Raw React — no JSX, no Flux, no ES6, no Webpack…](http://jamesknelson.com/learn-raw-react-no-jsx-flux-es6-webpack/) 31 | 32 | ### It's overkill for my project! 33 | 34 | See above. 35 | 36 | ### I prefer `(Alt|MobX|SomethingElse)`! 37 | 38 | React Boilerplate is a baseline for _your_ app: go for it! 39 | 40 | If you feel that we should take a closer look at supporting your preference 41 | out of the box, please let us know. 42 | -------------------------------------------------------------------------------- /docs/js/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing `redux-saga` 2 | 3 | **We don't recommend removing `redux-saga`**, as we strongly feel that it's the 4 | way to go for most redux based applications. 5 | 6 | If you really want to get rid of it, you will have to delete its traces from several places. 7 | 8 | **app/store.js** 9 | 10 | 1. Remove statement `import createSagaMiddleware from 'redux-saga'`. 11 | 2. Remove statement `const sagaMiddleware = createSagaMiddleware()`. 12 | 3. Remove `sagaMiddleware` from `middlewares` array. 13 | 4. Remove statement `store.runSaga = sagaMiddleware.run` 14 | 15 | **app/utils/asyncInjectors.js** 16 | 17 | 1. Remove `runSaga: isFunction` from `shape`. 18 | 2. Remove function `injectAsyncSagas`. 19 | 3. Do not export `injectSagas: injectAsyncSagas(store, true)`. 20 | 21 | **app/routes.js** 22 | 23 | 1. Do not pull out `injectSagas` from `getAsyncInjectors()`. 24 | 2. Remove `sagas` from `importModules.then()`. 25 | 3. Remove `injectSagas(sagas.default)` from every route that uses Saga. 26 | 27 | **Finally, remove it from the `package.json`. Then you should be good to go with whatever 28 | side-effect management library you want to use!** 29 | 30 | ## Removing `reselect` 31 | 32 | To remove `reselect`, remove it from your dependencies in `package.json` and then write 33 | your `mapStateToProps` functions like you normally would! 34 | 35 | You'll also need to hook up the history directly to the store. Make changes to `app/app.js`. 36 | 37 | 1. Remove statement `import { selectLocationState } from 'containers/App/selectors'` 38 | 2. Make necessary changes to `history` as follows: 39 | 40 | ```js 41 | 42 | const selectLocationState = () => { 43 | let prevRoutingState; 44 | let prevRoutingStateJS; 45 | 46 | return (state) => { 47 | const routingState = state.get('route'); // or state.route 48 | 49 | if (!routingState.equals(prevRoutingState)) { 50 | prevRoutingState = routingState; 51 | prevRoutingStateJS = routingState.toJS(); 52 | } 53 | 54 | return prevRoutingStateJS; 55 | }; 56 | }; 57 | 58 | const history = syncHistoryWithStore(browserHistory, store, { 59 | selectLocationState: selectLocationState(), 60 | }); 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/js/reselect.md: -------------------------------------------------------------------------------- 1 | # `reselect` 2 | 3 | reselect memoizes ("caches") previous state trees and calculations based on said 4 | tree. This means repeated changes and calculations are fast and efficient, 5 | providing us with a performance boost over standard `mapStateToProps` 6 | implementations. 7 | 8 | The [official documentation](https://github.com/reactjs/reselect) 9 | offers a good starting point! 10 | 11 | ## Usage 12 | 13 | There are two different kinds of selectors, simple and complex ones. 14 | 15 | ### Simple selectors 16 | 17 | Simple selectors are just that: they take the application state and select a 18 | part of it. 19 | 20 | ```javascript 21 | const mySelector = (state) => state.get('someState'); 22 | 23 | export { 24 | mySelector, 25 | }; 26 | ``` 27 | 28 | ### Complex selectors 29 | 30 | If we need to, we can combine simple selectors to build more complex ones which 31 | get nested state parts with reselect's `createSelector` function. We import other 32 | selectors and pass them to the `createSelector` call: 33 | 34 | ```javascript 35 | import { createSelector } from 'reselect'; 36 | import mySelector from 'mySelector'; 37 | 38 | const myComplexSelector = createSelector( 39 | mySelector, 40 | (myState) => myState.get('someNestedState') 41 | ); 42 | 43 | export { 44 | myComplexSelector, 45 | }; 46 | ``` 47 | 48 | These selectors can then either be used directly in our containers as 49 | `mapStateToProps` functions or be nested with `createSelector` once again: 50 | 51 | ```javascript 52 | export default connect(createSelector( 53 | myComplexSelector, 54 | (myNestedState) => ({ data: myNestedState }) 55 | ))(SomeComponent); 56 | ``` 57 | 58 | ### Adding a new selector 59 | 60 | If you have a `selectors.js` file next to the reducer which's part of the state 61 | you want to select, add your selector to said file. If you don't have one yet, 62 | add a new one into your container folder and fill it with this boilerplate code: 63 | 64 | ```JS 65 | import { createSelector } from 'reselect'; 66 | 67 | const selectMyState = () => createSelector( 68 | 69 | ); 70 | 71 | export { 72 | selectMyState, 73 | }; 74 | ``` 75 | 76 | --- 77 | 78 | _Don't like this feature? [Click here](remove.md)_ 79 | -------------------------------------------------------------------------------- /docs/js/routing.md: -------------------------------------------------------------------------------- 1 | # Routing via `react-router` and `react-router-redux` 2 | 3 | `react-router` is the de-facto standard routing solution for react applications. 4 | The thing is that with redux and a single state tree, the URL is part of that 5 | state. `react-router-redux` takes care of synchronizing the location of our 6 | application with the application state. 7 | 8 | (See the [`react-router-redux` documentation](https://github.com/reactjs/react-router-redux) 9 | for more information) 10 | 11 | ## Usage 12 | 13 | To add a new route, use the generator with `npm run generate route`. 14 | 15 | This is what a standard (generated) route looks like for a container: 16 | 17 | ```JS 18 | { 19 | path: '/', 20 | name: 'home', 21 | getComponent(nextState, cb) { 22 | const importModules = Promise.all([ 23 | System.import('containers/HomePage') 24 | ]); 25 | 26 | const renderRoute = loadModule(cb); 27 | 28 | importModules.then(([component]) => { 29 | renderRoute(component); 30 | }); 31 | 32 | importModules.catch(errorLoading); 33 | }, 34 | } 35 | ``` 36 | 37 | To go to a new page use the `push` function by `react-router-redux`: 38 | 39 | ```JS 40 | import { push } from 'react-router-redux'; 41 | 42 | dispatch(push('/some/page')); 43 | ``` 44 | 45 | ## Child Routes 46 | `npm run generate route` does not currently support automatically generating child routes if you need them, but they can be easily created manually. 47 | 48 | For example, if you have a route called `about` at `/about` and want to make a child route called `team` at `/about/our-team` you can just add that child page to the parent page's `childRoutes` array like so: 49 | 50 | ```JS 51 | /* your app's other routes would already be in this array */ 52 | { 53 | path: '/about', 54 | name: 'about', 55 | getComponent(nextState, cb) { 56 | const importModules = Promise.all([ 57 | System.import('containers/AboutPage'), 58 | ]); 59 | 60 | const renderRoute = loadModule(cb); 61 | 62 | importModules.then(([component]) => { 63 | renderRoute(component); 64 | }); 65 | 66 | importModules.catch(errorLoading); 67 | }, 68 | childRoutes: [ 69 | { 70 | path: '/about/our-team', 71 | name: 'team', 72 | getComponent(nextState, cb) { 73 | const importModules = Promise.all([ 74 | System.import('containers/TeamPage'), 75 | ]); 76 | 77 | const renderRoute = loadModule(cb); 78 | 79 | importModules.then(([component]) => { 80 | renderRoute(component); 81 | }); 82 | 83 | importModules.catch(errorLoading); 84 | }, 85 | }, 86 | ] 87 | } 88 | ``` 89 | 90 | ## Index routes 91 | 92 | To add an index route, use the following pattern: 93 | 94 | ```JS 95 | { 96 | path: '/', 97 | name: 'home', 98 | getComponent(nextState, cb) { 99 | const importModules = Promise.all([ 100 | System.import('containers/HomePage') 101 | ]); 102 | 103 | const renderRoute = loadModule(cb); 104 | 105 | importModules.then(([component]) => { 106 | renderRoute(component); 107 | }); 108 | 109 | importModules.catch(errorLoading); 110 | }, 111 | indexRoute: { 112 | getComponent(partialNextState, cb) { 113 | const importModules = Promise.all([ 114 | System.import('containers/HomeView') 115 | ]); 116 | 117 | const renderRoute = loadModule(cb); 118 | 119 | importModules.then(([component]) => { 120 | renderRoute(component); 121 | }); 122 | 123 | importModules.catch(errorLoading); 124 | }, 125 | }, 126 | } 127 | ``` 128 | 129 | ## Dynamic routes 130 | 131 | To go to a dynamic route such as 'post/:slug' eg 'post/cool-new-post', firstly add the route to your `routes.js`, as per documentation: 132 | 133 | ```JS 134 | path: '/posts/:slug', 135 | name: 'post', 136 | getComponent(nextState, cb) { 137 | const importModules = Promise.all([ 138 | System.import('containers/Post/reducer'), 139 | System.import('containers/Post/sagas'), 140 | System.import('containers/Post'), 141 | ]); 142 | 143 | const renderRoute = loadModule(cb); 144 | 145 | importModules.then(([reducer, sagas, component]) => { 146 | injectReducer('post', reducer.default); 147 | injectSagas(sagas.default); 148 | renderRoute(component); 149 | }); 150 | 151 | importModules.catch(errorLoading); 152 | }, 153 | ``` 154 | 155 | ###Container: 156 | 157 | ```JSX 158 | 159 | ``` 160 | 161 | Clickable link with payload (you could use push if needed). 162 | 163 | ###Action: 164 | 165 | ```JS 166 | export function getPost(slug) { 167 | return { 168 | type: LOAD_POST, 169 | slug, 170 | }; 171 | } 172 | 173 | export function postLoaded(post) { 174 | return { 175 | type: LOAD_POST_SUCCESS, 176 | podcast, 177 | }; 178 | } 179 | ``` 180 | 181 | ###Saga: 182 | 183 | ```JS 184 | const { slug } = yield take(LOAD_POST); 185 | yield call(getXhrPodcast, slug); 186 | 187 | export function* getXhrPodcast(slug) { 188 | const requestURL = `http://your.api.com/api/posts/${slug}`; 189 | const post = yield call(request, requestURL); 190 | if (!post.err) { 191 | yield put(postLoaded(post)); 192 | } else { 193 | yield put(postLoadingError(post.err)); 194 | } 195 | } 196 | ``` 197 | 198 | Wait (`take`) for the LOAD_POST constant, which contains the slug payload from the `getPost()` function in actions.js. 199 | 200 | When the action is fired then dispatch the `getXhrPodcast()` function to get the response from your api. On success dispatch the `postLoaded()` action (`yield put`) which sends back the response and can be added into the reducer state. 201 | 202 | 203 | You can read more on [`react-router`'s documentation](https://github.com/reactjs/react-router/blob/master/docs/API.md#props-3). 204 | -------------------------------------------------------------------------------- /docs/testing/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | - [Unit Testing](unit-testing.md) 4 | - [Component Testing](component-testing.md) 5 | - [Remote Testing](remote-testing.md) 6 | 7 | Testing your application is a vital part of serious development. There are a few 8 | things you should test. If you've never done this before start with [unit testing](unit-testing.md). 9 | Move on to [component testing](component-testing.md) when you feel like you 10 | understand that! 11 | 12 | We also support [remote testing](remote-testing.md) your local application, 13 | which is quite awesome, so definitely check that out! 14 | 15 | ## Usage with this boilerplate 16 | 17 | To test your application started with this boilerplate do the following: 18 | 19 | 1. Sprinkle `.test.js` files directly next to the parts of your application you 20 | want to test. (Or in `test/` subdirectories, it doesn't really matter as long 21 | as they are directly next to those parts and end in `.test.js`) 22 | 23 | 1. Write your unit and component tests in those files. 24 | 25 | 1. Run `npm run test` in your terminal and see all the tests pass! (hopefully) 26 | 27 | There are a few more commands related to testing, checkout the [commands documentation](../general/commands.md#testing) 28 | for the full list! 29 | -------------------------------------------------------------------------------- /docs/testing/component-testing.md: -------------------------------------------------------------------------------- 1 | # Component testing 2 | 3 | [Unit testing your Redux actions and reducers](unit-testing.md) is nice, but you 4 | can do even more to make sure nothing breaks your application. Since React is 5 | the _view_ layer of your app, let's see how to test Components too! 6 | 7 | 8 | 9 | - [Shallow rendering](#shallow-rendering) 10 | - [Enzyme](#enzyme) 11 | 12 | 13 | 14 | ## Shallow rendering 15 | 16 | React provides us with a nice add-on called the Shallow Renderer. This renderer 17 | will render a React component **one level deep**. Lets take a look at what that 18 | means with a simple ` 34 | ); 35 | } 36 | 37 | export default Button; 38 | ``` 39 | 40 | _Note: This is a [state**less** ("dumb") component](../js/README.md#architecture-components-and-containers)_ 41 | 42 | It might be used in another component like this: 43 | 44 | ```javascript 45 | // HomePage.react.js 46 | 47 | import Button from './Button.react'; 48 | 49 | class HomePage extends React.Component { 50 | render() { 51 | return( 52 | 53 | ); 54 | } 55 | } 56 | ``` 57 | 58 | _Note: This is a [state**ful** ("smart") component](../js/README.md#architecture-components-and-containers)!_ 59 | 60 | When rendered normally with the standard `ReactDOM.render` function, this will 61 | be the HTML output 62 | (*Comments added in parallel to compare structures in HTML from JSX source*): 63 | 64 | ```html 65 | 69 | ``` 70 | 71 | Conversely, when rendered with the shallow renderer, we'll get a String 72 | containing this "HTML": 73 | 74 | ```html 75 | 79 | ``` 80 | 81 | If we test our `Button` with the normal renderer and there's a problem 82 | with the `CheckmarkIcon` then the test for the `Button` will fail as well... 83 | but finding the culprit will be hard. Using the _shallow_ renderer, we isolate 84 | the problem's cause since we don't render any other components other than the 85 | one we're testing! 86 | 87 | The problem with the shallow renderer is that all assertions have to be done 88 | manually, and you cannot do anything that needs the DOM. 89 | 90 | Thankfully, [AirBnB](https://twitter.com/AirbnbEng) has open sourced their 91 | wrapper around the React shallow renderer and jsdom, called `enzyme`. `enzyme` 92 | is a testing utility that gives us a nice assertion/traversal/manipulation API. 93 | 94 | ## Enzyme 95 | 96 | Lets test our ` 119 | ); 120 | expect( 121 | renderedComponent.find("button").node 122 | ).toExist(); 123 | }); 124 | ``` 125 | 126 | Nice! If somebody breaks our button component by having it render an `` tag 127 | or something else we'll immediately know! Let's do something a bit more advanced 128 | now, and check that our ` 138 | ); 139 | expect( 140 | renderedComponent.contains(text) 141 | ).toEqual(true); 142 | }); 143 | ``` 144 | 145 | Great! Onwards to our last and most advanced test: checking that our `