This commit is contained in:
Nathan Bierema 2020-10-26 08:05:44 -04:00
parent c6870a7f7b
commit 452380aa00
148 changed files with 6341 additions and 6636 deletions

View File

@ -1,4 +1,4 @@
{ {
"presets": [ "es2015", "stage-0", "react" ], "presets": ["es2015", "stage-0", "react"],
"plugins": [ "add-module-exports", "transform-decorators-legacy" ] "plugins": ["add-module-exports", "transform-decorators-legacy"]
} }

View File

@ -15,18 +15,16 @@
"react/jsx-quotes": 0, "react/jsx-quotes": 0,
"block-scoped-var": 0, "block-scoped-var": 0,
"padded-blocks": 0, "padded-blocks": 0,
"quotes": [ 1, "single" ], "quotes": [1, "single"],
"comma-style": [ 2, "last" ], "comma-style": [2, "last"],
"no-use-before-define": [0, "nofunc"], "no-use-before-define": [0, "nofunc"],
"func-names": 0, "func-names": 0,
"prefer-const": 0, "prefer-const": 0,
"comma-dangle": 0, "comma-dangle": 0,
"id-length": 0, "id-length": 0,
"indent": [2, 2, {"SwitchCase": 1}], "indent": [2, 2, { "SwitchCase": 1 }],
"new-cap": [2, { "capIsNewExceptions": ["Test"] }], "new-cap": [2, { "capIsNewExceptions": ["Test"] }],
"default-case": 0 "default-case": 0
}, },
"plugins": [ "plugins": ["react"]
"react"
]
} }

View File

@ -2,7 +2,7 @@ sudo: required
dist: trusty dist: trusty
language: node_js language: node_js
node_js: node_js:
- "6" - '6'
cache: cache:
directories: directories:
- $HOME/.yarn-cache - $HOME/.yarn-cache
@ -19,7 +19,7 @@ addons:
- g++-4.8 - g++-4.8
install: install:
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" - '/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16'
- npm install -g yarn - npm install -g yarn
- yarn install - yarn install

View File

@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment Examples of behavior that contributes to creating a positive environment
include: include:
* Using welcoming and inclusive language - Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences - Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism - Gracefully accepting constructive criticism
* Focusing on what is best for the community - Focusing on what is best for the community
* Showing empathy towards other community members - Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or - The use of sexualized language or imagery and unwelcome sexual attention or
advances advances
* Trolling, insulting/derogatory comments, and personal or political attacks - Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment - Public or private harassment
* Publishing others' private information, such as a physical or electronic - Publishing others' private information, such as a physical or electronic
address, without explicit permission address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Our Responsibilities ## Our Responsibilities

View File

@ -10,29 +10,35 @@
## Installation ## Installation
### 1. For Chrome ### 1. For Chrome
- from [Chrome Web Store](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd);
- or download `extension.zip` from [last releases](https://github.com/zalmoxisus/redux-devtools-extension/releases), unzip, open `chrome://extensions` url and turn on developer mode from top left and then click; on `Load Unpacked` and select the extracted folder for use - from [Chrome Web Store](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd);
- or build it with `npm i && npm run build:extension` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./build/extension`; - or download `extension.zip` from [last releases](https://github.com/zalmoxisus/redux-devtools-extension/releases), unzip, open `chrome://extensions` url and turn on developer mode from top left and then click; on `Load Unpacked` and select the extracted folder for use
- or run it in dev mode with `npm i && npm start` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./dev`. - or build it with `npm i && npm run build:extension` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./build/extension`;
- or run it in dev mode with `npm i && npm start` and [load the extension's folder](https://developer.chrome.com/extensions/getstarted#unpacked) `./dev`.
### 2. For Firefox ### 2. For Firefox
- from [Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/);
- or build it with `npm i && npm run build:firefox` and [load the extension's folder](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox) `./build/firefox` (just select a file from inside the dir). - from [Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/);
- or build it with `npm i && npm run build:firefox` and [load the extension's folder](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox) `./build/firefox` (just select a file from inside the dir).
### 3. For Electron ### 3. For Electron
- just specify `REDUX_DEVTOOLS` in [`electron-devtools-installer`](https://github.com/GPMDP/electron-devtools-installer).
- just specify `REDUX_DEVTOOLS` in [`electron-devtools-installer`](https://github.com/GPMDP/electron-devtools-installer).
### 4. For other browsers and non-browser environment ### 4. For other browsers and non-browser environment
- use [`remote-redux-devtools`](https://github.com/zalmoxisus/remote-redux-devtools).
- use [`remote-redux-devtools`](https://github.com/zalmoxisus/remote-redux-devtools).
## Usage ## Usage
> Note that starting from v2.7, `window.devToolsExtension` was renamed to `window.__REDUX_DEVTOOLS_EXTENSION__` / `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`. > Note that starting from v2.7, `window.devToolsExtension` was renamed to `window.__REDUX_DEVTOOLS_EXTENSION__` / `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`.
## 1. With Redux ## 1. With Redux
### 1.1 Basic store ### 1.1 Basic store
For a basic [Redux store](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) simply add: For a basic [Redux store](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) simply add:
```diff ```diff
const store = createStore( const store = createStore(
reducer, /* preloadedState, */ reducer, /* preloadedState, */
@ -43,16 +49,22 @@ For a basic [Redux store](https://redux.js.org/api/createstore#createstorereduce
Note that [`preloadedState`](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) argument is optional in Redux's [`createStore`](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer). Note that [`preloadedState`](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) argument is optional in Redux's [`createStore`](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer).
> For universal ("isomorphic") apps, prefix it with `typeof window !== 'undefined' &&`. > For universal ("isomorphic") apps, prefix it with `typeof window !== 'undefined' &&`.
```js ```js
const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; const composeEnhancers =
(typeof window !== 'undefined' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
``` ```
> For TypeScript use [`redux-devtools-extension` npm package](#13-use-redux-devtools-extension-package-from-npm), which contains all the definitions, or just use `(window as any)` (see [Recipes](/docs/Recipes.md#using-in-a-typescript-project) for an example). > For TypeScript use [`redux-devtools-extension` npm package](#13-use-redux-devtools-extension-package-from-npm), which contains all the definitions, or just use `(window as any)` (see [Recipes](/docs/Recipes.md#using-in-a-typescript-project) for an example).
```js ```js
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
``` ```
In case ESLint is configured to not allow using the underscore dangle, wrap it like so: In case ESLint is configured to not allow using the underscore dangle, wrap it like so:
```diff ```diff
+ /* eslint-disable no-underscore-dangle */ + /* eslint-disable no-underscore-dangle */
const store = createStore( const store = createStore(
@ -67,7 +79,9 @@ In case ESLint is configured to not allow using the underscore dangle, wrap it l
> You don't need to npm install [`redux-devtools`](https://github.com/gaearon/redux-devtools) when using the extension (that's a different lib). > You don't need to npm install [`redux-devtools`](https://github.com/gaearon/redux-devtools) when using the extension (that's a different lib).
### 1.2 Advanced store setup ### 1.2 Advanced store setup
If you setup your store with [middleware and enhancers](http://redux.js.org/docs/api/applyMiddleware.html), change: If you setup your store with [middleware and enhancers](http://redux.js.org/docs/api/applyMiddleware.html), change:
```diff ```diff
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
@ -77,19 +91,21 @@ If you setup your store with [middleware and enhancers](http://redux.js.org/docs
applyMiddleware(...middleware) applyMiddleware(...middleware)
)); ));
``` ```
> Note that when the extension is not installed, were using Redux compose here. > Note that when the extension is not installed, were using Redux compose here.
To specify [extensions options](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md), use it like so: To specify [extensions options](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md), use it like so:
```js ```js
const composeEnhancers = const composeEnhancers =
typeof window === 'object' && typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ // Specify extensions options like name, actionsBlacklist, actionsCreators, serialize...
// Specify extensions options like name, actionsBlacklist, actionsCreators, serialize... })
}) : compose; : compose;
const enhancer = composeEnhancers( const enhancer = composeEnhancers(
applyMiddleware(...middleware), applyMiddleware(...middleware)
// other store enhancers if any // other store enhancers if any
); );
const store = createStore(reducer, enhancer); const store = createStore(reducer, enhancer);
@ -100,20 +116,28 @@ const store = createStore(reducer, enhancer);
### 1.3 Use `redux-devtools-extension` package from npm ### 1.3 Use `redux-devtools-extension` package from npm
To make things easier, there's an npm package to install: To make things easier, there's an npm package to install:
``` ```
npm install --save redux-devtools-extension npm install --save redux-devtools-extension
``` ```
and to use like so: and to use like so:
```js ```js
import { createStore, applyMiddleware } from 'redux'; import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension'; import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(reducer, composeWithDevTools( const store = createStore(
applyMiddleware(...middleware), reducer,
// other store enhancers if any composeWithDevTools(
)); applyMiddleware(...middleware)
// other store enhancers if any
)
);
``` ```
To specify [extensions options](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#windowdevtoolsextensionconfig): To specify [extensions options](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#windowdevtoolsextensionconfig):
```js ```js
import { createStore, applyMiddleware } from 'redux'; import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension'; import { composeWithDevTools } from 'redux-devtools-extension';
@ -121,87 +145,109 @@ import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools({ const composeEnhancers = composeWithDevTools({
// Specify name here, actionsBlacklist, actionsCreators and other options if needed // Specify name here, actionsBlacklist, actionsCreators and other options if needed
}); });
const store = createStore(reducer, /* preloadedState, */ composeEnhancers( const store = createStore(
applyMiddleware(...middleware), reducer,
// other store enhancers if any /* preloadedState, */ composeEnhancers(
)); applyMiddleware(...middleware)
// other store enhancers if any
)
);
``` ```
> Therere just [few lines of code](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/index.js) added to your bundle. > Therere just [few lines of code](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/index.js) added to your bundle.
In case you don't include other enhancers and middlewares, just use `devToolsEnhancer`: In case you don't include other enhancers and middlewares, just use `devToolsEnhancer`:
```js ```js
import { createStore } from 'redux'; import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension'; import { devToolsEnhancer } from 'redux-devtools-extension';
const store = createStore(reducer, /* preloadedState, */ devToolsEnhancer( const store = createStore(
reducer,
/* preloadedState, */ devToolsEnhancer()
// Specify name here, actionsBlacklist, actionsCreators and other options if needed // Specify name here, actionsBlacklist, actionsCreators and other options if needed
)); );
``` ```
### 1.4 Using in production ### 1.4 Using in production
It's useful to include the extension in production as well. Usually you [can use it for development](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f). It's useful to include the extension in production as well. Usually you [can use it for development](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f).
If you want to restrict it there, use `redux-devtools-extension/logOnlyInProduction`: If you want to restrict it there, use `redux-devtools-extension/logOnlyInProduction`:
```js ```js
import { createStore } from 'redux'; import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension/logOnlyInProduction'; import { devToolsEnhancer } from 'redux-devtools-extension/logOnlyInProduction';
const store = createStore(reducer, /* preloadedState, */ devToolsEnhancer( const store = createStore(
reducer,
/* preloadedState, */ devToolsEnhancer()
// options like actionSanitizer, stateSanitizer // options like actionSanitizer, stateSanitizer
)); );
``` ```
or with middlewares and enhancers: or with middlewares and enhancers:
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
const composeEnhancers = composeWithDevTools({ ```js
// options like actionSanitizer, stateSanitizer import { createStore, applyMiddleware } from 'redux';
}); import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
));
```
> You'll have to add `'process.env.NODE_ENV': JSON.stringify('production')` in your Webpack config for the production bundle ([to envify](https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md#exclude-devtools-from-production-builds)). If you use `create-react-app`, [it already does it for you.](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/config/webpack.config.prod.js#L253-L257)
If you're already checking `process.env.NODE_ENV` when creating the store, include `redux-devtools-extension/logOnly` for production environment. const composeEnhancers = composeWithDevTools({
// options like actionSanitizer, stateSanitizer
});
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware)
// other store enhancers if any
)
);
```
If you dont want to allow the extension in production, just use `redux-devtools-extension/developmentOnly`. > You'll have to add `'process.env.NODE_ENV': JSON.stringify('production')` in your Webpack config for the production bundle ([to envify](https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md#exclude-devtools-from-production-builds)). If you use `create-react-app`, [it already does it for you.](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/config/webpack.config.prod.js#L253-L257)
If you're already checking `process.env.NODE_ENV` when creating the store, include `redux-devtools-extension/logOnly` for production environment.
If you dont want to allow the extension in production, just use `redux-devtools-extension/developmentOnly`.
> See [the article](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f) for more details. > See [the article](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f) for more details.
### 1.5 For React Native, hybrid, desktop and server side Redux apps ### 1.5 For React Native, hybrid, desktop and server side Redux apps
For React Native we can use [`react-native-debugger`](https://github.com/jhen0409/react-native-debugger), which already included [the same API](https://github.com/jhen0409/react-native-debugger/blob/master/docs/redux-devtools-integration.md) with Redux DevTools Extension. For React Native we can use [`react-native-debugger`](https://github.com/jhen0409/react-native-debugger), which already included [the same API](https://github.com/jhen0409/react-native-debugger/blob/master/docs/redux-devtools-integration.md) with Redux DevTools Extension.
For most platforms, include [`Remote Redux DevTools`](https://github.com/zalmoxisus/remote-redux-devtools)'s store enhancer, and from the extension's context menu choose 'Open Remote DevTools' for remote monitoring. For most platforms, include [`Remote Redux DevTools`](https://github.com/zalmoxisus/remote-redux-devtools)'s store enhancer, and from the extension's context menu choose 'Open Remote DevTools' for remote monitoring.
## 2. Without Redux ## 2. Without Redux
See [integrations](docs/Integrations.md) and [the blog post](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f) for more details on how to use the extension with any architecture. See [integrations](docs/Integrations.md) and [the blog post](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f) for more details on how to use the extension with any architecture.
## Docs ## Docs
- [Options (arguments)](docs/API/Arguments.md)
- [Methods (advanced API)](docs/API/Methods.md) - [Options (arguments)](docs/API/Arguments.md)
- [FAQ](docs/FAQ.md) - [Methods (advanced API)](docs/API/Methods.md)
- Features - [FAQ](docs/FAQ.md)
- [Trace actions calls](/docs/Features/Trace.md) - Features
- [Troubleshooting](docs/Troubleshooting.md) - [Trace actions calls](/docs/Features/Trace.md)
- [Articles](docs/Articles.md) - [Troubleshooting](docs/Troubleshooting.md)
- [Videos](docs/Videos.md) - [Articles](docs/Articles.md)
- [Feedback](docs/Feedback.md) - [Videos](docs/Videos.md)
- [Feedback](docs/Feedback.md)
## Demo ## Demo
Live demos to use the extension with: Live demos to use the extension with:
- [Counter](http://zalmoxisus.github.io/examples/counter/) - [Counter](http://zalmoxisus.github.io/examples/counter/)
- [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/) - [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/)
- [Redux Form](http://redux-form.com/6.5.0/examples/simple/) - [Redux Form](http://redux-form.com/6.5.0/examples/simple/)
- [React Tetris](https://chvin.github.io/react-tetris/?lan=en) - [React Tetris](https://chvin.github.io/react-tetris/?lan=en)
- [Book Collection (Angular ngrx store)](https://ngrx.github.io/platform/example-app/) - [Book Collection (Angular ngrx store)](https://ngrx.github.io/platform/example-app/)
Also see [`./examples` folder](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/examples). Also see [`./examples` folder](https://github.com/zalmoxisus/redux-devtools-extension/tree/master/examples).
## Backers ## Backers
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/redux-devtools-extension#backer)] Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/redux-devtools-extension#backer)]
<a href="https://opencollective.com/redux-devtools-extension/backer/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/0/avatar.svg"></a> <a href="https://opencollective.com/redux-devtools-extension/backer/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/0/avatar.svg"></a>
@ -235,8 +281,8 @@ Support us with a monthly donation and help us continue our activities. [[Become
<a href="https://opencollective.com/redux-devtools-extension/backer/28/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/28/avatar.svg"></a> <a href="https://opencollective.com/redux-devtools-extension/backer/28/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/redux-devtools-extension/backer/29/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/29/avatar.svg"></a> <a href="https://opencollective.com/redux-devtools-extension/backer/29/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/29/avatar.svg"></a>
## Sponsors ## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/redux-devtools-extension#sponsor)] Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/redux-devtools-extension#sponsor)]
<a href="https://opencollective.com/redux-devtools-extension/sponsor/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/0/avatar.svg"></a> <a href="https://opencollective.com/redux-devtools-extension/sponsor/0/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/sponsor/0/avatar.svg"></a>

View File

@ -3,7 +3,7 @@ environment:
- nodejs_version: '6' - nodejs_version: '6'
cache: cache:
- "%LOCALAPPDATA%/Yarn" - '%LOCALAPPDATA%/Yarn'
- node_modules - node_modules
install: install:

View File

@ -1,98 +1,141 @@
# Options # Options
Use with Use with
- `window.__REDUX_DEVTOOLS_EXTENSION__([options])`
- `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__([options])()`
- `window.__REDUX_DEVTOOLS_EXTENSION__.connect([options])`
- `redux-devtools-extension` npm package:
```js
import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools(options); - `window.__REDUX_DEVTOOLS_EXTENSION__([options])`
const store = createStore(reducer, /* preloadedState, */ composeEnhancers( - `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__([options])()`
applyMiddleware(...middleware), - `window.__REDUX_DEVTOOLS_EXTENSION__.connect([options])`
- `redux-devtools-extension` npm package:
```js
import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools(options);
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware)
// other store enhancers if any // other store enhancers if any
)); )
``` );
```
The `options` object is optional, and can include any of the following. The `options` object is optional, and can include any of the following.
### `name` ### `name`
*string* - the instance name to be shown on the monitor page. Default value is `document.title`. If not specified and there's no document title, it will consist of `tabId` and `instanceId`.
_string_ - the instance name to be shown on the monitor page. Default value is `document.title`. If not specified and there's no document title, it will consist of `tabId` and `instanceId`.
### `actionCreators` ### `actionCreators`
*array* or *object* - action creators functions to be available in the Dispatcher. See [the example](https://github.com/zalmoxisus/redux-devtools-extension/commit/477e69d8649dfcdc9bf84dd45605dab7d9775c03).
_array_ or _object_ - action creators functions to be available in the Dispatcher. See [the example](https://github.com/zalmoxisus/redux-devtools-extension/commit/477e69d8649dfcdc9bf84dd45605dab7d9775c03).
### `latency` ### `latency`
*number (in ms)* - if more than one action is dispatched in the indicated interval, all new actions will be collected and sent at once. It is the joint between performance and speed. When set to `0`, all actions will be sent instantly. Set it to a higher value when experiencing perf issues (also `maxAge` to a lower value). Default is `500 ms`.
_number (in ms)_ - if more than one action is dispatched in the indicated interval, all new actions will be collected and sent at once. It is the joint between performance and speed. When set to `0`, all actions will be sent instantly. Set it to a higher value when experiencing perf issues (also `maxAge` to a lower value). Default is `500 ms`.
### `maxAge` ### `maxAge`
*number* (>1) - maximum allowed actions to be stored in the history tree. The oldest actions are removed once maxAge is reached. It's critical for performance. Default is `50`.
_number_ (>1) - maximum allowed actions to be stored in the history tree. The oldest actions are removed once maxAge is reached. It's critical for performance. Default is `50`.
### `trace` ### `trace`
*boolean* or *function* - if set to `true`, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code ([more details](../Features/Trace.md)). You can use a function (with action object as argument) which should return `new Error().stack` string, getting the stack outside of reducers. Default to `false`.
_boolean_ or _function_ - if set to `true`, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code ([more details](../Features/Trace.md)). You can use a function (with action object as argument) which should return `new Error().stack` string, getting the stack outside of reducers. Default to `false`.
### `traceLimit` ### `traceLimit`
*number* - maximum stack trace frames to be stored (in case `trace` option was provided as `true`). By default it's `10`. Note that, because extension's calls are excluded, the resulted frames could be 1 less. If `trace` option is a function, `traceLimit` will have no effect, as it's supposed to be handled there.
_number_ - maximum stack trace frames to be stored (in case `trace` option was provided as `true`). By default it's `10`. Note that, because extension's calls are excluded, the resulted frames could be 1 less. If `trace` option is a function, `traceLimit` will have no effect, as it's supposed to be handled there.
### `serialize` ### `serialize`
*boolean* or *object* which contains:
_boolean_ or _object_ which contains:
- **options** `object or boolean`: - **options** `object or boolean`:
- `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
- `false` - will handle also circular references.
- `true` - will handle also date, regex, undefined, primitives, error objects, symbols, maps, sets and functions.
- object, which contains `date`, `regex`, `undefined`, `nan`, `infinity`, `error`, `symbol`, `map`, `set` and `function` keys. For each of them you can indicate if to include (by setting as `true`). For `function` key you can also specify a custom function which handles serialization. See [`jsan`](https://github.com/kolodny/jsan) for more details. Example:
```js - `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ - `false` - will handle also circular references.
serialize: { - `true` - will handle also date, regex, undefined, primitives, error objects, symbols, maps, sets and functions.
options: { - object, which contains `date`, `regex`, `undefined`, `nan`, `infinity`, `error`, `symbol`, `map`, `set` and `function` keys. For each of them you can indicate if to include (by setting as `true`). For `function` key you can also specify a custom function which handles serialization. See [`jsan`](https://github.com/kolodny/jsan) for more details. Example:
undefined: true,
function: function(fn) { return fn.toString() } ```js
} const store = Redux.createStore(
} reducer,
})); window.__REDUX_DEVTOOLS_EXTENSION__ &&
``` window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
options: {
undefined: true,
function: function (fn) {
return fn.toString();
},
},
},
})
);
```
- **replacer** `function(key, value)` - [JSON `replacer` function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter) used for both actions and states stringify. - **replacer** `function(key, value)` - [JSON `replacer` function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter) used for both actions and states stringify.
Example of usage with [mori data structures](https://github.com/swannodette/mori): Example of usage with [mori data structures](https://github.com/swannodette/mori):
```js ```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ const store = Redux.createStore(
serialize: { reducer,
replacer: (key, value) => value && mori.isMap(value) ? mori.toJs(value) : value window.__REDUX_DEVTOOLS_EXTENSION__ &&
} window.__REDUX_DEVTOOLS_EXTENSION__({
})); serialize: {
replacer: (key, value) =>
value && mori.isMap(value) ? mori.toJs(value) : value,
},
})
);
``` ```
In addition, you can specify a data type by adding a [`__serializedType__`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/helpers/index.js#L4) key. So you can deserialize it back while importing or persisting data. Moreover, it will also [show a nice preview showing the provided custom type](https://cloud.githubusercontent.com/assets/7957859/21814330/a17d556a-d761-11e6-85ef-159dd12f36c5.png): In addition, you can specify a data type by adding a [`__serializedType__`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/helpers/index.js#L4) key. So you can deserialize it back while importing or persisting data. Moreover, it will also [show a nice preview showing the provided custom type](https://cloud.githubusercontent.com/assets/7957859/21814330/a17d556a-d761-11e6-85ef-159dd12f36c5.png):
```js ```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ const store = Redux.createStore(
serialize: { reducer,
replacer: (key, value) => { window.__REDUX_DEVTOOLS_EXTENSION__ &&
if (Immutable.List.isList(value)) { // use your custom data type checker window.__REDUX_DEVTOOLS_EXTENSION__({
return { serialize: {
data: value.toArray(), // ImmutableJS custom method to get JS data as array replacer: (key, value) => {
__serializedType__: 'ImmutableList' // mark you custom data type to show and retrieve back if (Immutable.List.isList(value)) {
} // use your custom data type checker
} return {
} data: value.toArray(), // ImmutableJS custom method to get JS data as array
} __serializedType__: 'ImmutableList', // mark you custom data type to show and retrieve back
})); };
}
},
},
})
);
``` ```
- **reviver** `function(key, value)` - [JSON `reviver` function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) used for parsing the imported actions and states. See [`remotedev-serialize`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/immutable/serialize.js#L8-L41) as an example on how to serialize special data types and get them back: - **reviver** `function(key, value)` - [JSON `reviver` function](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Using_the_reviver_parameter) used for parsing the imported actions and states. See [`remotedev-serialize`](https://github.com/zalmoxisus/remotedev-serialize/blob/master/immutable/serialize.js#L8-L41) as an example on how to serialize special data types and get them back:
```js ```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ const store = Redux.createStore(
serialize: { reducer,
reviver: (key, value) => { window.__REDUX_DEVTOOLS_EXTENSION__ &&
if (typeof value === 'object' && value !== null && '__serializedType__' in value) { window.__REDUX_DEVTOOLS_EXTENSION__({
switch (value.__serializedType__) { serialize: {
case 'ImmutableList': return Immutable.List(value.data); reviver: (key, value) => {
} if (
} typeof value === 'object' &&
} value !== null &&
} '__serializedType__' in value
})); ) {
switch (value.__serializedType__) {
case 'ImmutableList':
return Immutable.List(value.data);
}
}
},
},
})
);
``` ```
- **immutable** `object` - automatically serialize/deserialize immutablejs via [remotedev-serialize](https://github.com/zalmoxisus/remotedev-serialize). Just pass the Immutable library like so: - **immutable** `object` - automatically serialize/deserialize immutablejs via [remotedev-serialize](https://github.com/zalmoxisus/remotedev-serialize). Just pass the Immutable library like so:
@ -101,118 +144,164 @@ The `options` object is optional, and can include any of the following.
import Immutable from 'immutable'; // https://facebook.github.io/immutable-js/ import Immutable from 'immutable'; // https://facebook.github.io/immutable-js/
// ... // ...
// Like above, only showing off compose this time. Reminder you might not want this in prod. // Like above, only showing off compose this time. Reminder you might not want this in prod.
const composeEnhancers = typeof window === 'object' && typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ !== 'undefined' ? const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ typeof window === 'object' &&
serialize: { typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ !== 'undefined'
immutable: Immutable ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
} serialize: {
}) : compose; immutable: Immutable,
},
})
: compose;
``` ```
It will support all ImmutableJS structures. You can even export them into a file and get them back. The only exception is `Record` class, for which you should pass in addition the references to your classes in `refs`. It will support all ImmutableJS structures. You can even export them into a file and get them back. The only exception is `Record` class, for which you should pass in addition the references to your classes in `refs`.
- **refs** `array` - ImmutableJS `Record` classes used to make possible restore its instances back when importing, persisting... Example of usage: - **refs** `array` - ImmutableJS `Record` classes used to make possible restore its instances back when importing, persisting... Example of usage:
``` js
```js
import Immutable from 'immutable'; import Immutable from 'immutable';
// ... // ...
const ABRecord = Immutable.Record({ a:1, b:2 }); const ABRecord = Immutable.Record({ a: 1, b: 2 });
const myRecord = new ABRecord({ b:3 }); // used in the reducers const myRecord = new ABRecord({ b: 3 }); // used in the reducers
const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ const store = createStore(
serialize: { rootReducer,
immutable: Immutable, window.__REDUX_DEVTOOLS_EXTENSION__ &&
refs: [ABRecord] window.__REDUX_DEVTOOLS_EXTENSION__({
} serialize: {
})); immutable: Immutable,
refs: [ABRecord],
},
})
);
``` ```
Also you can specify alternative values right in the state object (in the initial state of the reducer) by adding `toJSON` function: Also you can specify alternative values right in the state object (in the initial state of the reducer) by adding `toJSON` function:
In the example bellow it will always send `{ component: '[React]' }`, regardless of the state's `component` value (useful when you don't want to send lots of unnecessary data): In the example bellow it will always send `{ component: '[React]' }`, regardless of the state's `component` value (useful when you don't want to send lots of unnecessary data):
```js ```js
function component( function component(
state = { component: null, toJSON: () => ({ component: '[React]' }) }, state = { component: null, toJSON: () => ({ component: '[React]' }) },
action action
) { ) {
switch (action.type) { switch (action.type) {
case 'ADD_COMPONENT': return { component: action.component }; case 'ADD_COMPONENT':
default: return state; return { component: action.component };
default:
return state;
} }
} }
``` ```
You could also alter the value. For example when state is `{ count: 1 }`, we'll send `{ counter: 10 }` (notice we don't have an arrow function this time to use the object's `this`): You could also alter the value. For example when state is `{ count: 1 }`, we'll send `{ counter: 10 }` (notice we don't have an arrow function this time to use the object's `this`):
```js ```js
function counter( function counter(
state = { count: 0, toJSON: function (){ return { conter: this.count * 10 }; } }, state = {
count: 0,
toJSON: function () {
return { conter: this.count * 10 };
},
},
action action
) { ) {
switch (action.type) { switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 }; case 'INCREMENT':
default: return state; return { count: state.count + 1 };
default:
return state;
} }
} }
``` ```
### `actionSanitizer` / `stateSanitizer` ### `actionSanitizer` / `stateSanitizer`
- **actionSanitizer** (*function*) - function which takes `action` object and id number as arguments, and should return `action` object back. See the example bellow.
- **stateSanitizer** (*function*) - function which takes `state` object and index as arguments, and should return `state` object back. - **actionSanitizer** (_function_) - function which takes `action` object and id number as arguments, and should return `action` object back. See the example bellow.
- **stateSanitizer** (_function_) - function which takes `state` object and index as arguments, and should return `state` object back.
Example of usage: Example of usage:
```js ```js
const actionSanitizer = (action) => ( const actionSanitizer = (action) =>
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data ? action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
{ ...action, data: '<<LONG_BLOB>>' } : action ? { ...action, data: '<<LONG_BLOB>>' }
: action;
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
actionSanitizer,
stateSanitizer: (state) =>
state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
})
); );
const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
actionSanitizer,
stateSanitizer: (state) => state.data ? { ...state, data: '<<LONG_BLOB>>' } : state
}));
``` ```
### `actionsBlacklist` / `actionsWhitelist` ### `actionsBlacklist` / `actionsWhitelist`
*string or array of strings as regex* - actions types to be hidden / shown in the monitors (while passed to the reducers). If `actionsWhitelist` specified, `actionsBlacklist` is ignored.
_string or array of strings as regex_ - actions types to be hidden / shown in the monitors (while passed to the reducers). If `actionsWhitelist` specified, `actionsBlacklist` is ignored.
Example: Example:
```js ```js
createStore(reducer, remotedev({ createStore(
sendTo: 'http://localhost:8000', reducer,
actionsBlacklist: 'SOME_ACTION' remotedev({
// or actionsBlacklist: ['SOME_ACTION', 'SOME_OTHER_ACTION'] sendTo: 'http://localhost:8000',
// or just actionsBlacklist: 'SOME_' to omit both actionsBlacklist: 'SOME_ACTION',
})) // or actionsBlacklist: ['SOME_ACTION', 'SOME_OTHER_ACTION']
// or just actionsBlacklist: 'SOME_' to omit both
})
);
``` ```
### `predicate` ### `predicate`
*function* - called for every action before sending, takes `state` and `action` object, and returns `true` in case it allows sending the current data to the monitor. Use it as a more advanced version of `actionsBlacklist`/`actionsWhitelist` parameters.
_function_ - called for every action before sending, takes `state` and `action` object, and returns `true` in case it allows sending the current data to the monitor. Use it as a more advanced version of `actionsBlacklist`/`actionsWhitelist` parameters.
Example of usage: Example of usage:
```js ```js
const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ const store = createStore(
predicate: (state, action) => state.dev.logLevel === VERBOSE && !action.forwarded rootReducer,
})); window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
predicate: (state, action) =>
state.dev.logLevel === VERBOSE && !action.forwarded,
})
);
``` ```
### `shouldRecordChanges` ### `shouldRecordChanges`
*boolean* - if specified as `false`, it will not record the changes till clicking on `Start recording` button. Default is `true`. Available only for Redux enhancer, for others use `autoPause`.
_boolean_ - if specified as `false`, it will not record the changes till clicking on `Start recording` button. Default is `true`. Available only for Redux enhancer, for others use `autoPause`.
### `pauseActionType` ### `pauseActionType`
*string* - if specified, whenever clicking on `Pause recording` button and there are actions in the history log, will add this action type. If not specified, will commit when paused. Available only for Redux enhancer. Default is `@@PAUSED`.
_string_ - if specified, whenever clicking on `Pause recording` button and there are actions in the history log, will add this action type. If not specified, will commit when paused. Available only for Redux enhancer. Default is `@@PAUSED`.
### `autoPause` ### `autoPause`
*boolean* - auto pauses when the extensions window is not opened, and so has zero impact on your app when not in use. Not available for Redux enhancer (as it already does it but storing the data to be sent). Default is `false`.
_boolean_ - auto pauses when the extensions window is not opened, and so has zero impact on your app when not in use. Not available for Redux enhancer (as it already does it but storing the data to be sent). Default is `false`.
### `shouldStartLocked` ### `shouldStartLocked`
*boolean* - if specified as `true`, it will not allow any non-monitor actions to be dispatched till clicking on `Unlock changes` button. Available only for Redux enhancer. Default is `false`.
_boolean_ - if specified as `true`, it will not allow any non-monitor actions to be dispatched till clicking on `Unlock changes` button. Available only for Redux enhancer. Default is `false`.
### `shouldHotReload` ### `shouldHotReload`
*boolean* - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Available only for Redux enhancer. Default to `true`.
_boolean_ - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Available only for Redux enhancer. Default to `true`.
### `shouldCatchErrors` ### `shouldCatchErrors`
*boolean* - if specified as `true`, whenever there's an exception in reducers, the monitors will show the error message, and next actions will not be dispatched.
_boolean_ - if specified as `true`, whenever there's an exception in reducers, the monitors will show the error message, and next actions will not be dispatched.
### `features` ### `features`
If you want to restrict the extension, just specify the features you allow: If you want to restrict the extension, just specify the features you allow:
```js ```js
const composeEnhancers = composeWithDevTools({ const composeEnhancers = composeWithDevTools({
features: { features: {
@ -225,10 +314,11 @@ const composeEnhancers = composeWithDevTools({
skip: true, // skip (cancel) actions skip: true, // skip (cancel) actions
reorder: true, // drag and drop actions in the history list reorder: true, // drag and drop actions in the history list
dispatch: true, // dispatch custom actions or action creators dispatch: true, // dispatch custom actions or action creators
test: true // generate tests for the selected actions test: true, // generate tests for the selected actions
}, },
// other options like actionSanitizer, stateSanitizer // other options like actionSanitizer, stateSanitizer
}); });
``` ```
If not specified, all of the features are enabled. When set as an object, only those included as `true` will be allowed. If not specified, all of the features are enabled. When set as an object, only those included as `true` will be allowed.
Note that except `true`/`false`, `import` and `export` can be set as `custom` (which is by default for Redux enhancer), meaning that the importing/exporting occurs on the client side. Otherwise, you'll get/set the data right from the monitor part. Note that except `true`/`false`, `import` and `export` can be set as `custom` (which is by default for Redux enhancer), meaning that the importing/exporting occurs on the client side. Otherwise, you'll get/set the data right from the monitor part.

View File

@ -12,14 +12,16 @@ Use the following methods of `window.__REDUX_DEVTOOLS_EXTENSION__`:
- [notifyErrors](#notifyerrors) - [notifyErrors](#notifyerrors)
<a id="connect"></a> <a id="connect"></a>
### connect([options]) ### connect([options])
##### Arguments ##### Arguments
- [`options`] *Object* - [see the available options](Arguments.md). - [`options`] _Object_ - [see the available options](Arguments.md).
##### Returns ##### Returns
*Object* containing the following methods:
_Object_ containing the following methods:
- `subscribe(listener)` - adds a change listener. It will be called any time an action is dispatched from the monitor. Returns a function to unsubscribe the current listener. - `subscribe(listener)` - adds a change listener. It will be called any time an action is dispatched from the monitor. Returns a function to unsubscribe the current listener.
- `unsubscribe()` - unsubscribes all listeners. - `unsubscribe()` - unsubscribes all listeners.
@ -37,7 +39,7 @@ devTools.subscribe((message) => {
} }
}); });
devTools.init({ value: 'initial state' }); devTools.init({ value: 'initial state' });
devTools.send('change state', { value: 'state changed' }) devTools.send('change state', { value: 'state changed' });
``` ```
See [redux enhancer's example](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/logOnly.js), [react example](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/examples/react-counter-messaging/components/Counter.js) and [blog post](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f) for more details. See [redux enhancer's example](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/npm-package/logOnly.js), [react example](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/examples/react-counter-messaging/components/Counter.js) and [blog post](https://medium.com/@zalmoxis/redux-devtools-without-redux-or-how-to-have-a-predictable-state-with-any-architecture-61c5f5a7716f) for more details.
@ -47,41 +49,45 @@ See [redux enhancer's example](https://github.com/zalmoxisus/redux-devtools-exte
Remove extensions listener and disconnect extensions background script connection. Usually just unsubscribing the listener inside the `connect` is enough. Remove extensions listener and disconnect extensions background script connection. Usually just unsubscribing the listener inside the `connect` is enough.
<a id="send"></a> <a id="send"></a>
### send(action, state, [options, instanceId]) ### send(action, state, [options, instanceId])
Send a new action and state manually to be shown on the monitor. It's recommended to use [`connect`](connect), unless you want to hook into an already created instance. Send a new action and state manually to be shown on the monitor. It's recommended to use [`connect`](connect), unless you want to hook into an already created instance.
##### Arguments ##### Arguments
- `action` *String* (action type) or *Object* with required `type` key. - `action` _String_ (action type) or _Object_ with required `type` key.
- `state` *any* - usually object to expand. - `state` _any_ - usually object to expand.
- [`options`] *Object* - [see the available options](Arguments.md). - [`options`] _Object_ - [see the available options](Arguments.md).
- [`instanceId`] *String* - instance id for which to include the log. If not specified and not present in the `options` object, will be the first available instance. - [`instanceId`] _String_ - instance id for which to include the log. If not specified and not present in the `options` object, will be the first available instance.
<a id="listen"></a> <a id="listen"></a>
### listen(onMessage, instanceId) ### listen(onMessage, instanceId)
Listen for messages dispatched for specific `instanceId`. For most cases it's better to use `subcribe` inside the [`connect`](connect). Listen for messages dispatched for specific `instanceId`. For most cases it's better to use `subcribe` inside the [`connect`](connect).
##### Arguments ##### Arguments
- `onMessage` *Function* to call when there's an action from the monitor. - `onMessage` _Function_ to call when there's an action from the monitor.
- `instanceId` *String* - instance id for which to handle actions. - `instanceId` _String_ - instance id for which to handle actions.
<a id="open"></a> <a id="open"></a>
### open([position]) ### open([position])
Open the extension's window. This should be conditional (usually you don't need to open extension's window automatically). Open the extension's window. This should be conditional (usually you don't need to open extension's window automatically).
##### Arguments ##### Arguments
- [`position`] *String* - window position: `left`, `right`, `bottom`. Also can be `panel` to [open it in a Chrome panel](../FAQ.md#how-to-keep-devtools-window-focused-all-the-time-in-a-chrome-panel). Or `remote` to [open remote monitor](../FAQ.md#how-to-get-it-work-with-webworkers-react-native-hybrid-desktop-and-server-side-apps). By default is `left`. - [`position`] _String_ - window position: `left`, `right`, `bottom`. Also can be `panel` to [open it in a Chrome panel](../FAQ.md#how-to-keep-devtools-window-focused-all-the-time-in-a-chrome-panel). Or `remote` to [open remote monitor](../FAQ.md#how-to-get-it-work-with-webworkers-react-native-hybrid-desktop-and-server-side-apps). By default is `left`.
<a id="notifyErrors"></a> <a id="notifyErrors"></a>
### notifyErrors([onError]) ### notifyErrors([onError])
When called, the extension will listen for uncaught exceptions on the page, and, if any, will show native notifications. Optionally, you can provide a function to be called when an exception occurs. When called, the extension will listen for uncaught exceptions on the page, and, if any, will show native notifications. Optionally, you can provide a function to be called when an exception occurs.
##### Arguments ##### Arguments
- [`onError`] *Function* to call when there's an exceptions. - [`onError`] _Function_ to call when there's an exceptions.

View File

@ -1,11 +1,11 @@
# Credits # Credits
- Built using [Crossbuilder](https://github.com/zalmoxisus/crossbuilder) boilerplate. - Built using [Crossbuilder](https://github.com/zalmoxisus/crossbuilder) boilerplate.
- Includes [Dan Abramov](https://github.com/gaearon)'s [redux-devtools](https://github.com/gaearon/redux-devtools) and the following monitors: - Includes [Dan Abramov](https://github.com/gaearon)'s [redux-devtools](https://github.com/gaearon/redux-devtools) and the following monitors:
- [Log Monitor](https://github.com/gaearon/redux-devtools-log-monitor) - [Log Monitor](https://github.com/gaearon/redux-devtools-log-monitor)
- [Inspector](https://github.com/alexkuz/redux-devtools-inspector) - [Inspector](https://github.com/alexkuz/redux-devtools-inspector)
- [Dispatch](https://github.com/YoruNoHikage/redux-devtools-dispatch) - [Dispatch](https://github.com/YoruNoHikage/redux-devtools-dispatch)
- [Slider](https://github.com/calesce/redux-slider-monitor) - [Slider](https://github.com/calesce/redux-slider-monitor)
- [Chart](https://github.com/romseguy/redux-devtools-chart-monitor) - [Chart](https://github.com/romseguy/redux-devtools-chart-monitor)
- [The logo icon](https://github.com/reactjs/redux/issues/151) made by [Keith Yong](https://github.com/keithyong) . - [The logo icon](https://github.com/reactjs/redux/issues/151) made by [Keith Yong](https://github.com/keithyong) .
- Examples from [Redux](https://github.com/rackt/redux/tree/master/examples). - Examples from [Redux](https://github.com/rackt/redux/tree/master/examples).

View File

@ -1,6 +1,7 @@
# Redux DevTools Extension FAQ # Redux DevTools Extension FAQ
## Table of Contents ## Table of Contents
- [How to get it work](#how-to-get-it-work) - [How to get it work](#how-to-get-it-work)
- [How to disable/enable it in production](#how-to-disable-it-in-production) - [How to disable/enable it in production](#how-to-disable-it-in-production)
- [How to persist debug sessions across page reloads](#how-to-persist-debug-sessions-across-page-reloads) - [How to persist debug sessions across page reloads](#how-to-persist-debug-sessions-across-page-reloads)
@ -10,22 +11,35 @@
- [Keyboard shortcuts](#keyboard-shortcuts) - [Keyboard shortcuts](#keyboard-shortcuts)
#### How to get it work #### How to get it work
- Check the extension with [Counter](http://zalmoxisus.github.io/examples/counter/) or [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/) demo. - Check the extension with [Counter](http://zalmoxisus.github.io/examples/counter/) or [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/) demo.
- Reload the extension on the extensions page (`chrome://extensions/`). - Reload the extension on the extensions page (`chrome://extensions/`).
- If something goes wrong, [open an issue](https://github.com/zalmoxisus/redux-devtools-extension/issues) or tweet me: [@mdiordiev](https://twitter.com/mdiordiev). - If something goes wrong, [open an issue](https://github.com/zalmoxisus/redux-devtools-extension/issues) or tweet me: [@mdiordiev](https://twitter.com/mdiordiev).
#### How to disable it in production #### How to disable it in production
Usually you don't have to. See [the article for details on how to include it in production](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f). Usually you don't have to. See [the article for details on how to include it in production](https://medium.com/@zalmoxis/using-redux-devtools-in-production-4c5b56c5600f).
#### How to persist debug sessions across page reloads #### How to persist debug sessions across page reloads
Just click the `Persist` button or add `?debug_session=<session_name>` to the url. Just click the `Persist` button or add `?debug_session=<session_name>` to the url.
#### How to open DevTools programmatically #### How to open DevTools programmatically
```js ```js
window.__REDUX_DEVTOOLS_EXTENSION__.open(); window.__REDUX_DEVTOOLS_EXTENSION__.open();
``` ```
Make sure to have it conditionally. Auto opening windows is a bad DX. See the [API](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Methods.md#open) for details. Make sure to have it conditionally. Auto opening windows is a bad DX. See the [API](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Methods.md#open) for details.
#### How to enable/disable errors notifying #### How to enable/disable errors notifying
Just find `Redux DevTools` on the extensions page (`chrome://extensions/`) and click the `Options` link to customize everything. The errors notifying is disabled by default. If enabled, it works only when the store enhancer is called (in order not to show notifications for any sites you visit). In case you want notifications for a non-redux app, init it explicitly by calling `window.__REDUX_DEVTOOLS_EXTENSION__.notifyErrors()` (probably you'll check if `window.__REDUX_DEVTOOLS_EXTENSION__` exists before calling it). Just find `Redux DevTools` on the extensions page (`chrome://extensions/`) and click the `Options` link to customize everything. The errors notifying is disabled by default. If enabled, it works only when the store enhancer is called (in order not to show notifications for any sites you visit). In case you want notifications for a non-redux app, init it explicitly by calling `window.__REDUX_DEVTOOLS_EXTENSION__.notifyErrors()` (probably you'll check if `window.__REDUX_DEVTOOLS_EXTENSION__` exists before calling it).
#### How to get it work with WebWorkers, React Native, hybrid, desktop and server side apps #### How to get it work with WebWorkers, React Native, hybrid, desktop and server side apps
It is not possible to inject extension's script there and to communicate directly. To solve this, use [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools). After including it inside the app, click `Remote` button for remote monitoring. It is not possible to inject extension's script there and to communicate directly. To solve this, use [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools). After including it inside the app, click `Remote` button for remote monitoring.
#### Keyboard shortcuts #### Keyboard shortcuts
To set/change the keyboard shortcuts, click "Keyboard shortcuts" button on the bottom of the extensions page (`chrome://extensions/`). By default only `Cmd` (`Ctrl`) + `Shift` + `E` is available, which will open the extension popup (only when the Redux store is available in the current page). To set/change the keyboard shortcuts, click "Keyboard shortcuts" button on the bottom of the extensions page (`chrome://extensions/`). By default only `Cmd` (`Ctrl`) + `Shift` + `E` is available, which will open the extension popup (only when the Redux store is available in the current page).

View File

@ -1,6 +1,7 @@
# Integrations for js and non-js frameworks # Integrations for js and non-js frameworks
Mostly functional: Mostly functional:
- [React](#react) - [React](#react)
- [Angular](#angular) - [Angular](#angular)
- [Cycle](#cycle) - [Cycle](#cycle)
@ -13,26 +14,32 @@ Mostly functional:
- [Aurelia](#aurelia) - [Aurelia](#aurelia)
In progress: In progress:
- [ClojureScript](#clojurescript) - [ClojureScript](#clojurescript)
- [Horizon](#horizon) - [Horizon](#horizon)
- [Python](#python) - [Python](#python)
- [Swift](#swift) - [Swift](#swift)
### [React](https://github.com/facebook/react) ### [React](https://github.com/facebook/react)
#### Inspect React props #### Inspect React props
##### [`react-inspect-props`](https://github.com/lucasconstantino/react-inspect-props) ##### [`react-inspect-props`](https://github.com/lucasconstantino/react-inspect-props)
```js ```js
import { compose, withState } from 'recompose' import { compose, withState } from 'recompose';
import { inspectProps } from 'react-inspect-props' import { inspectProps } from 'react-inspect-props';
compose( compose(
withState('count', 'setCount', 0), withState('count', 'setCount', 0),
inspectProps('Counter inspector') inspectProps('Counter inspector')
)(Counter) )(Counter);
``` ```
#### Inspect React states #### Inspect React states
##### [`remotedev-react-state`](https://github.com/jhen0409/remotedev-react-state) ##### [`remotedev-react-state`](https://github.com/jhen0409/remotedev-react-state)
```js ```js
import connectToDevTools from 'remotedev-react-state' import connectToDevTools from 'remotedev-react-state'
@ -43,18 +50,22 @@ componentWillMount() {
``` ```
#### Inspect React hooks (useState and useReducer) #### Inspect React hooks (useState and useReducer)
##### [`reinspect`](https://github.com/troch/reinspect) ##### [`reinspect`](https://github.com/troch/reinspect)
```js ```js
import { useState } from 'reinspect' import { useState } from 'reinspect';
export function CounterWithUseState({ id }) { export function CounterWithUseState({ id }) {
const [count, setCount] = useState(0, id) const [count, setCount] = useState(0, id);
// ... // ...
} }
``` ```
### [Mobx](https://github.com/mobxjs/mobx) ### [Mobx](https://github.com/mobxjs/mobx)
#### [`mobx-remotedev`](https://github.com/zalmoxisus/mobx-remotedev) #### [`mobx-remotedev`](https://github.com/zalmoxisus/mobx-remotedev)
```js ```js
import remotedev from 'mobx-remotedev'; import remotedev from 'mobx-remotedev';
// or import remotedev from 'mobx-remotedev/lib/dev' // or import remotedev from 'mobx-remotedev/lib/dev'
@ -66,14 +77,16 @@ const appStore = observable({
// Or // Or
class appStore { class appStore {
// ... // ...
} }
export default remotedev(appStore); export default remotedev(appStore);
```` ```
### [Angular](https://github.com/angular/angular) ### [Angular](https://github.com/angular/angular)
#### [ng2-redux](https://github.com/angular-redux/ng2-redux) #### [ng2-redux](https://github.com/angular-redux/ng2-redux)
```js ```js
import { NgReduxModule, NgRedux, DevToolsExtension } from 'ng2-redux'; import { NgReduxModule, NgRedux, DevToolsExtension } from 'ng2-redux';
@ -101,9 +114,11 @@ import { NgReduxModule, NgRedux, DevToolsExtension } from 'ng2-redux';
} }
} }
``` ```
For Angular 1 see [ng-redux](https://github.com/angular-redux/ng-redux). For Angular 1 see [ng-redux](https://github.com/angular-redux/ng-redux).
#### [Angular @ngrx/store](https://ngrx.io/) + [`@ngrx/store-devtools`](https://ngrx.io/guide/store-devtools) #### [Angular @ngrx/store](https://ngrx.io/) + [`@ngrx/store-devtools`](https://ngrx.io/guide/store-devtools)
```js ```js
import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@ -112,55 +127,71 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
StoreModule.forRoot(rootReducer), StoreModule.forRoot(rootReducer),
// Instrumentation must be imported after importing StoreModule (config is optional) // Instrumentation must be imported after importing StoreModule (config is optional)
StoreDevtoolsModule.instrument({ StoreDevtoolsModule.instrument({
maxAge: 5 maxAge: 5,
}) }),
] ],
}) })
export class AppModule { } export class AppModule {}
``` ```
[`Example of integration`](https://github.com/ngrx/platform/tree/master/projects/example-app/) ([live demo](https://ngrx.github.io/platform/example-app/)). [`Example of integration`](https://github.com/ngrx/platform/tree/master/projects/example-app/) ([live demo](https://ngrx.github.io/platform/example-app/)).
### [Ember](http://emberjs.com/) ### [Ember](http://emberjs.com/)
#### [`ember-redux`](https://github.com/ember-redux/ember-redux) #### [`ember-redux`](https://github.com/ember-redux/ember-redux)
```js ```js
//app/enhancers/index.js //app/enhancers/index.js
import { compose } from 'redux'; import { compose } from 'redux';
var devtools = window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f; var devtools = window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (f) => f;
export default compose(devtools); export default compose(devtools);
``` ```
### [Cycle](https://github.com/cyclejs/cyclejs) ### [Cycle](https://github.com/cyclejs/cyclejs)
#### [`@culli/store`](https://github.com/milankinen/culli/tree/master/packages/store)
```js
import {run} from "@cycle/most-run"
import {makeDOMDriver as DOM} from "@cycle/dom"
import Store, {ReduxDevtools} from "@culli/store"
import App, {newId} from "./App"
#### [`@culli/store`](https://github.com/milankinen/culli/tree/master/packages/store)
```js
import { run } from '@cycle/most-run';
import { makeDOMDriver as DOM } from '@cycle/dom';
import Store, { ReduxDevtools } from '@culli/store';
import App, { newId } from './App';
run(App, { run(App, {
DOM: DOM("#app"), DOM: DOM('#app'),
Store: Store(ReduxDevtools({items: [{id: newId(), num: 0}, {id: newId(), num: 0}]})) Store: Store(
}) ReduxDevtools({
items: [
{ id: newId(), num: 0 },
{ id: newId(), num: 0 },
],
})
),
});
``` ```
### [Freezer](https://github.com/arqex/freezer) ### [Freezer](https://github.com/arqex/freezer)
#### [`freezer-redux-devtools`](https://github.com/arqex/freezer-redux-devtools) #### [`freezer-redux-devtools`](https://github.com/arqex/freezer-redux-devtools)
```js ```js
import React, { Component } from 'react'; import React, { Component } from 'react';
import { supportChromeExtension } from 'freezer-redux-devtools/freezer-redux-middleware'; import { supportChromeExtension } from 'freezer-redux-devtools/freezer-redux-middleware';
import Freezer from 'freezer-js'; import Freezer from 'freezer-js';
// Our state is a freezer object // Our state is a freezer object
var State = new Freezer({hello: 'world'}); var State = new Freezer({ hello: 'world' });
// Enable the extension // Enable the extension
supportChromeExtension( State ); supportChromeExtension(State);
``` ```
### [Horizon](https://github.com/rethinkdb/horizon) ### [Horizon](https://github.com/rethinkdb/horizon)
#### [`horizon-remotedev`](https://github.com/zalmoxisus/horizon-remotedev) #### [`horizon-remotedev`](https://github.com/zalmoxisus/horizon-remotedev)
```js ```js
// import hzRemotedev from 'horizon-remotedev'; // import hzRemotedev from 'horizon-remotedev';
// or import hzRemotedev from 'horizon-remotedev/lib/dev' // or import hzRemotedev from 'horizon-remotedev/lib/dev'
@ -171,11 +202,13 @@ const horizon = Horizon();
// ... // ...
// Specify the horizon instance to monitor // Specify the horizon instance to monitor
hzRemotedev(horizon("react_messages")) hzRemotedev(horizon('react_messages'));
``` ```
### [Fable](https://github.com/fable-compiler/Fable) ### [Fable](https://github.com/fable-compiler/Fable)
#### [`fable-elmish/debugger`](https://github.com/fable-elmish/debugger) #### [`fable-elmish/debugger`](https://github.com/fable-elmish/debugger)
```fsharp ```fsharp
open Elmish.Debug open Elmish.Debug
@ -196,18 +229,25 @@ Program.mkProgram init update view
``` ```
### [PureScript](https://github.com/purescript/purescript) ### [PureScript](https://github.com/purescript/purescript)
#### [`purescript-react-redux`](https://github.com/ethul/purescript-react-redux) #### [`purescript-react-redux`](https://github.com/ethul/purescript-react-redux)
[`Example of integration`](https://github.com/ethul/purescript-react-redux-example). [`Example of integration`](https://github.com/ethul/purescript-react-redux-example).
### [ClojureScript](https://github.com/clojure/clojurescript) ### [ClojureScript](https://github.com/clojure/clojurescript)
[`Example of integration`](http://gitlab.xet.ru:9999/publicpr/clojurescript-redux/tree/master#dev-setup) [`Example of integration`](http://gitlab.xet.ru:9999/publicpr/clojurescript-redux/tree/master#dev-setup)
### [Python](https://www.python.org/) ### [Python](https://www.python.org/)
#### [`pyredux`](https://github.com/peterpeter5/pyredux) #### [`pyredux`](https://github.com/peterpeter5/pyredux)
[WIP](https://github.com/zalmoxisus/remotedev-server/issues/34) [WIP](https://github.com/zalmoxisus/remotedev-server/issues/34)
### [Swift](https://github.com/apple/swift) ### [Swift](https://github.com/apple/swift)
#### [`katanaMonitor`](https://github.com/bolismauro/katanaMonitor-lib-swift) for [`katana-swift`](https://github.com/BendingSpoons/katana-swift) #### [`katanaMonitor`](https://github.com/bolismauro/katanaMonitor-lib-swift) for [`katana-swift`](https://github.com/BendingSpoons/katana-swift)
```swift ```swift
import KatanaMonitor import KatanaMonitor
@ -221,7 +261,9 @@ middleware.append(MonitorMiddleware.create(using: .defaultConfiguration))
``` ```
### [Reductive](https://github.com/reasonml-community/reductive) ### [Reductive](https://github.com/reasonml-community/reductive)
#### [`reductive-dev-tools`](https://github.com/ambientlight/reductive-dev-tools) #### [`reductive-dev-tools`](https://github.com/ambientlight/reductive-dev-tools)
```reason ```reason
let storeEnhancer = let storeEnhancer =
ReductiveDevTools.( ReductiveDevTools.(
@ -234,7 +276,9 @@ let storeCreator = storeEnhancer @@ Reductive.Store.create;
``` ```
### [Aurelia](http://aurelia.io) ### [Aurelia](http://aurelia.io)
#### [`aurelia-store`](https://aurelia.io/docs/plugins/store) #### [`aurelia-store`](https://aurelia.io/docs/plugins/store)
```ts ```ts
import {Aurelia} from 'aurelia-framework'; import {Aurelia} from 'aurelia-framework';
import {initialState} from './state'; import {initialState} from './state';

View File

@ -1,21 +1,21 @@
# Documentation # Documentation
* [Extension](/README.md) - [Extension](/README.md)
* [Installation](/README.md#installation) - [Installation](/README.md#installation)
* [Usage](/README.md#usage) - [Usage](/README.md#usage)
* [Demo](/README.md#demo) - [Demo](/README.md#demo)
* [API Reference](/docs/API/README.md) - [API Reference](/docs/API/README.md)
* [Options (arguments)](/docs/API/Arguments.md) - [Options (arguments)](/docs/API/Arguments.md)
* [Methods (advanced API)](/docs/API/Methods.md) - [Methods (advanced API)](/docs/API/Methods.md)
* Features - Features
* [Trace actions calls](/docs/Features/Trace.md) - [Trace actions calls](/docs/Features/Trace.md)
* [Integrations](/docs/Integrations.md) - [Integrations](/docs/Integrations.md)
* [FAQ](/docs/FAQ.md) - [FAQ](/docs/FAQ.md)
* [Troubleshooting](/docs/Troubleshooting.md) - [Troubleshooting](/docs/Troubleshooting.md)
* [Recipes](/docs/Recipes.md) - [Recipes](/docs/Recipes.md)
* [Articles](/docs/Articles.md) - [Articles](/docs/Articles.md)
* [Videos](/docs/Videos.md) - [Videos](/docs/Videos.md)
* [Credits](/docs/Credits.md) - [Credits](/docs/Credits.md)
* [Support us](/README.md#backers) - [Support us](/README.md#backers)
* [Feedback](/docs/Feedback.md) - [Feedback](/docs/Feedback.md)
* [Change Log](https://github.com/zalmoxisus/redux-devtools-extension/releases) - [Change Log](https://github.com/zalmoxisus/redux-devtools-extension/releases)

View File

@ -12,37 +12,42 @@ const store = createStore(
(window as any).__REDUX_DEVTOOLS_EXTENSION__() (window as any).__REDUX_DEVTOOLS_EXTENSION__()
); );
``` ```
Note that you many need to set `no-any` to false in your `tslint.json` file. Note that you many need to set `no-any` to false in your `tslint.json` file.
Alternatively you can use typeguard in order to avoid Alternatively you can use typeguard in order to avoid
casting to any. casting to any.
```typescript ```typescript
import { createStore, StoreEnhancer } from "redux"; import { createStore, StoreEnhancer } from 'redux';
// ... // ...
type WindowWithDevTools = Window & { type WindowWithDevTools = Window & {
__REDUX_DEVTOOLS_EXTENSION__: () => StoreEnhancer<unknown, {}> __REDUX_DEVTOOLS_EXTENSION__: () => StoreEnhancer<unknown, {}>;
} };
const isReduxDevtoolsExtenstionExist = const isReduxDevtoolsExtenstionExist = (
(arg: Window | WindowWithDevTools): arg: Window | WindowWithDevTools
arg is WindowWithDevTools => { ): arg is WindowWithDevTools => {
return '__REDUX_DEVTOOLS_EXTENSION__' in arg; return '__REDUX_DEVTOOLS_EXTENSION__' in arg;
} };
// ... // ...
const store = createStore(rootReducer, initialState, const store = createStore(
isReduxDevtoolsExtenstionExist(window) ? rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__() : undefined) initialState,
isReduxDevtoolsExtenstionExist(window)
? window.__REDUX_DEVTOOLS_EXTENSION__()
: undefined
);
``` ```
### Export from browser console or from application ### Export from browser console or from application
```js ```js
store.liftedStore.getState() store.liftedStore.getState();
``` ```
The extension is not sharing `store` object, so you should take care of that. The extension is not sharing `store` object, so you should take care of that.
@ -55,16 +60,19 @@ We're [not allowing that from instrumentation part](https://github.com/zalmoxisu
import { createStore, compose } from 'redux'; import { createStore, compose } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension/logOnly'; import { devToolsEnhancer } from 'redux-devtools-extension/logOnly';
const store = createStore(reducer, /* preloadedState, */ compose( const store = createStore(
devToolsEnhancer({ reducer,
instaceID: 1, /* preloadedState, */ compose(
name: 'Blacklisted', devToolsEnhancer({
actionsBlacklist: '...' instaceID: 1,
}), name: 'Blacklisted',
devToolsEnhancer({ actionsBlacklist: '...',
instaceID: 2, }),
name: 'Whitelisted', devToolsEnhancer({
actionsWhitelist: '...' instaceID: 2,
}) name: 'Whitelisted',
)); actionsWhitelist: '...',
})
)
);
``` ```

View File

@ -25,11 +25,17 @@ Most likely you mutate the state. Check it by [adding `redux-immutable-state-inv
Usually the extension's store enhancer should be last in the compose. When you're using [`window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`](/README.md#12-advanced-store-setup) or [`composeWithDevTools`](/README.md#13-use-redux-devtools-extension-package-from-npm) helper you don't have to worry about the enhancers order. However some enhancers ([like `redux-batched-subscribe`](https://github.com/zalmoxisus/redux-devtools-extension/issues/261)) also have this requirement to be the last in the compose. In this case you can use it like so: Usually the extension's store enhancer should be last in the compose. When you're using [`window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`](/README.md#12-advanced-store-setup) or [`composeWithDevTools`](/README.md#13-use-redux-devtools-extension-package-from-npm) helper you don't have to worry about the enhancers order. However some enhancers ([like `redux-batched-subscribe`](https://github.com/zalmoxisus/redux-devtools-extension/issues/261)) also have this requirement to be the last in the compose. In this case you can use it like so:
```js ```js
const store = createStore(reducer, preloadedState, compose( const store = createStore(
// applyMiddleware(thunk), reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : noop => noop, preloadedState,
batchedSubscribe(/* ... */) compose(
)); // applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (noop) => noop,
batchedSubscribe(/* ... */)
)
);
``` ```
Where `batchedSubscribe` is `redux-batched-subscribe` store enhancer. Where `batchedSubscribe` is `redux-batched-subscribe` store enhancer.
@ -41,14 +47,19 @@ That is happening due to serialization of some huge objects included in the stat
You can do that by including/omitting data containing specific values, having specific types... In the example below we're omitting parts of action and state objects with the key `data` (in case of action only when was dispatched action `FILE_DOWNLOAD_SUCCESS`): You can do that by including/omitting data containing specific values, having specific types... In the example below we're omitting parts of action and state objects with the key `data` (in case of action only when was dispatched action `FILE_DOWNLOAD_SUCCESS`):
```js ```js
const actionSanitizer = (action) => ( const actionSanitizer = (action) =>
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data ? action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
{ ...action, data: '<<LONG_BLOB>>' } : action ? { ...action, data: '<<LONG_BLOB>>' }
: action;
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
actionSanitizer,
stateSanitizer: (state) =>
state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
})
); );
const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
actionSanitizer,
stateSanitizer: (state) => state.data ? { ...state, data: '<<LONG_BLOB>>' } : state
}));
``` ```
There's a more advanced [example on how to implement that for `ui-router`](https://github.com/zalmoxisus/redux-devtools-extension/issues/455#issuecomment-404538385). There's a more advanced [example on how to implement that for `ui-router`](https://github.com/zalmoxisus/redux-devtools-extension/issues/455#issuecomment-404538385).
@ -60,15 +71,16 @@ The extension is in different process and cannot access the store object directl
React synthetic event cannot be reused for performance reason. So, it's not possible to serialize event objects you pass to action payloads. React synthetic event cannot be reused for performance reason. So, it's not possible to serialize event objects you pass to action payloads.
1. The best solution is **not to pass the whole event object to reducers, but the data you need**: 1. The best solution is **not to pass the whole event object to reducers, but the data you need**:
```diff
function click(event) { ```diff
return { function click(event) {
type: ELEMENT_CLICKED, return {
- event: event type: ELEMENT_CLICKED,
+ value: event.target.value - event: event
}; + value: event.target.value
} };
``` }
```
2. If you cannot pick data from the event object or, for some reason, you need the whole object, use `event.persist()` as suggested in [React Docs](https://facebook.github.io/react/docs/events.html#event-pooling), but it will consume RAM while not needed. 2. If you cannot pick data from the event object or, for some reason, you need the whole object, use `event.persist()` as suggested in [React Docs](https://facebook.github.io/react/docs/events.html#event-pooling), but it will consume RAM while not needed.
@ -95,6 +107,7 @@ React synthetic event cannot be reused for performance reason. So, it's not poss
}; };
} }
``` ```
Note that it shouldn't be arrow function as we want to have access to the function's `this`. Note that it shouldn't be arrow function as we want to have access to the function's `this`.
As we don't have access to the original object, skipping and recomputing actions during hot reloading will not work in this case. We recommend to use the first solution whenever possible. As we don't have access to the original object, skipping and recomputing actions during hot reloading will not work in this case. We recommend to use the first solution whenever possible.
@ -102,10 +115,15 @@ React synthetic event cannot be reused for performance reason. So, it's not poss
### Symbols or other unserializable data not shown ### Symbols or other unserializable data not shown
To get data which cannot be serialized by `JSON.stringify`, set [`serialize` parameter](/docs/API/Arguments.md#serialize): To get data which cannot be serialized by `JSON.stringify`, set [`serialize` parameter](/docs/API/Arguments.md#serialize):
```js ```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({ const store = Redux.createStore(
serialize: true reducer,
})); window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: true,
})
);
``` ```
It will handle also date, regex, undefined, error objects, symbols, maps, sets and functions. It will handle also date, regex, undefined, error objects, symbols, maps, sets and functions.

View File

@ -13,7 +13,7 @@ var exampleDirs = fs.readdirSync(__dirname).filter((file) => {
// Ordering is important here. `npm install` must come first. // Ordering is important here. `npm install` must come first.
var cmdArgs = [ var cmdArgs = [
{ cmd: 'npm', args: ['install'] }, { cmd: 'npm', args: ['install'] },
{ cmd: 'webpack', args: ['index.js'] } { cmd: 'webpack', args: ['index.js'] },
]; ];
for (const dir of exampleDirs) { for (const dir of exampleDirs) {
@ -21,7 +21,7 @@ for (const dir of exampleDirs) {
// declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158
const opts = { const opts = {
cwd: path.join(__dirname, dir), cwd: path.join(__dirname, dir),
stdio: 'inherit' stdio: 'inherit',
}; };
let result = {}; let result = {};
if (process.platform === 'win32') { if (process.platform === 'win32') {

View File

@ -1,3 +1,3 @@
{ {
"presets": [ "es2015", "stage-0", "react" ] "presets": ["es2015", "stage-0", "react"]
} }

View File

@ -5,18 +5,18 @@ let t;
export function increment() { export function increment() {
return { return {
type: INCREMENT_COUNTER type: INCREMENT_COUNTER,
}; };
} }
export function decrement() { export function decrement() {
return { return {
type: DECREMENT_COUNTER type: DECREMENT_COUNTER,
}; };
} }
export function autoIncrement(delay = 10) { export function autoIncrement(delay = 10) {
return dispatch => { return (dispatch) => {
if (t) { if (t) {
clearInterval(t); clearInterval(t);
t = undefined; t = undefined;
@ -29,7 +29,7 @@ export function autoIncrement(delay = 10) {
} }
export function incrementAsync(delay = 1000) { export function incrementAsync(delay = 1000) {
return dispatch => { return (dispatch) => {
setTimeout(() => { setTimeout(() => {
dispatch(increment()); dispatch(increment());
}, delay); }, delay);

View File

@ -3,17 +3,18 @@ import PropTypes from 'prop-types';
class Counter extends Component { class Counter extends Component {
render() { render() {
const { increment, autoIncrement, incrementAsync, decrement, counter } = this.props; const {
increment,
autoIncrement,
incrementAsync,
decrement,
counter,
} = this.props;
return ( return (
<p> <p>
Clicked: {counter} times Clicked: {counter} times <button onClick={increment}>+</button>{' '}
{' '} <button onClick={decrement}>-</button>{' '}
<button onClick={increment}>+</button> <button onClick={incrementAsync}>Increment async</button>{' '}
{' '}
<button onClick={decrement}>-</button>
{' '}
<button onClick={incrementAsync}>Increment async</button>
{' '}
<button onClick={autoIncrement}>Auto increment</button> <button onClick={autoIncrement}>Auto increment</button>
</p> </p>
); );
@ -25,7 +26,7 @@ Counter.propTypes = {
autoIncrement: PropTypes.func.isRequired, autoIncrement: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired, incrementAsync: PropTypes.func.isRequired,
decrement: PropTypes.func.isRequired, decrement: PropTypes.func.isRequired,
counter: PropTypes.number.isRequired counter: PropTypes.number.isRequired,
}; };
export default Counter; export default Counter;

View File

@ -5,7 +5,7 @@ import * as CounterActions from '../actions/counter';
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
counter: state.counter counter: state.counter,
}; };
} }

View File

@ -4,8 +4,7 @@
<title>Redux counter example</title> <title>Redux counter example</title>
</head> </head>
<body> <body>
<div id="root"> <div id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -2,11 +2,11 @@ import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';
export default function counter(state = 0, action) { export default function counter(state = 0, action) {
switch (action.type) { switch (action.type) {
case INCREMENT_COUNTER: case INCREMENT_COUNTER:
return state + 1; return state + 1;
case DECREMENT_COUNTER: case DECREMENT_COUNTER:
return state - 1; return state - 1;
default: default:
return state; return state;
} }
} }

View File

@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
import counter from './counter'; import counter from './counter';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
counter counter,
}); });
export default rootReducer; export default rootReducer;

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4001; var port = 4001;
var compiler = webpack(config); var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(
webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
})
);
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
app.get("/", function(req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.listen(port, function(error) { app.listen(port, function (error) {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); console.info(
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
port,
port
);
} }
}); });

View File

@ -6,15 +6,21 @@ import reducer from '../reducers';
import * as actionCreators from '../actions/counter'; import * as actionCreators from '../actions/counter';
export default function configureStore(preloadedState) { export default function configureStore(preloadedState) {
const composeEnhancers = composeWithDevTools({ actionCreators, trace: true, traceLimit: 25 }); const composeEnhancers = composeWithDevTools({
const store = createStore(reducer, preloadedState, composeEnhancers( actionCreators,
applyMiddleware(invariant(), thunk) trace: true,
)); traceLimit: 25,
});
const store = createStore(
reducer,
preloadedState,
composeEnhancers(applyMiddleware(invariant(), thunk))
);
if (module.hot) { if (module.hot) {
// Enable Webpack hot module replacement for reducers // Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => { module.hot.accept('../reducers', () => {
store.replaceReducer(require('../reducers').default) store.replaceReducer(require('../reducers').default);
}); });
} }

View File

@ -12,16 +12,17 @@ function mockStore(getState, expectedActions, onLastAction) {
if (!Array.isArray(expectedActions)) { if (!Array.isArray(expectedActions)) {
throw new Error('expectedActions should be an array of expected actions.'); throw new Error('expectedActions should be an array of expected actions.');
} }
if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') { if (
typeof onLastAction !== 'undefined' &&
typeof onLastAction !== 'function'
) {
throw new Error('onLastAction should either be undefined or function.'); throw new Error('onLastAction should either be undefined or function.');
} }
function mockStoreWithoutMiddleware() { function mockStoreWithoutMiddleware() {
return { return {
getState() { getState() {
return typeof getState === 'function' ? return typeof getState === 'function' ? getState() : getState;
getState() :
getState;
}, },
dispatch(action) { dispatch(action) {
@ -31,13 +32,13 @@ function mockStore(getState, expectedActions, onLastAction) {
onLastAction(); onLastAction();
} }
return action; return action;
} },
}; };
} }
const mockStoreWithMiddleware = applyMiddleware( const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
...middlewares mockStoreWithoutMiddleware
)(mockStoreWithoutMiddleware); );
return mockStoreWithMiddleware(); return mockStoreWithMiddleware();
} }
@ -52,9 +53,7 @@ describe('actions', () => {
}); });
it('incrementIfOdd should create increment action', (done) => { it('incrementIfOdd should create increment action', (done) => {
const expectedActions = [ const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
{ type: actions.INCREMENT_COUNTER }
];
const store = mockStore({ counter: 1 }, expectedActions, done); const store = mockStore({ counter: 1 }, expectedActions, done);
store.dispatch(actions.incrementIfOdd()); store.dispatch(actions.incrementIfOdd());
}); });
@ -67,9 +66,7 @@ describe('actions', () => {
}); });
it('incrementAsync should create increment action', (done) => { it('incrementAsync should create increment action', (done) => {
const expectedActions = [ const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
{ type: actions.INCREMENT_COUNTER }
];
const store = mockStore({ counter: 0 }, expectedActions, done); const store = mockStore({ counter: 0 }, expectedActions, done);
store.dispatch(actions.incrementAsync(100)); store.dispatch(actions.incrementAsync(100));
}); });

View File

@ -8,14 +8,16 @@ function setup() {
increment: expect.createSpy(), increment: expect.createSpy(),
incrementIfOdd: expect.createSpy(), incrementIfOdd: expect.createSpy(),
incrementAsync: expect.createSpy(), incrementAsync: expect.createSpy(),
decrement: expect.createSpy() decrement: expect.createSpy(),
}; };
const component = TestUtils.renderIntoDocument(<Counter counter={1} {...actions} />); const component = TestUtils.renderIntoDocument(
<Counter counter={1} {...actions} />
);
return { return {
component: component, component: component,
actions: actions, actions: actions,
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'), buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p') p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
}; };
} }

View File

@ -15,7 +15,7 @@ function setup(initialState) {
return { return {
app: app, app: app,
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'), buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p') p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
}; };
} }

View File

@ -4,23 +4,20 @@ var webpack = require('webpack');
module.exports = { module.exports = {
mode: 'development', mode: 'development',
devtool: 'source-map', devtool: 'source-map',
entry: [ entry: ['webpack-hot-middleware/client', './index'],
'webpack-hot-middleware/client',
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
plugins: [ plugins: [new webpack.HotModuleReplacementPlugin()],
new webpack.HotModuleReplacementPlugin()
],
module: { module: {
rules: [{ rules: [
test: /\.js$/, {
loaders: ['babel-loader'], test: /\.js$/,
exclude: /node_modules/ loaders: ['babel-loader'],
}] exclude: /node_modules/,
} },
],
},
}; };

View File

@ -1,3 +1,3 @@
{ {
"presets": [ "es2015", "stage-0", "react" ] "presets": ["es2015", "stage-0", "react"]
} }

View File

@ -1,9 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
const withDevTools = ( const withDevTools =
// process.env.NODE_ENV === 'development' && // process.env.NODE_ENV === 'development' &&
typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__ typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__;
);
class Counter extends Component { class Counter extends Component {
constructor() { constructor() {
@ -20,7 +19,10 @@ class Counter extends Component {
this.unsubscribe = this.devTools.subscribe((message) => { this.unsubscribe = this.devTools.subscribe((message) => {
// Implement monitors actions. // Implement monitors actions.
// For example time traveling: // For example time traveling:
if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_STATE') { if (
message.type === 'DISPATCH' &&
message.payload.type === 'JUMP_TO_STATE'
) {
this.setState(message.state); this.setState(message.state);
} }
}); });
@ -50,10 +52,7 @@ class Counter extends Component {
const { counter } = this.state; const { counter } = this.state;
return ( return (
<p> <p>
Clicked: {counter} times Clicked: {counter} times <button onClick={this.increment}>+</button>{' '}
{' '}
<button onClick={this.increment}>+</button>
{' '}
<button onClick={this.decrement}>-</button> <button onClick={this.decrement}>-</button>
</p> </p>
); );

View File

@ -4,8 +4,7 @@
<title>React counter example</title> <title>React counter example</title>
</head> </head>
<body> <body>
<div id="root"> <div id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -2,7 +2,4 @@ import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import Counter from './components/Counter'; import Counter from './components/Counter';
render( render(<Counter />, document.getElementById('root'));
<Counter />,
document.getElementById('root')
);

View File

@ -4,22 +4,22 @@ var webpack = require('webpack');
module.exports = { module.exports = {
mode: 'development', mode: 'development',
devtool: 'source-map', devtool: 'source-map',
entry: [ entry: ['./index'],
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
module: { module: {
rules: [{ rules: [
test: /\.js$/, {
loaders: ['babel-loader'], test: /\.js$/,
exclude: /node_modules/ loaders: ['babel-loader'],
}] exclude: /node_modules/,
},
],
}, },
devServer: { devServer: {
port: 4004 port: 4004,
} },
}; };

View File

@ -1,4 +1,4 @@
{ {
"presets": [ "es2015", "stage-0", "react" ], "presets": ["es2015", "stage-0", "react"],
"plugins": [ "add-module-exports", "transform-decorators-legacy" ] "plugins": ["add-module-exports", "transform-decorators-legacy"]
} }

View File

@ -1,11 +1,15 @@
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const FILTER_TITLES = { const FILTER_TITLES = {
[SHOW_ALL]: 'All', [SHOW_ALL]: 'All',
[SHOW_ACTIVE]: 'Active', [SHOW_ACTIVE]: 'Active',
[SHOW_COMPLETED]: 'Completed' [SHOW_COMPLETED]: 'Completed',
}; };
class Footer extends Component { class Footer extends Component {
@ -25,9 +29,11 @@ class Footer extends Component {
const { filter: selectedFilter, onShow } = this.props; const { filter: selectedFilter, onShow } = this.props;
return ( return (
<a className={classnames({ selected: filter === selectedFilter })} <a
style={{ cursor: 'pointer' }} className={classnames({ selected: filter === selectedFilter })}
onClick={() => onShow(filter)}> style={{ cursor: 'pointer' }}
onClick={() => onShow(filter)}
>
{title} {title}
</a> </a>
); );
@ -37,8 +43,7 @@ class Footer extends Component {
const { completedCount, onClearCompleted } = this.props; const { completedCount, onClearCompleted } = this.props;
if (completedCount > 0) { if (completedCount > 0) {
return ( return (
<button className="clear-completed" <button className="clear-completed" onClick={onClearCompleted}>
onClick={onClearCompleted} >
Clear completed Clear completed
</button> </button>
); );
@ -50,11 +55,9 @@ class Footer extends Component {
<footer className="footer"> <footer className="footer">
{this.renderTodoCount()} {this.renderTodoCount()}
<ul className="filters"> <ul className="filters">
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter => {[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
<li key={filter}> <li key={filter}>{this.renderFilterLink(filter)}</li>
{this.renderFilterLink(filter)} ))}
</li>
)}
</ul> </ul>
{this.renderClearButton()} {this.renderClearButton()}
</footer> </footer>
@ -67,7 +70,7 @@ Footer.propTypes = {
activeCount: PropTypes.number.isRequired, activeCount: PropTypes.number.isRequired,
filter: PropTypes.string.isRequired, filter: PropTypes.string.isRequired,
onClearCompleted: PropTypes.func.isRequired, onClearCompleted: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired onShow: PropTypes.func.isRequired,
}; };
export default Footer; export default Footer;

View File

@ -12,17 +12,19 @@ class Header extends Component {
const { path } = this.props; const { path } = this.props;
return ( return (
<header className="header"> <header className="header">
<h1 style={{ fontSize: 80 }}>{path}</h1> <h1 style={{ fontSize: 80 }}>{path}</h1>
<TodoTextInput newTodo <TodoTextInput
onSave={this.handleSave.bind(this)} newTodo
placeholder="What needs to be done?" /> onSave={this.handleSave.bind(this)}
placeholder="What needs to be done?"
/>
</header> </header>
); );
} }
} }
Header.propTypes = { Header.propTypes = {
addTodo: PropTypes.func.isRequired addTodo: PropTypes.func.isRequired,
}; };
export default Header; export default Header;

View File

@ -1,12 +1,16 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import TodoItem from './TodoItem'; import TodoItem from './TodoItem';
import Footer from './Footer'; import Footer from './Footer';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const TODO_FILTERS = { const TODO_FILTERS = {
[SHOW_ALL]: () => true, [SHOW_ALL]: () => true,
[SHOW_ACTIVE]: todo => !todo.completed, [SHOW_ACTIVE]: (todo) => !todo.completed,
[SHOW_COMPLETED]: todo => todo.completed [SHOW_COMPLETED]: (todo) => todo.completed,
}; };
class MainSection extends Component { class MainSection extends Component {
@ -16,7 +20,7 @@ class MainSection extends Component {
} }
handleClearCompleted() { handleClearCompleted() {
const atLeastOneCompleted = this.props.todos.some(todo => todo.completed); const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
if (atLeastOneCompleted) { if (atLeastOneCompleted) {
this.props.actions.clearCompleted(); this.props.actions.clearCompleted();
} }
@ -30,10 +34,12 @@ class MainSection extends Component {
const { todos, actions } = this.props; const { todos, actions } = this.props;
if (todos.length > 0) { if (todos.length > 0) {
return ( return (
<input className="toggle-all" <input
type="checkbox" className="toggle-all"
checked={completedCount === todos.length} type="checkbox"
onChange={actions.completeAll} /> checked={completedCount === todos.length}
onChange={actions.completeAll}
/>
); );
} }
} }
@ -45,11 +51,13 @@ class MainSection extends Component {
if (todos.length) { if (todos.length) {
return ( return (
<Footer completedCount={completedCount} <Footer
activeCount={activeCount} completedCount={completedCount}
filter={filter} activeCount={activeCount}
onClearCompleted={this.handleClearCompleted.bind(this)} filter={filter}
onShow={this.handleShow.bind(this)} /> onClearCompleted={this.handleClearCompleted.bind(this)}
onShow={this.handleShow.bind(this)}
/>
); );
} }
} }
@ -59,8 +67,8 @@ class MainSection extends Component {
const { filter } = this.state; const { filter } = this.state;
const filteredTodos = todos.filter(TODO_FILTERS[filter]); const filteredTodos = todos.filter(TODO_FILTERS[filter]);
const completedCount = todos.reduce((count, todo) => const completedCount = todos.reduce(
todo.completed ? count + 1 : count, (count, todo) => (todo.completed ? count + 1 : count),
0 0
); );
@ -68,9 +76,9 @@ class MainSection extends Component {
<section className="main"> <section className="main">
{this.renderToggleAll(completedCount)} {this.renderToggleAll(completedCount)}
<ul className="todo-list"> <ul className="todo-list">
{filteredTodos.map(todo => {filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} {...actions} /> <TodoItem key={todo.id} todo={todo} {...actions} />
)} ))}
</ul> </ul>
{this.renderFooter(completedCount)} {this.renderFooter(completedCount)}
</section> </section>
@ -80,7 +88,7 @@ class MainSection extends Component {
MainSection.propTypes = { MainSection.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
export default MainSection; export default MainSection;

View File

@ -6,7 +6,7 @@ class TodoItem extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
editing: false editing: false,
}; };
} }
@ -24,36 +24,41 @@ class TodoItem extends Component {
} }
render() { render() {
const {todo, completeTodo, deleteTodo} = this.props; const { todo, completeTodo, deleteTodo } = this.props;
let element; let element;
if (this.state.editing) { if (this.state.editing) {
element = ( element = (
<TodoTextInput text={todo.text} <TodoTextInput
editing={this.state.editing} text={todo.text}
onSave={(text) => this.handleSave(todo.id, text)} /> editing={this.state.editing}
onSave={(text) => this.handleSave(todo.id, text)}
/>
); );
} else { } else {
element = ( element = (
<div className="view"> <div className="view">
<input className="toggle" <input
type="checkbox" className="toggle"
checked={todo.completed} type="checkbox"
onChange={() => completeTodo(todo.id)} /> checked={todo.completed}
onChange={() => completeTodo(todo.id)}
/>
<label onDoubleClick={this.handleDoubleClick.bind(this)}> <label onDoubleClick={this.handleDoubleClick.bind(this)}>
{todo.text} {todo.text}
</label> </label>
<button className="destroy" <button className="destroy" onClick={() => deleteTodo(todo.id)} />
onClick={() => deleteTodo(todo.id)} />
</div> </div>
); );
} }
return ( return (
<li className={classnames({ <li
completed: todo.completed, className={classnames({
editing: this.state.editing completed: todo.completed,
})}> editing: this.state.editing,
})}
>
{element} {element}
</li> </li>
); );
@ -64,7 +69,7 @@ TodoItem.propTypes = {
todo: PropTypes.object.isRequired, todo: PropTypes.object.isRequired,
editTodo: PropTypes.func.isRequired, editTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired, deleteTodo: PropTypes.func.isRequired,
completeTodo: PropTypes.func.isRequired completeTodo: PropTypes.func.isRequired,
}; };
export default TodoItem; export default TodoItem;

View File

@ -5,7 +5,7 @@ class TodoTextInput extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
text: this.props.text || '' text: this.props.text || '',
}; };
} }
@ -31,10 +31,10 @@ class TodoTextInput extends Component {
render() { render() {
return ( return (
<input className={ <input
classnames({ className={classnames({
edit: this.props.editing, edit: this.props.editing,
'new-todo': this.props.newTodo 'new-todo': this.props.newTodo,
})} })}
type="text" type="text"
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
@ -42,7 +42,8 @@ class TodoTextInput extends Component {
value={this.state.text} value={this.state.text}
onBlur={this.handleBlur.bind(this)} onBlur={this.handleBlur.bind(this)}
onChange={this.handleChange.bind(this)} onChange={this.handleChange.bind(this)}
onKeyDown={this.handleSubmit.bind(this)} /> onKeyDown={this.handleSubmit.bind(this)}
/>
); );
} }
} }
@ -52,7 +53,7 @@ TodoTextInput.propTypes = {
text: PropTypes.string, text: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
editing: PropTypes.bool, editing: PropTypes.bool,
newTodo: PropTypes.bool newTodo: PropTypes.bool,
}; };
export default TodoTextInput; export default TodoTextInput;

View File

@ -19,23 +19,20 @@ class App extends Component {
App.propTypes = { App.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
todos: state.todos, todos: state.todos,
path: state.router.location.pathname path: state.router.location.pathname,
}; };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
actions: bindActionCreators(TodoActions, dispatch) actions: bindActionCreators(TodoActions, dispatch),
}; };
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(App);
mapStateToProps,
mapDispatchToProps
)(App);

View File

@ -9,9 +9,9 @@ class Root extends Component {
render() { render() {
return ( return (
<ReduxRouter> <ReduxRouter>
<Redirect from="/" to="Standard Todo"/> <Redirect from="/" to="Standard Todo" />
<Route path="/" component={Wrapper}> <Route path="/" component={Wrapper}>
<Route path="/:id" component={App}/> <Route path="/:id" component={App} />
</Route> </Route>
</ReduxRouter> </ReduxRouter>
); );

View File

@ -8,14 +8,14 @@ import * as TodoActions from '../actions/todos';
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
pushState: bindActionCreators(pushState, dispatch), pushState: bindActionCreators(pushState, dispatch),
actions: bindActionCreators(TodoActions, dispatch) actions: bindActionCreators(TodoActions, dispatch),
}; };
} }
@connect((state) => ({}), mapDispatchToProps) @connect((state) => ({}), mapDispatchToProps)
class Wrapper extends Component { class Wrapper extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node children: PropTypes.node,
}; };
constructor(props) { constructor(props) {
@ -41,11 +41,23 @@ class Wrapper extends Component {
} }
render() { render() {
return ( return (
<div> <div>
<div style={{ padding: 20, backgroundColor: '#eee', fontWeight: 'bold', textAlign: 'center' }}> <div
<a href="#" onClick={this.handleClick}>Standard Todo</a> | <a href="#" onClick={this.handleClick}>AutoTodo</a> style={{
padding: 20,
backgroundColor: '#eee',
fontWeight: 'bold',
textAlign: 'center',
}}
>
<a href="#" onClick={this.handleClick}>
Standard Todo
</a>{' '}
|{' '}
<a href="#" onClick={this.handleClick}>
AutoTodo
</a>
</div> </div>
{this.props.children} {this.props.children}
</div> </div>

View File

@ -4,8 +4,7 @@
<title>Redux TodoMVC example</title> <title>Redux TodoMVC example</title>
</head> </head>
<body> <body>
<div class="todoapp" id="root"> <div class="todoapp" id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -4,7 +4,7 @@ import todos from './todos';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
todos, todos,
router: routerStateReducer router: routerStateReducer,
}); });
export default rootReducer; export default rootReducer;

View File

@ -1,49 +1,61 @@
import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'; import {
ADD_TODO,
DELETE_TODO,
EDIT_TODO,
COMPLETE_TODO,
COMPLETE_ALL,
CLEAR_COMPLETED,
} from '../constants/ActionTypes';
const initialState = [{ const initialState = [
text: 'Use Redux', {
completed: false, text: 'Use Redux',
id: 0 completed: false,
}]; id: 0,
},
];
export default function todos(state = initialState, action) { export default function todos(state = initialState, action) {
switch (action.type) { switch (action.type) {
case ADD_TODO: case ADD_TODO:
return [{ return [
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, {
completed: false, id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
text: action.text completed: false,
}, ...state]; text: action.text,
},
...state,
];
case DELETE_TODO: case DELETE_TODO:
return state.filter(todo => return state.filter((todo) => todo.id !== action.id);
todo.id !== action.id
);
case EDIT_TODO: case EDIT_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { text: action.text }) : ? Object.assign({}, todo, { text: action.text })
todo : todo
); );
case COMPLETE_TODO: case COMPLETE_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { completed: !todo.completed }) : ? Object.assign({}, todo, { completed: !todo.completed })
todo : todo
); );
case COMPLETE_ALL: case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed); const areAllMarked = state.every((todo) => todo.completed);
return state.map(todo => Object.assign({}, todo, { return state.map((todo) =>
completed: !areAllMarked Object.assign({}, todo, {
})); completed: !areAllMarked,
})
);
case CLEAR_COMPLETED: case CLEAR_COMPLETED:
return state.filter(todo => todo.completed === false); return state.filter((todo) => todo.completed === false);
default: default:
return state; return state;
} }
} }

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4002; var port = 4002;
var compiler = webpack(config); var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(
webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
})
);
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
app.get("/", function(req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.listen(port, function(error) { app.listen(port, function (error) {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); console.info(
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
port,
port
);
} }
}); });

View File

@ -1,5 +1,9 @@
import { createStore, compose } from 'redux'; import { createStore, compose } from 'redux';
import { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router'; import {
reduxReactRouter,
routerStateReducer,
ReduxRouter,
} from 'redux-router';
//import createHistory from 'history/lib/createBrowserHistory'; //import createHistory from 'history/lib/createBrowserHistory';
import createHistory from 'history/lib/createHashHistory'; import createHistory from 'history/lib/createHashHistory';
import rootReducer from '../reducers'; import rootReducer from '../reducers';
@ -7,7 +11,7 @@ import rootReducer from '../reducers';
export default function configureStore(initialState) { export default function configureStore(initialState) {
let finalCreateStore = compose( let finalCreateStore = compose(
reduxReactRouter({ createHistory }), reduxReactRouter({ createHistory }),
global.devToolsExtension ? global.devToolsExtension() : f => f global.devToolsExtension ? global.devToolsExtension() : (f) => f
)(createStore); )(createStore);
const store = finalCreateStore(rootReducer, initialState); const store = finalCreateStore(rootReducer, initialState);

View File

@ -6,14 +6,14 @@ describe('todo actions', () => {
it('addTodo should create ADD_TODO action', () => { it('addTodo should create ADD_TODO action', () => {
expect(actions.addTodo('Use Redux')).toEqual({ expect(actions.addTodo('Use Redux')).toEqual({
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Use Redux' text: 'Use Redux',
}); });
}); });
it('deleteTodo should create DELETE_TODO action', () => { it('deleteTodo should create DELETE_TODO action', () => {
expect(actions.deleteTodo(1)).toEqual({ expect(actions.deleteTodo(1)).toEqual({
type: types.DELETE_TODO, type: types.DELETE_TODO,
id: 1 id: 1,
}); });
}); });
@ -21,26 +21,26 @@ describe('todo actions', () => {
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
type: types.EDIT_TODO, type: types.EDIT_TODO,
id: 1, id: 1,
text: 'Use Redux everywhere' text: 'Use Redux everywhere',
}); });
}); });
it('completeTodo should create COMPLETE_TODO action', () => { it('completeTodo should create COMPLETE_TODO action', () => {
expect(actions.completeTodo(1)).toEqual({ expect(actions.completeTodo(1)).toEqual({
type: types.COMPLETE_TODO, type: types.COMPLETE_TODO,
id: 1 id: 1,
}); });
}); });
it('completeAll should create COMPLETE_ALL action', () => { it('completeAll should create COMPLETE_ALL action', () => {
expect(actions.completeAll()).toEqual({ expect(actions.completeAll()).toEqual({
type: types.COMPLETE_ALL type: types.COMPLETE_ALL,
}); });
}); });
it('clearCompleted should create CLEAR_COMPLETED action', () => { it('clearCompleted should create CLEAR_COMPLETED action', () => {
expect(actions.clearCompleted('Use Redux')).toEqual({ expect(actions.clearCompleted('Use Redux')).toEqual({
type: types.CLEAR_COMPLETED type: types.CLEAR_COMPLETED,
}); });
}); });
}); });

View File

@ -5,13 +5,16 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
completedCount: 0, {
activeCount: 0, completedCount: 0,
filter: SHOW_ALL, activeCount: 0,
onClearCompleted: expect.createSpy(), filter: SHOW_ALL,
onShow: expect.createSpy() onClearCompleted: expect.createSpy(),
}, propOverrides); onShow: expect.createSpy(),
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<Footer {...props} />); renderer.render(<Footer {...props} />);
@ -19,13 +22,14 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output output: output,
}; };
} }
function getTextContent(elem) { function getTextContent(elem) {
const children = Array.isArray(elem.props.children) ? const children = Array.isArray(elem.props.children)
elem.props.children : [elem.props.children]; ? elem.props.children
: [elem.props.children];
return children.reduce(function concatText(out, child) { return children.reduce(function concatText(out, child) {
// Children are either elements or text strings // Children are either elements or text strings
@ -63,11 +67,13 @@ describe('components', () => {
expect(filter.type).toBe('li'); expect(filter.type).toBe('li');
const a = filter.props.children; const a = filter.props.children;
expect(a.props.className).toBe(i === 0 ? 'selected' : ''); expect(a.props.className).toBe(i === 0 ? 'selected' : '');
expect(a.props.children).toBe({ expect(a.props.children).toBe(
0: 'All', {
1: 'Active', 0: 'All',
2: 'Completed' 1: 'Active',
}[i]); 2: 'Completed',
}[i]
);
}); });
}); });
@ -81,20 +87,20 @@ describe('components', () => {
it('shouldnt show clear button when no completed todos', () => { it('shouldnt show clear button when no completed todos', () => {
const { output } = setup({ completedCount: 0 }); const { output } = setup({ completedCount: 0 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear).toBe(undefined); expect(clear).toBe(undefined);
}); });
it('should render clear button when completed todos', () => { it('should render clear button when completed todos', () => {
const { output } = setup({ completedCount: 1 }); const { output } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear.type).toBe('button'); expect(clear.type).toBe('button');
expect(clear.props.children).toBe('Clear completed'); expect(clear.props.children).toBe('Clear completed');
}); });
it('should call onClearCompleted on clear button click', () => { it('should call onClearCompleted on clear button click', () => {
const { output, props } = setup({ completedCount: 1 }); const { output, props } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
clear.props.onClick({}); clear.props.onClick({});
expect(props.onClearCompleted).toHaveBeenCalled(); expect(props.onClearCompleted).toHaveBeenCalled();
}); });

View File

@ -6,7 +6,7 @@ import TodoTextInput from '../../components/TodoTextInput';
function setup() { function setup() {
const props = { const props = {
addTodo: expect.createSpy() addTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
@ -16,7 +16,7 @@ function setup() {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -7,24 +7,30 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
todos: [{ {
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}, { completed: false,
text: 'Run the tests', id: 0,
completed: true, },
id: 1 {
}], text: 'Run the tests',
actions: { completed: true,
editTodo: expect.createSpy(), id: 1,
deleteTodo: expect.createSpy(), },
completeTodo: expect.createSpy(), ],
completeAll: expect.createSpy(), actions: {
clearCompleted: expect.createSpy() editTodo: expect.createSpy(),
} deleteTodo: expect.createSpy(),
}, propOverrides); completeTodo: expect.createSpy(),
completeAll: expect.createSpy(),
clearCompleted: expect.createSpy(),
},
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<MainSection {...props} />); renderer.render(<MainSection {...props} />);
@ -33,7 +39,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -55,11 +61,15 @@ describe('components', () => {
}); });
it('should be checked if all todos completed', () => { it('should be checked if all todos completed', () => {
const { output } = setup({ todos: [{ const { output } = setup({
text: 'Use Redux', todos: [
completed: true, {
id: 0 text: 'Use Redux',
}]}); completed: true,
id: 0,
},
],
});
const [toggle] = output.props.children; const [toggle] = output.props.children;
expect(toggle.props.checked).toBe(true); expect(toggle.props.checked).toBe(true);
}); });
@ -75,7 +85,7 @@ describe('components', () => {
describe('footer', () => { describe('footer', () => {
it('should render', () => { it('should render', () => {
const { output } = setup(); const { output } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
expect(footer.type).toBe(Footer); expect(footer.type).toBe(Footer);
expect(footer.props.completedCount).toBe(1); expect(footer.props.completedCount).toBe(1);
expect(footer.props.activeCount).toBe(1); expect(footer.props.activeCount).toBe(1);
@ -84,27 +94,31 @@ describe('components', () => {
it('onShow should set the filter', () => { it('onShow should set the filter', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [,, updatedFooter] = updated.props.children; const [, , updatedFooter] = updated.props.children;
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED); expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
}); });
it('onClearCompleted should call clearCompleted', () => { it('onClearCompleted should call clearCompleted', () => {
const { output, props } = setup(); const { output, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted).toHaveBeenCalled(); expect(props.actions.clearCompleted).toHaveBeenCalled();
}); });
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => { it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
const { output, props } = setup({ todos: [{ const { output, props } = setup({
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}]}); completed: false,
const [,, footer] = output.props.children; id: 0,
},
],
});
const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted.calls.length).toBe(0); expect(props.actions.clearCompleted.calls.length).toBe(0);
}); });
@ -124,7 +138,7 @@ describe('components', () => {
it('should filter items', () => { it('should filter items', () => {
const { output, renderer, props } = setup(); const { output, renderer, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [, updatedList] = updated.props.children; const [, updatedList] = updated.props.children;

View File

@ -4,23 +4,21 @@ import TestUtils from 'react-addons-test-utils';
import TodoItem from '../../components/TodoItem'; import TodoItem from '../../components/TodoItem';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup( editing = false ) { function setup(editing = false) {
const props = { const props = {
todo: { todo: {
id: 0, id: 0,
text: 'Use Redux', text: 'Use Redux',
completed: false completed: false,
}, },
editTodo: expect.createSpy(), editTodo: expect.createSpy(),
deleteTodo: expect.createSpy(), deleteTodo: expect.createSpy(),
completeTodo: expect.createSpy() completeTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoItem {...props} />);
<TodoItem {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -33,7 +31,7 @@ function setup( editing = false ) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -4,19 +4,20 @@ import TestUtils from 'react-addons-test-utils';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
onSave: expect.createSpy(), {
text: 'Use Redux', onSave: expect.createSpy(),
placeholder: 'What needs to be done?', text: 'Use Redux',
editing: false, placeholder: 'What needs to be done?',
newTodo: false editing: false,
}, propOverrides); newTodo: false,
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoTextInput {...props} />);
<TodoTextInput {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -25,7 +26,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -50,33 +51,33 @@ describe('components', () => {
it('should update value on change', () => { it('should update value on change', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
output.props.onChange({ target: { value: 'Use Radox' }}); output.props.onChange({ target: { value: 'Use Radox' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual('Use Radox'); expect(updated.props.value).toEqual('Use Radox');
}); });
it('should call onSave on return key press', () => { it('should call onSave on return key press', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('should reset state on return key press if newTodo', () => { it('should reset state on return key press if newTodo', () => {
const { output, renderer } = setup({ newTodo: true }); const { output, renderer } = setup({ newTodo: true });
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual(''); expect(updated.props.value).toEqual('');
}); });
it('should call onSave on blur', () => { it('should call onSave on blur', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('shouldnt call onSave on blur if newTodo', () => { it('shouldnt call onSave on blur if newTodo', () => {
const { output, props } = setup({ newTodo: true }); const { output, props } = setup({ newTodo: true });
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave.calls.length).toBe(0); expect(props.onSave.calls.length).toBe(0);
}); });
}); });

View File

@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
describe('todos reducer', () => { describe('todos reducer', () => {
it('should handle initial state', () => { it('should handle initial state', () => {
expect( expect(todos(undefined, {})).toEqual([
todos(undefined, {}) {
).toEqual([{ text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ]);
}); });
it('should handle ADD_TODO', () => { it('should handle ADD_TODO', () => {
expect( expect(
todos([], { todos([], {
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Run the tests' text: 'Run the tests',
}) })
).toEqual([{ ).toEqual([
text: 'Run the tests', {
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.ADD_TODO,
text: 'Run the tests'
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: false,
id: 1 id: 0,
}, { },
]);
expect(
todos(
[
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.ADD_TODO,
text: 'Run the tests',
}
)
).toEqual([
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.ADD_TODO, ]);
text: 'Fix the tests'
}) expect(
).toEqual([{ todos(
text: 'Fix the tests', [
completed: false, {
id: 2 text: 'Run the tests',
}, { completed: false,
text: 'Run the tests', id: 1,
completed: false, },
id: 1 {
}, { text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ],
{
type: types.ADD_TODO,
text: 'Fix the tests',
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false,
id: 2,
},
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
]);
}); });
it('should handle DELETE_TODO', () => { it('should handle DELETE_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: false, {
id: 1 text: 'Run the tests',
}, { completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.DELETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.DELETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle EDIT_TODO', () => { it('should handle EDIT_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.EDIT_TODO,
text: 'Fix the tests',
id: 1,
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.EDIT_TODO, ]);
text: 'Fix the tests',
id: 1
})
).toEqual([{
text: 'Fix the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_TODO', () => { it('should handle COMPLETE_TODO', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_ALL', () => { it('should handle COMPLETE_ALL', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: true,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: true,
id: 0
}]);
// Unmark if all todos are currently completed // Unmark if all todos are currently completed
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: true,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: true, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle CLEAR_COMPLETED', () => { it('should handle CLEAR_COMPLETED', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: true, {
id: 1 text: 'Run the tests',
}, { completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.CLEAR_COMPLETED,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.CLEAR_COMPLETED ]);
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should not generate duplicate ids after CLEAR_COMPLETED', () => { it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
expect( expect(
[{ [
type: types.COMPLETE_TODO, {
id: 0 type: types.COMPLETE_TODO,
}, { id: 0,
type: types.CLEAR_COMPLETED },
}, { {
type: types.ADD_TODO, type: types.CLEAR_COMPLETED,
text: 'Write more tests' },
}].reduce(todos, [{ {
id: 0, type: types.ADD_TODO,
text: 'Write more tests',
},
].reduce(todos, [
{
id: 0,
completed: false,
text: 'Use Redux',
},
{
id: 1,
completed: false,
text: 'Write tests',
},
])
).toEqual([
{
text: 'Write more tests',
completed: false,
id: 2,
},
{
text: 'Write tests',
completed: false, completed: false,
text: 'Use Redux'
}, {
id: 1, id: 1,
completed: false, },
text: 'Write tests' ]);
}])
).toEqual([{
text: 'Write more tests',
completed: false,
id: 2
}, {
text: 'Write tests',
completed: false,
id: 1
}]);
}); });
}); });

View File

@ -3,30 +3,30 @@ var webpack = require('webpack');
module.exports = { module.exports = {
devtool: 'cheap-module-eval-source-map', devtool: 'cheap-module-eval-source-map',
entry: [ entry: ['webpack-hot-middleware/client', './index'],
'webpack-hot-middleware/client',
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
plugins: [ plugins: [
new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin() new webpack.NoErrorsPlugin(),
], ],
module: { module: {
loaders: [{ loaders: [
test: /\.js$/, {
loaders: ['babel'], test: /\.js$/,
exclude: /node_modules/, loaders: ['babel'],
include: __dirname exclude: /node_modules/,
}, { include: __dirname,
test: /\.css?$/, },
loaders: ['style', 'raw'], {
include: __dirname test: /\.css?$/,
}] loaders: ['style', 'raw'],
} include: __dirname,
},
],
},
}; };

View File

@ -1,3 +1,3 @@
{ {
"presets": [ "es2015", "stage-0", "react" ] "presets": ["es2015", "stage-0", "react"]
} }

View File

@ -1,14 +1,13 @@
<!doctype html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redux Saga Counter example</title> <title>Redux Saga Counter example</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="text/javascript" src="/static/bundle.js"></script> <script type="text/javascript" src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -1,33 +1,27 @@
import React from 'react' import React from 'react';
import PropTypes from 'prop-types' import PropTypes from 'prop-types';
const Counter = ({ value, onIncrement, onIncrementAsync, onDecrement, onIncrementIfOdd }) => const Counter = ({
<p> value,
Clicked: {value} times onIncrement,
{' '} onIncrementAsync,
<button onClick={onIncrement}> onDecrement,
+ onIncrementIfOdd,
</button> }) => (
{' '} <p>
<button onClick={onDecrement}> Clicked: {value} times <button onClick={onIncrement}>+</button>{' '}
- <button onClick={onDecrement}>-</button>{' '}
</button> <button onClick={onIncrementIfOdd}>Increment if odd</button>{' '}
{' '} <button onClick={onIncrementAsync}>Increment async</button>
<button onClick={onIncrementIfOdd}> </p>
Increment if odd );
</button>
{' '}
<button onClick={onIncrementAsync}>
Increment async
</button>
</p>
Counter.propTypes = { Counter.propTypes = {
value: PropTypes.number.isRequired, value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired, onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired, onDecrement: PropTypes.func.isRequired,
onIncrementAsync: PropTypes.func.isRequired, onIncrementAsync: PropTypes.func.isRequired,
onIncrementIfOdd: PropTypes.func.isRequired onIncrementIfOdd: PropTypes.func.isRequired,
} };
export default Counter export default Counter;

View File

@ -1,26 +1,30 @@
import "babel-polyfill" import 'babel-polyfill';
import React from 'react' import React from 'react';
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux' import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga' import createSagaMiddleware from 'redux-saga';
// import sagaMonitor from './sagaMonitor' // import sagaMonitor from './sagaMonitor'
import Counter from './components/Counter' import Counter from './components/Counter';
import reducer from './reducers' import reducer from './reducers';
import rootSaga from './sagas' import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware(/* {sagaMonitor} */);
const sagaMiddleware = createSagaMiddleware(/* {sagaMonitor} */) const composeEnhancers =
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, traceLimit: 25 }) || compose; window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
trace: true,
traceLimit: 25,
})) ||
compose;
const store = createStore( const store = createStore(
reducer, reducer,
composeEnhancers(applyMiddleware(sagaMiddleware)) composeEnhancers(applyMiddleware(sagaMiddleware))
) );
sagaMiddleware.run(rootSaga) sagaMiddleware.run(rootSaga);
const action = type => store.dispatch({type}) const action = (type) => store.dispatch({ type });
function render() { function render() {
ReactDOM.render( ReactDOM.render(
@ -29,10 +33,11 @@ function render() {
onIncrement={() => action('INCREMENT')} onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')} onDecrement={() => action('DECREMENT')}
onIncrementIfOdd={() => action('INCREMENT_IF_ODD')} onIncrementIfOdd={() => action('INCREMENT_IF_ODD')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />, onIncrementAsync={() => action('INCREMENT_ASYNC')}
/>,
document.getElementById('root') document.getElementById('root')
) );
} }
render() render();
store.subscribe(render) store.subscribe(render);

View File

@ -1,12 +1,12 @@
export default function counter(state = 0, action) { export default function counter(state = 0, action) {
switch (action.type) { switch (action.type) {
case 'INCREMENT': case 'INCREMENT':
return state + 1 return state + 1;
case 'INCREMENT_IF_ODD': case 'INCREMENT_IF_ODD':
return (state % 2 !== 0) ? state + 1 : state return state % 2 !== 0 ? state + 1 : state;
case 'DECREMENT': case 'DECREMENT':
return state - 1 return state - 1;
default: default:
return state return state;
} }
} }

View File

@ -1,14 +1,14 @@
/* eslint-disable no-constant-condition */ /* eslint-disable no-constant-condition */
import { takeEvery } from 'redux-saga' import { takeEvery } from 'redux-saga';
import { put, call } from 'redux-saga/effects' import { put, call } from 'redux-saga/effects';
import { delay } from 'redux-saga' import { delay } from 'redux-saga';
export function* incrementAsync() { export function* incrementAsync() {
yield call(delay, 1000) yield call(delay, 1000);
yield put({type: 'INCREMENT'}) yield put({ type: 'INCREMENT' });
} }
export default function* rootSaga() { export default function* rootSaga() {
yield* takeEvery('INCREMENT_ASYNC', incrementAsync) yield* takeEvery('INCREMENT_ASYNC', incrementAsync);
} }

View File

@ -4,22 +4,22 @@ var webpack = require('webpack');
module.exports = { module.exports = {
mode: 'development', mode: 'development',
devtool: 'source-map', devtool: 'source-map',
entry: [ entry: [path.join(__dirname, 'src', 'main')],
path.join(__dirname, 'src', 'main')
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
module: { module: {
rules: [{ rules: [
test: /\.js$/, {
loaders: ['babel-loader'], test: /\.js$/,
exclude: /node_modules/ loaders: ['babel-loader'],
}] exclude: /node_modules/,
},
],
}, },
devServer: { devServer: {
port: 4003 port: 4003,
} },
}; };

View File

@ -1,3 +1,3 @@
{ {
"presets": [ "es2015", "stage-0", "react" ] "presets": ["es2015", "stage-0", "react"]
} }

View File

@ -1,12 +1,16 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const FILTER_TITLES = { const FILTER_TITLES = {
[SHOW_ALL]: 'All', [SHOW_ALL]: 'All',
[SHOW_ACTIVE]: 'Active', [SHOW_ACTIVE]: 'Active',
[SHOW_COMPLETED]: 'Completed' [SHOW_COMPLETED]: 'Completed',
}; };
class Footer extends Component { class Footer extends Component {
@ -26,9 +30,11 @@ class Footer extends Component {
const { filter: selectedFilter, onShow } = this.props; const { filter: selectedFilter, onShow } = this.props;
return ( return (
<a className={classnames({ selected: filter === selectedFilter })} <a
style={{ cursor: 'pointer' }} className={classnames({ selected: filter === selectedFilter })}
onClick={() => onShow(filter)}> style={{ cursor: 'pointer' }}
onClick={() => onShow(filter)}
>
{title} {title}
</a> </a>
); );
@ -38,8 +44,7 @@ class Footer extends Component {
const { completedCount, onClearCompleted } = this.props; const { completedCount, onClearCompleted } = this.props;
if (completedCount > 0) { if (completedCount > 0) {
return ( return (
<button className="clear-completed" <button className="clear-completed" onClick={onClearCompleted}>
onClick={onClearCompleted} >
Clear completed Clear completed
</button> </button>
); );
@ -51,11 +56,9 @@ class Footer extends Component {
<footer className="footer"> <footer className="footer">
{this.renderTodoCount()} {this.renderTodoCount()}
<ul className="filters"> <ul className="filters">
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter => {[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
<li key={filter}> <li key={filter}>{this.renderFilterLink(filter)}</li>
{this.renderFilterLink(filter)} ))}
</li>
)}
</ul> </ul>
{this.renderClearButton()} {this.renderClearButton()}
</footer> </footer>
@ -68,7 +71,7 @@ Footer.propTypes = {
activeCount: PropTypes.number.isRequired, activeCount: PropTypes.number.isRequired,
filter: PropTypes.string.isRequired, filter: PropTypes.string.isRequired,
onClearCompleted: PropTypes.func.isRequired, onClearCompleted: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired onShow: PropTypes.func.isRequired,
}; };
export default Footer; export default Footer;

View File

@ -12,17 +12,19 @@ class Header extends Component {
render() { render() {
return ( return (
<header className="header"> <header className="header">
<h1>todos</h1> <h1>todos</h1>
<TodoTextInput newTodo <TodoTextInput
onSave={this.handleSave.bind(this)} newTodo
placeholder="What needs to be done?" /> onSave={this.handleSave.bind(this)}
placeholder="What needs to be done?"
/>
</header> </header>
); );
} }
} }
Header.propTypes = { Header.propTypes = {
addTodo: PropTypes.func.isRequired addTodo: PropTypes.func.isRequired,
}; };
export default Header; export default Header;

View File

@ -2,12 +2,16 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TodoItem from './TodoItem'; import TodoItem from './TodoItem';
import Footer from './Footer'; import Footer from './Footer';
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_COMPLETED,
SHOW_ACTIVE,
} from '../constants/TodoFilters';
const TODO_FILTERS = { const TODO_FILTERS = {
[SHOW_ALL]: () => true, [SHOW_ALL]: () => true,
[SHOW_ACTIVE]: todo => !todo.completed, [SHOW_ACTIVE]: (todo) => !todo.completed,
[SHOW_COMPLETED]: todo => todo.completed [SHOW_COMPLETED]: (todo) => todo.completed,
}; };
class MainSection extends Component { class MainSection extends Component {
@ -17,7 +21,7 @@ class MainSection extends Component {
} }
handleClearCompleted() { handleClearCompleted() {
const atLeastOneCompleted = this.props.todos.some(todo => todo.completed); const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
if (atLeastOneCompleted) { if (atLeastOneCompleted) {
this.props.actions.clearCompleted(); this.props.actions.clearCompleted();
} }
@ -31,10 +35,12 @@ class MainSection extends Component {
const { todos, actions } = this.props; const { todos, actions } = this.props;
if (todos.length > 0) { if (todos.length > 0) {
return ( return (
<input className="toggle-all" <input
type="checkbox" className="toggle-all"
checked={completedCount === todos.length} type="checkbox"
onChange={actions.completeAll} /> checked={completedCount === todos.length}
onChange={actions.completeAll}
/>
); );
} }
} }
@ -46,11 +52,13 @@ class MainSection extends Component {
if (todos.length) { if (todos.length) {
return ( return (
<Footer completedCount={completedCount} <Footer
activeCount={activeCount} completedCount={completedCount}
filter={filter} activeCount={activeCount}
onClearCompleted={this.handleClearCompleted.bind(this)} filter={filter}
onShow={this.handleShow.bind(this)} /> onClearCompleted={this.handleClearCompleted.bind(this)}
onShow={this.handleShow.bind(this)}
/>
); );
} }
} }
@ -60,8 +68,8 @@ class MainSection extends Component {
const { filter } = this.state; const { filter } = this.state;
const filteredTodos = todos.filter(TODO_FILTERS[filter]); const filteredTodos = todos.filter(TODO_FILTERS[filter]);
const completedCount = todos.reduce((count, todo) => const completedCount = todos.reduce(
todo.completed ? count + 1 : count, (count, todo) => (todo.completed ? count + 1 : count),
0 0
); );
@ -69,9 +77,9 @@ class MainSection extends Component {
<section className="main"> <section className="main">
{this.renderToggleAll(completedCount)} {this.renderToggleAll(completedCount)}
<ul className="todo-list"> <ul className="todo-list">
{filteredTodos.map(todo => {filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} {...actions} /> <TodoItem key={todo.id} todo={todo} {...actions} />
)} ))}
</ul> </ul>
{this.renderFooter(completedCount)} {this.renderFooter(completedCount)}
</section> </section>
@ -81,7 +89,7 @@ class MainSection extends Component {
MainSection.propTypes = { MainSection.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
export default MainSection; export default MainSection;

View File

@ -7,7 +7,7 @@ class TodoItem extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
editing: false editing: false,
}; };
} }
@ -25,36 +25,41 @@ class TodoItem extends Component {
} }
render() { render() {
const {todo, completeTodo, deleteTodo} = this.props; const { todo, completeTodo, deleteTodo } = this.props;
let element; let element;
if (this.state.editing) { if (this.state.editing) {
element = ( element = (
<TodoTextInput text={todo.text} <TodoTextInput
editing={this.state.editing} text={todo.text}
onSave={(text) => this.handleSave(todo.id, text)} /> editing={this.state.editing}
onSave={(text) => this.handleSave(todo.id, text)}
/>
); );
} else { } else {
element = ( element = (
<div className="view"> <div className="view">
<input className="toggle" <input
type="checkbox" className="toggle"
checked={todo.completed} type="checkbox"
onChange={() => completeTodo(todo.id)} /> checked={todo.completed}
onChange={() => completeTodo(todo.id)}
/>
<label onDoubleClick={this.handleDoubleClick.bind(this)}> <label onDoubleClick={this.handleDoubleClick.bind(this)}>
{todo.text} {todo.text}
</label> </label>
<button className="destroy" <button className="destroy" onClick={() => deleteTodo(todo.id)} />
onClick={() => deleteTodo(todo.id)} />
</div> </div>
); );
} }
return ( return (
<li className={classnames({ <li
completed: todo.completed, className={classnames({
editing: this.state.editing completed: todo.completed,
})}> editing: this.state.editing,
})}
>
{element} {element}
</li> </li>
); );
@ -65,7 +70,7 @@ TodoItem.propTypes = {
todo: PropTypes.object.isRequired, todo: PropTypes.object.isRequired,
editTodo: PropTypes.func.isRequired, editTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired, deleteTodo: PropTypes.func.isRequired,
completeTodo: PropTypes.func.isRequired completeTodo: PropTypes.func.isRequired,
}; };
export default TodoItem; export default TodoItem;

View File

@ -6,7 +6,7 @@ class TodoTextInput extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
text: this.props.text || '' text: this.props.text || '',
}; };
} }
@ -32,10 +32,10 @@ class TodoTextInput extends Component {
render() { render() {
return ( return (
<input className={ <input
classnames({ className={classnames({
edit: this.props.editing, edit: this.props.editing,
'new-todo': this.props.newTodo 'new-todo': this.props.newTodo,
})} })}
type="text" type="text"
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
@ -43,7 +43,8 @@ class TodoTextInput extends Component {
value={this.state.text} value={this.state.text}
onBlur={this.handleBlur.bind(this)} onBlur={this.handleBlur.bind(this)}
onChange={this.handleChange.bind(this)} onChange={this.handleChange.bind(this)}
onKeyDown={this.handleSubmit.bind(this)} /> onKeyDown={this.handleSubmit.bind(this)}
/>
); );
} }
} }
@ -53,7 +54,7 @@ TodoTextInput.propTypes = {
text: PropTypes.string, text: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
editing: PropTypes.bool, editing: PropTypes.bool,
newTodo: PropTypes.bool newTodo: PropTypes.bool,
}; };
export default TodoTextInput; export default TodoTextInput;

View File

@ -20,22 +20,19 @@ class App extends Component {
App.propTypes = { App.propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
todos: state.todos todos: state.todos,
}; };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
actions: bindActionCreators(TodoActions, dispatch) actions: bindActionCreators(TodoActions, dispatch),
}; };
} }
export default connect( export default connect(mapStateToProps, mapDispatchToProps)(App);
mapStateToProps,
mapDispatchToProps
)(App);

View File

@ -4,8 +4,7 @@
<title>Redux TodoMVC example</title> <title>Redux TodoMVC example</title>
</head> </head>
<body> <body>
<div class="todoapp" id="root"> <div class="todoapp" id="root"></div>
</div>
<script src="/static/bundle.js"></script> <script src="/static/bundle.js"></script>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
import todos from './todos'; import todos from './todos';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
todos todos,
}); });
export default rootReducer; export default rootReducer;

View File

@ -1,51 +1,67 @@
import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'; import {
ADD_TODO,
DELETE_TODO,
EDIT_TODO,
COMPLETE_TODO,
COMPLETE_ALL,
CLEAR_COMPLETED,
} from '../constants/ActionTypes';
const initialState = [{ const initialState = [
text: 'Use Redux', {
completed: false, text: 'Use Redux',
modified: new Date(), completed: false,
id: 0 modified: new Date(),
}]; id: 0,
},
];
export default function todos(state = initialState, action) { export default function todos(state = initialState, action) {
switch (action.type) { switch (action.type) {
case ADD_TODO: case ADD_TODO:
return [{ return [
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, {
completed: false, id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
modified: new Date(), completed: false,
text: action.text modified: new Date(),
}, ...state]; text: action.text,
},
...state,
];
case DELETE_TODO: case DELETE_TODO:
return state.filter(todo => return state.filter((todo) => todo.id !== action.id);
todo.id !== action.id
);
case EDIT_TODO: case EDIT_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { text: action.text, modified: new Date() }) : ? Object.assign({}, todo, { text: action.text, modified: new Date() })
todo : todo
); );
case COMPLETE_TODO: case COMPLETE_TODO:
return state.map(todo => return state.map((todo) =>
todo.id === action.id ? todo.id === action.id
Object.assign({}, todo, { completed: !todo.completed, modified: new Date() }) : ? Object.assign({}, todo, {
todo completed: !todo.completed,
); modified: new Date(),
})
: todo
);
case COMPLETE_ALL: case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed); const areAllMarked = state.every((todo) => todo.completed);
return state.map(todo => Object.assign({}, todo, { return state.map((todo) =>
completed: !areAllMarked, modified: new Date() Object.assign({}, todo, {
})); completed: !areAllMarked,
modified: new Date(),
})
);
case CLEAR_COMPLETED: case CLEAR_COMPLETED:
return state.filter(todo => todo.completed === false); return state.filter((todo) => todo.completed === false);
default: default:
return state; return state;
} }
} }

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4002; var port = 4002;
var compiler = webpack(config); var compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(
webpackDevMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath,
})
);
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
app.get("/", function(req, res) { app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.listen(port, function(error) { app.listen(port, function (error) {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); console.info(
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
port,
port
);
} }
}); });

View File

@ -3,11 +3,18 @@ import rootReducer from '../reducers';
import * as actionCreators from '../actions'; import * as actionCreators from '../actions';
export default function configureStore(preloadedState) { export default function configureStore(preloadedState) {
const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ && const enhancer =
window.__REDUX_DEVTOOLS_EXTENSION__({ actionCreators, serialize: true, trace: true }); window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
actionCreators,
serialize: true,
trace: true,
});
if (!enhancer) { if (!enhancer) {
console.warn('Install Redux DevTools Extension to inspect the app state: ' + console.warn(
'https://github.com/zalmoxisus/redux-devtools-extension#installation') 'Install Redux DevTools Extension to inspect the app state: ' +
'https://github.com/zalmoxisus/redux-devtools-extension#installation'
);
} }
const store = createStore(rootReducer, preloadedState, enhancer); const store = createStore(rootReducer, preloadedState, enhancer);
@ -15,7 +22,7 @@ export default function configureStore(preloadedState) {
if (module.hot) { if (module.hot) {
// Enable Webpack hot module replacement for reducers // Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => { module.hot.accept('../reducers', () => {
store.replaceReducer(require('../reducers').default) store.replaceReducer(require('../reducers').default);
}); });
} }

View File

@ -6,14 +6,14 @@ describe('todo actions', () => {
it('addTodo should create ADD_TODO action', () => { it('addTodo should create ADD_TODO action', () => {
expect(actions.addTodo('Use Redux')).toEqual({ expect(actions.addTodo('Use Redux')).toEqual({
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Use Redux' text: 'Use Redux',
}); });
}); });
it('deleteTodo should create DELETE_TODO action', () => { it('deleteTodo should create DELETE_TODO action', () => {
expect(actions.deleteTodo(1)).toEqual({ expect(actions.deleteTodo(1)).toEqual({
type: types.DELETE_TODO, type: types.DELETE_TODO,
id: 1 id: 1,
}); });
}); });
@ -21,26 +21,26 @@ describe('todo actions', () => {
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
type: types.EDIT_TODO, type: types.EDIT_TODO,
id: 1, id: 1,
text: 'Use Redux everywhere' text: 'Use Redux everywhere',
}); });
}); });
it('completeTodo should create COMPLETE_TODO action', () => { it('completeTodo should create COMPLETE_TODO action', () => {
expect(actions.completeTodo(1)).toEqual({ expect(actions.completeTodo(1)).toEqual({
type: types.COMPLETE_TODO, type: types.COMPLETE_TODO,
id: 1 id: 1,
}); });
}); });
it('completeAll should create COMPLETE_ALL action', () => { it('completeAll should create COMPLETE_ALL action', () => {
expect(actions.completeAll()).toEqual({ expect(actions.completeAll()).toEqual({
type: types.COMPLETE_ALL type: types.COMPLETE_ALL,
}); });
}); });
it('clearCompleted should create CLEAR_COMPLETED action', () => { it('clearCompleted should create CLEAR_COMPLETED action', () => {
expect(actions.clearCompleted('Use Redux')).toEqual({ expect(actions.clearCompleted('Use Redux')).toEqual({
type: types.CLEAR_COMPLETED type: types.CLEAR_COMPLETED,
}); });
}); });
}); });

View File

@ -5,13 +5,16 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
completedCount: 0, {
activeCount: 0, completedCount: 0,
filter: SHOW_ALL, activeCount: 0,
onClearCompleted: expect.createSpy(), filter: SHOW_ALL,
onShow: expect.createSpy() onClearCompleted: expect.createSpy(),
}, propOverrides); onShow: expect.createSpy(),
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<Footer {...props} />); renderer.render(<Footer {...props} />);
@ -19,13 +22,14 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output output: output,
}; };
} }
function getTextContent(elem) { function getTextContent(elem) {
const children = Array.isArray(elem.props.children) ? const children = Array.isArray(elem.props.children)
elem.props.children : [elem.props.children]; ? elem.props.children
: [elem.props.children];
return children.reduce(function concatText(out, child) { return children.reduce(function concatText(out, child) {
// Children are either elements or text strings // Children are either elements or text strings
@ -63,11 +67,13 @@ describe('components', () => {
expect(filter.type).toBe('li'); expect(filter.type).toBe('li');
const a = filter.props.children; const a = filter.props.children;
expect(a.props.className).toBe(i === 0 ? 'selected' : ''); expect(a.props.className).toBe(i === 0 ? 'selected' : '');
expect(a.props.children).toBe({ expect(a.props.children).toBe(
0: 'All', {
1: 'Active', 0: 'All',
2: 'Completed' 1: 'Active',
}[i]); 2: 'Completed',
}[i]
);
}); });
}); });
@ -81,20 +87,20 @@ describe('components', () => {
it('shouldnt show clear button when no completed todos', () => { it('shouldnt show clear button when no completed todos', () => {
const { output } = setup({ completedCount: 0 }); const { output } = setup({ completedCount: 0 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear).toBe(undefined); expect(clear).toBe(undefined);
}); });
it('should render clear button when completed todos', () => { it('should render clear button when completed todos', () => {
const { output } = setup({ completedCount: 1 }); const { output } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
expect(clear.type).toBe('button'); expect(clear.type).toBe('button');
expect(clear.props.children).toBe('Clear completed'); expect(clear.props.children).toBe('Clear completed');
}); });
it('should call onClearCompleted on clear button click', () => { it('should call onClearCompleted on clear button click', () => {
const { output, props } = setup({ completedCount: 1 }); const { output, props } = setup({ completedCount: 1 });
const [,, clear] = output.props.children; const [, , clear] = output.props.children;
clear.props.onClick({}); clear.props.onClick({});
expect(props.onClearCompleted).toHaveBeenCalled(); expect(props.onClearCompleted).toHaveBeenCalled();
}); });

View File

@ -6,7 +6,7 @@ import TodoTextInput from '../../components/TodoTextInput';
function setup() { function setup() {
const props = { const props = {
addTodo: expect.createSpy() addTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
@ -16,7 +16,7 @@ function setup() {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -7,24 +7,30 @@ import Footer from '../../components/Footer';
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters'; import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
todos: [{ {
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}, { completed: false,
text: 'Run the tests', id: 0,
completed: true, },
id: 1 {
}], text: 'Run the tests',
actions: { completed: true,
editTodo: expect.createSpy(), id: 1,
deleteTodo: expect.createSpy(), },
completeTodo: expect.createSpy(), ],
completeAll: expect.createSpy(), actions: {
clearCompleted: expect.createSpy() editTodo: expect.createSpy(),
} deleteTodo: expect.createSpy(),
}, propOverrides); completeTodo: expect.createSpy(),
completeAll: expect.createSpy(),
clearCompleted: expect.createSpy(),
},
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render(<MainSection {...props} />); renderer.render(<MainSection {...props} />);
@ -33,7 +39,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -55,11 +61,15 @@ describe('components', () => {
}); });
it('should be checked if all todos completed', () => { it('should be checked if all todos completed', () => {
const { output } = setup({ todos: [{ const { output } = setup({
text: 'Use Redux', todos: [
completed: true, {
id: 0 text: 'Use Redux',
}]}); completed: true,
id: 0,
},
],
});
const [toggle] = output.props.children; const [toggle] = output.props.children;
expect(toggle.props.checked).toBe(true); expect(toggle.props.checked).toBe(true);
}); });
@ -75,7 +85,7 @@ describe('components', () => {
describe('footer', () => { describe('footer', () => {
it('should render', () => { it('should render', () => {
const { output } = setup(); const { output } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
expect(footer.type).toBe(Footer); expect(footer.type).toBe(Footer);
expect(footer.props.completedCount).toBe(1); expect(footer.props.completedCount).toBe(1);
expect(footer.props.activeCount).toBe(1); expect(footer.props.activeCount).toBe(1);
@ -84,27 +94,31 @@ describe('components', () => {
it('onShow should set the filter', () => { it('onShow should set the filter', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [,, updatedFooter] = updated.props.children; const [, , updatedFooter] = updated.props.children;
expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED); expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED);
}); });
it('onClearCompleted should call clearCompleted', () => { it('onClearCompleted should call clearCompleted', () => {
const { output, props } = setup(); const { output, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted).toHaveBeenCalled(); expect(props.actions.clearCompleted).toHaveBeenCalled();
}); });
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => { it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
const { output, props } = setup({ todos: [{ const { output, props } = setup({
text: 'Use Redux', todos: [
completed: false, {
id: 0 text: 'Use Redux',
}]}); completed: false,
const [,, footer] = output.props.children; id: 0,
},
],
});
const [, , footer] = output.props.children;
footer.props.onClearCompleted(); footer.props.onClearCompleted();
expect(props.actions.clearCompleted.calls.length).toBe(0); expect(props.actions.clearCompleted.calls.length).toBe(0);
}); });
@ -124,7 +138,7 @@ describe('components', () => {
it('should filter items', () => { it('should filter items', () => {
const { output, renderer, props } = setup(); const { output, renderer, props } = setup();
const [,, footer] = output.props.children; const [, , footer] = output.props.children;
footer.props.onShow(SHOW_COMPLETED); footer.props.onShow(SHOW_COMPLETED);
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
const [, updatedList] = updated.props.children; const [, updatedList] = updated.props.children;

View File

@ -4,23 +4,21 @@ import TestUtils from 'react-addons-test-utils';
import TodoItem from '../../components/TodoItem'; import TodoItem from '../../components/TodoItem';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup( editing = false ) { function setup(editing = false) {
const props = { const props = {
todo: { todo: {
id: 0, id: 0,
text: 'Use Redux', text: 'Use Redux',
completed: false completed: false,
}, },
editTodo: expect.createSpy(), editTodo: expect.createSpy(),
deleteTodo: expect.createSpy(), deleteTodo: expect.createSpy(),
completeTodo: expect.createSpy() completeTodo: expect.createSpy(),
}; };
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoItem {...props} />);
<TodoItem {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -33,7 +31,7 @@ function setup( editing = false ) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }

View File

@ -4,19 +4,20 @@ import TestUtils from 'react-addons-test-utils';
import TodoTextInput from '../../components/TodoTextInput'; import TodoTextInput from '../../components/TodoTextInput';
function setup(propOverrides) { function setup(propOverrides) {
const props = Object.assign({ const props = Object.assign(
onSave: expect.createSpy(), {
text: 'Use Redux', onSave: expect.createSpy(),
placeholder: 'What needs to be done?', text: 'Use Redux',
editing: false, placeholder: 'What needs to be done?',
newTodo: false editing: false,
}, propOverrides); newTodo: false,
},
propOverrides
);
const renderer = TestUtils.createRenderer(); const renderer = TestUtils.createRenderer();
renderer.render( renderer.render(<TodoTextInput {...props} />);
<TodoTextInput {...props} />
);
let output = renderer.getRenderOutput(); let output = renderer.getRenderOutput();
@ -25,7 +26,7 @@ function setup(propOverrides) {
return { return {
props: props, props: props,
output: output, output: output,
renderer: renderer renderer: renderer,
}; };
} }
@ -50,33 +51,33 @@ describe('components', () => {
it('should update value on change', () => { it('should update value on change', () => {
const { output, renderer } = setup(); const { output, renderer } = setup();
output.props.onChange({ target: { value: 'Use Radox' }}); output.props.onChange({ target: { value: 'Use Radox' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual('Use Radox'); expect(updated.props.value).toEqual('Use Radox');
}); });
it('should call onSave on return key press', () => { it('should call onSave on return key press', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('should reset state on return key press if newTodo', () => { it('should reset state on return key press if newTodo', () => {
const { output, renderer } = setup({ newTodo: true }); const { output, renderer } = setup({ newTodo: true });
output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' }}); output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } });
const updated = renderer.getRenderOutput(); const updated = renderer.getRenderOutput();
expect(updated.props.value).toEqual(''); expect(updated.props.value).toEqual('');
}); });
it('should call onSave on blur', () => { it('should call onSave on blur', () => {
const { output, props } = setup(); const { output, props } = setup();
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave).toHaveBeenCalledWith('Use Redux'); expect(props.onSave).toHaveBeenCalledWith('Use Redux');
}); });
it('shouldnt call onSave on blur if newTodo', () => { it('shouldnt call onSave on blur if newTodo', () => {
const { output, props } = setup({ newTodo: true }); const { output, props } = setup({ newTodo: true });
output.props.onBlur({ target: { value: 'Use Redux' }}); output.props.onBlur({ target: { value: 'Use Redux' } });
expect(props.onSave.calls.length).toBe(0); expect(props.onSave.calls.length).toBe(0);
}); });
}); });

View File

@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
describe('todos reducer', () => { describe('todos reducer', () => {
it('should handle initial state', () => { it('should handle initial state', () => {
expect( expect(todos(undefined, {})).toEqual([
todos(undefined, {}) {
).toEqual([{ text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ]);
}); });
it('should handle ADD_TODO', () => { it('should handle ADD_TODO', () => {
expect( expect(
todos([], { todos([], {
type: types.ADD_TODO, type: types.ADD_TODO,
text: 'Run the tests' text: 'Run the tests',
}) })
).toEqual([{ ).toEqual([
text: 'Run the tests', {
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.ADD_TODO,
text: 'Run the tests'
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
expect(
todos([{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: false,
id: 1 id: 0,
}, { },
]);
expect(
todos(
[
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.ADD_TODO,
text: 'Run the tests',
}
)
).toEqual([
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.ADD_TODO, ]);
text: 'Fix the tests'
}) expect(
).toEqual([{ todos(
text: 'Fix the tests', [
completed: false, {
id: 2 text: 'Run the tests',
}, { completed: false,
text: 'Run the tests', id: 1,
completed: false, },
id: 1 {
}, { text: 'Use Redux',
text: 'Use Redux', completed: false,
completed: false, id: 0,
id: 0 },
}]); ],
{
type: types.ADD_TODO,
text: 'Fix the tests',
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false,
id: 2,
},
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
]);
}); });
it('should handle DELETE_TODO', () => { it('should handle DELETE_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: false, {
id: 1 text: 'Run the tests',
}, { completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.DELETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.DELETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle EDIT_TODO', () => { it('should handle EDIT_TODO', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.EDIT_TODO,
text: 'Fix the tests',
id: 1,
}
)
).toEqual([
{
text: 'Fix the tests',
completed: false, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.EDIT_TODO, ]);
text: 'Fix the tests',
id: 1
})
).toEqual([{
text: 'Fix the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_TODO', () => { it('should handle COMPLETE_TODO', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_TODO,
id: 1,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: false, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_TODO, ]);
id: 1
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle COMPLETE_ALL', () => { it('should handle COMPLETE_ALL', () => {
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: true,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: true,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: true,
id: 0
}]);
// Unmark if all todos are currently completed // Unmark if all todos are currently completed
expect( expect(
todos([{ todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: true,
id: 0,
},
],
{
type: types.COMPLETE_ALL,
}
)
).toEqual([
{
text: 'Run the tests', text: 'Run the tests',
completed: true, completed: false,
id: 1 id: 1,
}, { },
{
text: 'Use Redux', text: 'Use Redux',
completed: true, completed: false,
id: 0 id: 0,
}], { },
type: types.COMPLETE_ALL ]);
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should handle CLEAR_COMPLETED', () => { it('should handle CLEAR_COMPLETED', () => {
expect( expect(
todos([{ todos(
text: 'Run the tests', [
completed: true, {
id: 1 text: 'Run the tests',
}, { completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.CLEAR_COMPLETED,
}
)
).toEqual([
{
text: 'Use Redux', text: 'Use Redux',
completed: false, completed: false,
id: 0 id: 0,
}], { },
type: types.CLEAR_COMPLETED ]);
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
}); });
it('should not generate duplicate ids after CLEAR_COMPLETED', () => { it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
expect( expect(
[{ [
type: types.COMPLETE_TODO, {
id: 0 type: types.COMPLETE_TODO,
}, { id: 0,
type: types.CLEAR_COMPLETED },
}, { {
type: types.ADD_TODO, type: types.CLEAR_COMPLETED,
text: 'Write more tests' },
}].reduce(todos, [{ {
id: 0, type: types.ADD_TODO,
text: 'Write more tests',
},
].reduce(todos, [
{
id: 0,
completed: false,
text: 'Use Redux',
},
{
id: 1,
completed: false,
text: 'Write tests',
},
])
).toEqual([
{
text: 'Write more tests',
completed: false,
id: 2,
},
{
text: 'Write tests',
completed: false, completed: false,
text: 'Use Redux'
}, {
id: 1, id: 1,
completed: false, },
text: 'Write tests' ]);
}])
).toEqual([{
text: 'Write more tests',
completed: false,
id: 2
}, {
text: 'Write tests',
completed: false,
id: 1
}]);
}); });
}); });

View File

@ -4,27 +4,25 @@ var webpack = require('webpack');
module.exports = { module.exports = {
mode: 'development', mode: 'development',
devtool: 'source-map', devtool: 'source-map',
entry: [ entry: ['webpack-hot-middleware/client', './index'],
'webpack-hot-middleware/client',
'./index'
],
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/' publicPath: '/static/',
}, },
plugins: [ plugins: [new webpack.HotModuleReplacementPlugin()],
new webpack.HotModuleReplacementPlugin()
],
module: { module: {
rules: [{ rules: [
test: /\.js$/, {
loaders: ['babel-loader'], test: /\.js$/,
exclude: /node_modules/ loaders: ['babel-loader'],
}, { exclude: /node_modules/,
test: /\.css?$/, },
loaders: ['style-loader', 'raw-loader'], {
include: __dirname test: /\.css?$/,
}] loaders: ['style-loader', 'raw-loader'],
} include: __dirname,
},
],
},
}; };

View File

@ -19,14 +19,19 @@ function copy(dest) {
* common tasks * common tasks
*/ */
gulp.task('replace-webpack-code', () => { gulp.task('replace-webpack-code', () => {
const replaceTasks = [{ const replaceTasks = [
from: './webpack/replace/JsonpMainTemplate.runtime.js', {
to: './node_modules/webpack/lib/JsonpMainTemplate.runtime.js' from: './webpack/replace/JsonpMainTemplate.runtime.js',
}, { to: './node_modules/webpack/lib/JsonpMainTemplate.runtime.js',
from: './webpack/replace/log-apply-result.js', },
to: './node_modules/webpack/hot/log-apply-result.js' {
}]; from: './webpack/replace/log-apply-result.js',
replaceTasks.forEach(task => fs.writeFileSync(task.to, fs.readFileSync(task.from))); to: './node_modules/webpack/hot/log-apply-result.js',
},
];
replaceTasks.forEach((task) =>
fs.writeFileSync(task.to, fs.readFileSync(task.from))
);
}); });
/* /*
@ -44,15 +49,19 @@ gulp.task('webpack:dev', (callback) => {
}); });
gulp.task('views:dev', () => { gulp.task('views:dev', () => {
gulp.src('./src/browser/views/*.pug') gulp
.pipe(jade({ .src('./src/browser/views/*.pug')
locals: { env: 'dev' } .pipe(
})) jade({
locals: { env: 'dev' },
})
)
.pipe(gulp.dest('./dev')); .pipe(gulp.dest('./dev'));
}); });
gulp.task('copy:dev', () => { gulp.task('copy:dev', () => {
gulp.src('./src/browser/extension/manifest.json') gulp
.src('./src/browser/extension/manifest.json')
.pipe(rename('manifest.json')) .pipe(rename('manifest.json'))
.pipe(gulp.dest('./dev')); .pipe(gulp.dest('./dev'));
copy('./dev'); copy('./dev');
@ -87,29 +96,34 @@ gulp.task('webpack:build:extension', (callback) => {
}); });
gulp.task('views:build:extension', () => { gulp.task('views:build:extension', () => {
gulp.src([ gulp
'./src/browser/views/*.pug' .src(['./src/browser/views/*.pug'])
]) .pipe(
.pipe(jade({ jade({
locals: { env: 'prod' } locals: { env: 'prod' },
})) })
)
.pipe(gulp.dest('./build/extension')); .pipe(gulp.dest('./build/extension'));
}); });
gulp.task('copy:build:extension', () => { gulp.task('copy:build:extension', () => {
gulp.src('./src/browser/extension/manifest.json') gulp
.src('./src/browser/extension/manifest.json')
.pipe(rename('manifest.json')) .pipe(rename('manifest.json'))
.pipe(gulp.dest('./build/extension')); .pipe(gulp.dest('./build/extension'));
copy('./build/extension'); copy('./build/extension');
}); });
gulp.task('copy:build:firefox', ['build:extension'], () => { gulp.task('copy:build:firefox', ['build:extension'], () => {
gulp.src([ gulp
'./build/extension/**', '!./build/extension/js/redux-devtools-extension.js' .src([
]) './build/extension/**',
'!./build/extension/js/redux-devtools-extension.js',
])
.pipe(gulp.dest('./build/firefox')) .pipe(gulp.dest('./build/firefox'))
.on('finish', function() { .on('finish', function () {
gulp.src('./src/browser/firefox/manifest.json') gulp
.src('./src/browser/firefox/manifest.json')
.pipe(gulp.dest('./build/firefox')); .pipe(gulp.dest('./build/firefox'));
}); });
copy('./build/firefox'); copy('./build/firefox');
@ -120,13 +134,15 @@ gulp.task('copy:build:firefox', ['build:extension'], () => {
*/ */
gulp.task('compress:extension', () => { gulp.task('compress:extension', () => {
gulp.src('build/extension/**') gulp
.src('build/extension/**')
.pipe(zip('extension.zip')) .pipe(zip('extension.zip'))
.pipe(gulp.dest('./build')); .pipe(gulp.dest('./build'));
}); });
gulp.task('compress:firefox', () => { gulp.task('compress:firefox', () => {
gulp.src('build/firefox/**') gulp
.src('build/firefox/**')
.pipe(zip('firefox.zip')) .pipe(zip('firefox.zip'))
.pipe(gulp.dest('./build')); .pipe(gulp.dest('./build'));
}); });
@ -140,23 +156,40 @@ gulp.task('views:watch', () => {
}); });
gulp.task('copy:watch', () => { gulp.task('copy:watch', () => {
gulp.watch(['./src/browser/extension/manifest.json', './src/assets/**/*'], ['copy:dev']); gulp.watch(
['./src/browser/extension/manifest.json', './src/assets/**/*'],
['copy:dev']
);
}); });
gulp.task('test:chrome', () => { gulp.task('test:chrome', () => {
crdv.start(); crdv.start();
return gulp.src('./test/chrome/*.spec.js') return gulp
.src('./test/chrome/*.spec.js')
.pipe(mocha({ require: ['babel-polyfill', 'co-mocha'] })) .pipe(mocha({ require: ['babel-polyfill', 'co-mocha'] }))
.on('end', () => crdv.stop()); .on('end', () => crdv.stop());
}); });
gulp.task('test:electron', () => { gulp.task('test:electron', () => {
crdv.start(); crdv.start();
return gulp.src('./test/electron/*.spec.js') return gulp
.src('./test/electron/*.spec.js')
.pipe(mocha({ require: ['babel-polyfill', 'co-mocha'] })) .pipe(mocha({ require: ['babel-polyfill', 'co-mocha'] }))
.on('end', () => crdv.stop()); .on('end', () => crdv.stop());
}); });
gulp.task('default', ['replace-webpack-code', 'webpack:dev', 'views:dev', 'copy:dev', 'views:watch', 'copy:watch']); gulp.task('default', [
gulp.task('build:extension', ['replace-webpack-code', 'webpack:build:extension', 'views:build:extension', 'copy:build:extension']); 'replace-webpack-code',
'webpack:dev',
'views:dev',
'copy:dev',
'views:watch',
'copy:watch',
]);
gulp.task('build:extension', [
'replace-webpack-code',
'webpack:build:extension',
'views:build:extension',
'copy:build:extension',
]);
gulp.task('build:firefox', ['copy:build:firefox']); gulp.task('build:firefox', ['copy:build:firefox']);

View File

@ -3,55 +3,72 @@ import mapValues from 'lodash/mapValues';
export const FilterState = { export const FilterState = {
DO_NOT_FILTER: 'DO_NOT_FILTER', DO_NOT_FILTER: 'DO_NOT_FILTER',
BLACKLIST_SPECIFIC: 'BLACKLIST_SPECIFIC', BLACKLIST_SPECIFIC: 'BLACKLIST_SPECIFIC',
WHITELIST_SPECIFIC: 'WHITELIST_SPECIFIC' WHITELIST_SPECIFIC: 'WHITELIST_SPECIFIC',
}; };
export function getLocalFilter(config) { export function getLocalFilter(config) {
if (config.actionsBlacklist || config.actionsWhitelist) { if (config.actionsBlacklist || config.actionsWhitelist) {
return { return {
whitelist: Array.isArray(config.actionsWhitelist) ? config.actionsWhitelist.join('|') : config.actionsWhitelist, whitelist: Array.isArray(config.actionsWhitelist)
blacklist: Array.isArray(config.actionsBlacklist) ? config.actionsBlacklist.join('|') : config.actionsBlacklist ? config.actionsWhitelist.join('|')
: config.actionsWhitelist,
blacklist: Array.isArray(config.actionsBlacklist)
? config.actionsBlacklist.join('|')
: config.actionsBlacklist,
}; };
} }
return undefined; return undefined;
} }
export const noFiltersApplied = (localFilter) => ( export const noFiltersApplied = (localFilter) =>
// !predicate && // !predicate &&
!localFilter && (!window.devToolsOptions || !window.devToolsOptions.filter || !localFilter &&
window.devToolsOptions.filter === FilterState.DO_NOT_FILTER) (!window.devToolsOptions ||
); !window.devToolsOptions.filter ||
window.devToolsOptions.filter === FilterState.DO_NOT_FILTER);
export function isFiltered(action, localFilter) { export function isFiltered(action, localFilter) {
if ( if (
noFiltersApplied(localFilter) || noFiltersApplied(localFilter) ||
typeof action !== 'string' && typeof action.type.match !== 'function' (typeof action !== 'string' && typeof action.type.match !== 'function')
) return false; )
return false;
const { whitelist, blacklist } = localFilter || window.devToolsOptions || {}; const { whitelist, blacklist } = localFilter || window.devToolsOptions || {};
const actionType = action.type || action; const actionType = action.type || action;
return ( return (
whitelist && !actionType.match(whitelist) || (whitelist && !actionType.match(whitelist)) ||
blacklist && actionType.match(blacklist) (blacklist && actionType.match(blacklist))
); );
} }
function filterActions(actionsById, actionSanitizer) { function filterActions(actionsById, actionSanitizer) {
if (!actionSanitizer) return actionsById; if (!actionSanitizer) return actionsById;
return mapValues(actionsById, (action, id) => ( return mapValues(actionsById, (action, id) => ({
{ ...action, action: actionSanitizer(action.action, id) } ...action,
)); action: actionSanitizer(action.action, id),
}));
} }
function filterStates(computedStates, stateSanitizer) { function filterStates(computedStates, stateSanitizer) {
if (!stateSanitizer) return computedStates; if (!stateSanitizer) return computedStates;
return computedStates.map((state, idx) => ( return computedStates.map((state, idx) => ({
{ ...state, state: stateSanitizer(state.state, idx) } ...state,
)); state: stateSanitizer(state.state, idx),
}));
} }
export function filterState(state, type, localFilter, stateSanitizer, actionSanitizer, nextActionId, predicate) { export function filterState(
if (type === 'ACTION') return !stateSanitizer ? state : stateSanitizer(state, nextActionId - 1); state,
type,
localFilter,
stateSanitizer,
actionSanitizer,
nextActionId,
predicate
) {
if (type === 'ACTION')
return !stateSanitizer ? state : stateSanitizer(state, nextActionId - 1);
else if (type !== 'STATE') return state; else if (type !== 'STATE') return state;
if (predicate || !noFiltersApplied(localFilter)) { if (predicate || !noFiltersApplied(localFilter)) {
@ -74,11 +91,14 @@ export function filterState(state, type, localFilter, stateSanitizer, actionSani
filteredStagedActionIds.push(id); filteredStagedActionIds.push(id);
filteredComputedStates.push( filteredComputedStates.push(
stateSanitizer ? { ...liftedState, state: stateSanitizer(currState, idx) } : liftedState stateSanitizer
? { ...liftedState, state: stateSanitizer(currState, idx) }
: liftedState
); );
if (actionSanitizer) { if (actionSanitizer) {
sanitizedActionsById[id] = { sanitizedActionsById[id] = {
...liftedAction, action: actionSanitizer(currAction, id) ...liftedAction,
action: actionSanitizer(currAction, id),
}; };
} }
}); });
@ -87,7 +107,7 @@ export function filterState(state, type, localFilter, stateSanitizer, actionSani
...state, ...state,
actionsById: sanitizedActionsById || actionsById, actionsById: sanitizedActionsById || actionsById,
stagedActionIds: filteredStagedActionIds, stagedActionIds: filteredStagedActionIds,
computedStates: filteredComputedStates computedStates: filteredComputedStates,
}; };
} }
@ -95,12 +115,17 @@ export function filterState(state, type, localFilter, stateSanitizer, actionSani
return { return {
...state, ...state,
actionsById: filterActions(state.actionsById, actionSanitizer), actionsById: filterActions(state.actionsById, actionSanitizer),
computedStates: filterStates(state.computedStates, stateSanitizer) computedStates: filterStates(state.computedStates, stateSanitizer),
}; };
} }
export function startingFrom( export function startingFrom(
sendingActionId, state, localFilter, stateSanitizer, actionSanitizer, predicate sendingActionId,
state,
localFilter,
stateSanitizer,
actionSanitizer,
predicate
) { ) {
const stagedActionIds = state.stagedActionIds; const stagedActionIds = state.stagedActionIds;
if (sendingActionId <= stagedActionIds[1]) return state; if (sendingActionId <= stagedActionIds[1]) return state;
@ -124,17 +149,21 @@ export function startingFrom(
if (shouldFilter) { if (shouldFilter) {
if ( if (
predicate && !predicate(currState.state, currAction.action) || (predicate && !predicate(currState.state, currAction.action)) ||
isFiltered(currAction.action, localFilter) isFiltered(currAction.action, localFilter)
) continue; )
continue;
filteredStagedActionIds.push(key); filteredStagedActionIds.push(key);
if (i < index) continue; if (i < index) continue;
} }
newActionsById[key] = !actionSanitizer ? currAction : newActionsById[key] = !actionSanitizer
{ ...currAction, action: actionSanitizer(currAction.action, key) }; ? currAction
: { ...currAction, action: actionSanitizer(currAction.action, key) };
newComputedStates.push( newComputedStates.push(
!stateSanitizer ? currState : { ...currState, state: stateSanitizer(currState.state, i) } !stateSanitizer
? currState
: { ...currState, state: stateSanitizer(currState.state, i) }
); );
} }
@ -145,6 +174,6 @@ export function startingFrom(
computedStates: newComputedStates, computedStates: newComputedStates,
stagedActionIds: filteredStagedActionIds, stagedActionIds: filteredStagedActionIds,
currentStateIndex: state.currentStateIndex, currentStateIndex: state.currentStateIndex,
nextActionId: state.nextActionId nextActionId: state.nextActionId,
}; };
} }

View File

@ -3,38 +3,55 @@ import jsan from 'jsan';
import seralizeImmutable from 'remotedev-serialize/immutable/serialize'; import seralizeImmutable from 'remotedev-serialize/immutable/serialize';
function deprecate(param) { function deprecate(param) {
console.warn(`\`${param}\` parameter for Redux DevTools Extension is deprecated. Use \`serialize\` parameter instead: https://github.com/zalmoxisus/redux-devtools-extension/releases/tag/v2.12.1`); // eslint-disable-line console.warn(
`\`${param}\` parameter for Redux DevTools Extension is deprecated. Use \`serialize\` parameter instead: https://github.com/zalmoxisus/redux-devtools-extension/releases/tag/v2.12.1`
); // eslint-disable-line
} }
export default function importState(state, { deserializeState, deserializeAction, serialize }) { export default function importState(
state,
{ deserializeState, deserializeAction, serialize }
) {
if (!state) return undefined; if (!state) return undefined;
let parse = jsan.parse; let parse = jsan.parse;
if (serialize) { if (serialize) {
if (serialize.immutable) { if (serialize.immutable) {
parse = v => jsan.parse(v, seralizeImmutable( parse = (v) =>
serialize.immutable, serialize.refs, serialize.replacer, serialize.reviver jsan.parse(
).reviver); v,
seralizeImmutable(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
).reviver
);
} else if (serialize.reviver) { } else if (serialize.reviver) {
parse = v => jsan.parse(v, serialize.reviver); parse = (v) => jsan.parse(v, serialize.reviver);
} }
} }
let preloadedState; let preloadedState;
let nextLiftedState = parse(state); let nextLiftedState = parse(state);
if (nextLiftedState.payload) { if (nextLiftedState.payload) {
if (nextLiftedState.preloadedState) preloadedState = parse(nextLiftedState.preloadedState); if (nextLiftedState.preloadedState)
preloadedState = parse(nextLiftedState.preloadedState);
nextLiftedState = parse(nextLiftedState.payload); nextLiftedState = parse(nextLiftedState.payload);
} }
if (deserializeState) { if (deserializeState) {
deprecate('deserializeState'); deprecate('deserializeState');
if (typeof nextLiftedState.computedStates !== 'undefined') { if (typeof nextLiftedState.computedStates !== 'undefined') {
nextLiftedState.computedStates = nextLiftedState.computedStates.map(computedState => ({ nextLiftedState.computedStates = nextLiftedState.computedStates.map(
...computedState, (computedState) => ({
state: deserializeState(computedState.state) ...computedState,
})); state: deserializeState(computedState.state),
})
);
} }
if (typeof nextLiftedState.committedState !== 'undefined') { if (typeof nextLiftedState.committedState !== 'undefined') {
nextLiftedState.committedState = deserializeState(nextLiftedState.committedState); nextLiftedState.committedState = deserializeState(
nextLiftedState.committedState
);
} }
if (typeof preloadedState !== 'undefined') { if (typeof preloadedState !== 'undefined') {
preloadedState = deserializeState(preloadedState); preloadedState = deserializeState(preloadedState);
@ -42,10 +59,13 @@ export default function importState(state, { deserializeState, deserializeAction
} }
if (deserializeAction) { if (deserializeAction) {
deprecate('deserializeAction'); deprecate('deserializeAction');
nextLiftedState.actionsById = mapValues(nextLiftedState.actionsById, liftedAction => ({ nextLiftedState.actionsById = mapValues(
...liftedAction, nextLiftedState.actionsById,
action: deserializeAction(liftedAction.action) (liftedAction) => ({
})); ...liftedAction,
action: deserializeAction(liftedAction.action),
})
);
} }
return { nextLiftedState, preloadedState }; return { nextLiftedState, preloadedState };

View File

@ -21,20 +21,29 @@ function tryCatchStringify(obj) {
return JSON.stringify(obj); return JSON.stringify(obj);
} catch (err) { } catch (err) {
/* eslint-disable no-console */ /* eslint-disable no-console */
if (process.env.NODE_ENV !== 'production') console.log('Failed to stringify', err); if (process.env.NODE_ENV !== 'production')
console.log('Failed to stringify', err);
/* eslint-enable no-console */ /* eslint-enable no-console */
return jsan.stringify(obj, windowReplacer, null, { circular: '[CIRCULAR]', date: true }); return jsan.stringify(obj, windowReplacer, null, {
circular: '[CIRCULAR]',
date: true,
});
} }
} }
let stringifyWarned; let stringifyWarned;
function stringify(obj, serialize) { function stringify(obj, serialize) {
const str = typeof serialize === 'undefined' ? tryCatchStringify(obj) : const str =
jsan.stringify(obj, serialize.replacer, null, serialize.options); typeof serialize === 'undefined'
? tryCatchStringify(obj)
: jsan.stringify(obj, serialize.replacer, null, serialize.options);
if (!stringifyWarned && str && str.length > 16 * 1024 * 1024) { // 16 MB if (!stringifyWarned && str && str.length > 16 * 1024 * 1024) {
// 16 MB
/* eslint-disable no-console */ /* eslint-disable no-console */
console.warn('Application state or actions payloads are too large making Redux DevTools serialization slow and consuming a lot of memory. See https://git.io/fpcP5 on how to configure it.'); console.warn(
'Application state or actions payloads are too large making Redux DevTools serialization slow and consuming a lot of memory. See https://git.io/fpcP5 on how to configure it.'
);
/* eslint-enable no-console */ /* eslint-enable no-console */
stringifyWarned = true; stringifyWarned = true;
} }
@ -48,22 +57,34 @@ export function getSeralizeParameter(config, param) {
if (serialize === true) return { options: true }; if (serialize === true) return { options: true };
if (serialize.immutable) { if (serialize.immutable) {
const immutableSerializer = seralizeImmutable( const immutableSerializer = seralizeImmutable(
serialize.immutable, serialize.refs, serialize.replacer, serialize.reviver serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
); );
return { return {
replacer: immutableSerializer.replacer, replacer: immutableSerializer.replacer,
reviver: immutableSerializer.reviver, reviver: immutableSerializer.reviver,
options: typeof serialize.options === 'object' ? options:
{ ...immutableSerializer.options, ...serialize.options } : immutableSerializer.options typeof serialize.options === 'object'
? { ...immutableSerializer.options, ...serialize.options }
: immutableSerializer.options,
}; };
} }
if (!serialize.replacer && !serialize.reviver) return { options: serialize.options }; if (!serialize.replacer && !serialize.reviver)
return { replacer: serialize.replacer, reviver: serialize.reviver, options: serialize.options || true }; return { options: serialize.options };
return {
replacer: serialize.replacer,
reviver: serialize.reviver,
options: serialize.options || true,
};
} }
const value = config[param]; const value = config[param];
if (typeof value === 'undefined') return undefined; if (typeof value === 'undefined') return undefined;
console.warn(`\`${param}\` parameter for Redux DevTools Extension is deprecated. Use \`serialize\` parameter instead: https://github.com/zalmoxisus/redux-devtools-extension/releases/tag/v2.12.1`); // eslint-disable-line console.warn(
`\`${param}\` parameter for Redux DevTools Extension is deprecated. Use \`serialize\` parameter instead: https://github.com/zalmoxisus/redux-devtools-extension/releases/tag/v2.12.1`
); // eslint-disable-line
if (typeof serializeState === 'boolean') return { options: value }; if (typeof serializeState === 'boolean') return { options: value };
if (typeof serializeState === 'function') return { replacer: value }; if (typeof serializeState === 'function') return { replacer: value };
@ -94,10 +115,16 @@ function getStackTrace(config, toExcludeFromTrace) {
} }
stack = error.stack; stack = error.stack;
if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit; if (prevStackTraceLimit) Error.stackTraceLimit = prevStackTraceLimit;
if (extraFrames || typeof Error.stackTraceLimit !== 'number' || Error.stackTraceLimit > traceLimit) { if (
extraFrames ||
typeof Error.stackTraceLimit !== 'number' ||
Error.stackTraceLimit > traceLimit
) {
const frames = stack.split('\n'); const frames = stack.split('\n');
if (frames.length > traceLimit) { if (frames.length > traceLimit) {
stack = frames.slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0)).join('\n'); stack = frames
.slice(0, traceLimit + extraFrames + (frames[0] === 'Error' ? 1 : 0))
.join('\n');
} }
} }
return stack; return stack;
@ -106,7 +133,8 @@ function getStackTrace(config, toExcludeFromTrace) {
function amendActionType(action, config, toExcludeFromTrace) { function amendActionType(action, config, toExcludeFromTrace) {
let timestamp = Date.now(); let timestamp = Date.now();
let stack = getStackTrace(config, toExcludeFromTrace); let stack = getStackTrace(config, toExcludeFromTrace);
if (typeof action === 'string') return { action: { type: action }, timestamp, stack }; if (typeof action === 'string')
return { action: { type: action }, timestamp, stack };
if (!action.type) return { action: { type: 'update' }, timestamp, stack }; if (!action.type) return { action: { type: 'update' }, timestamp, stack };
if (action.action) return stack ? { stack, ...action } : action; if (action.action) return stack ? { stack, ...action } : action;
return { action, timestamp, stack }; return { action, timestamp, stack };
@ -117,7 +145,12 @@ export function toContentScript(message, serializeState, serializeAction) {
message.action = stringify(message.action, serializeAction); message.action = stringify(message.action, serializeAction);
message.payload = stringify(message.payload, serializeState); message.payload = stringify(message.payload, serializeState);
} else if (message.type === 'STATE' || message.type === 'PARTIAL_STATE') { } else if (message.type === 'STATE' || message.type === 'PARTIAL_STATE') {
const { actionsById, computedStates, committedState, ...rest } = message.payload; const {
actionsById,
computedStates,
committedState,
...rest
} = message.payload;
message.payload = rest; message.payload = rest;
message.actionsById = stringify(actionsById, serializeAction); message.actionsById = stringify(actionsById, serializeAction);
message.computedStates = stringify(computedStates, serializeState); message.computedStates = stringify(computedStates, serializeState);
@ -125,7 +158,10 @@ export function toContentScript(message, serializeState, serializeAction) {
} else if (message.type === 'EXPORT') { } else if (message.type === 'EXPORT') {
message.payload = stringify(message.payload, serializeAction); message.payload = stringify(message.payload, serializeAction);
if (typeof message.committedState !== 'undefined') { if (typeof message.committedState !== 'undefined') {
message.committedState = stringify(message.committedState, serializeState); message.committedState = stringify(
message.committedState,
serializeState
);
} }
} }
post(message); post(message);
@ -145,19 +181,23 @@ export function sendMessage(action, state, config, instanceId, name) {
maxAge: config.maxAge, maxAge: config.maxAge,
source, source,
name: config.name || name, name: config.name || name,
instanceId: config.instanceId || instanceId || 1 instanceId: config.instanceId || instanceId || 1,
}; };
toContentScript(message, config.serialize, config.serialize); toContentScript(message, config.serialize, config.serialize);
} }
function handleMessages(event) { function handleMessages(event) {
if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window)) return; if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window))
return;
const message = event.data; const message = event.data;
if (!message || message.source !== '@devtools-extension') return; if (!message || message.source !== '@devtools-extension') return;
Object.keys(listeners).forEach(id => { Object.keys(listeners).forEach((id) => {
if (message.id && id !== message.id) return; if (message.id && id !== message.id) return;
if (typeof listeners[id] === 'function') listeners[id](message); if (typeof listeners[id] === 'function') listeners[id](message);
else listeners[id].forEach(fn => { fn(message); }); else
listeners[id].forEach((fn) => {
fn(message);
});
}); });
} }
@ -166,13 +206,13 @@ export function setListener(onMessage, instanceId) {
window.addEventListener('message', handleMessages, false); window.addEventListener('message', handleMessages, false);
} }
const liftListener = (listener, config) => message => { const liftListener = (listener, config) => (message) => {
let data = {}; let data = {};
if (message.type === 'IMPORT') { if (message.type === 'IMPORT') {
data.type = 'DISPATCH'; data.type = 'DISPATCH';
data.payload = { data.payload = {
type: 'IMPORT_STATE', type: 'IMPORT_STATE',
...importState(message.state, config) ...importState(message.state, config),
}; };
} else { } else {
data = message; data = message;
@ -189,7 +229,9 @@ export function connect(preConfig) {
const config = preConfig || {}; const config = preConfig || {};
const id = generateId(config.instanceId); const id = generateId(config.instanceId);
if (!config.instanceId) config.instanceId = id; if (!config.instanceId) config.instanceId = id;
if (!config.name) config.name = document.title && id === 1 ? document.title : `Instance ${id}`; if (!config.name)
config.name =
document.title && id === 1 ? document.title : `Instance ${id}`;
if (config.serialize) config.serialize = getSeralizeParameter(config); if (config.serialize) config.serialize = getSeralizeParameter(config);
const actionCreators = config.actionCreators || {}; const actionCreators = config.actionCreators || {};
const latency = config.latency; const latency = config.latency;
@ -200,7 +242,7 @@ export function connect(preConfig) {
let delayedActions = []; let delayedActions = [];
let delayedStates = []; let delayedStates = [];
const rootListiner = action => { const rootListiner = (action) => {
if (autoPause) { if (autoPause) {
if (action.type === 'START') isPaused = false; if (action.type === 'START') isPaused = false;
else if (action.type === 'STOP') isPaused = true; else if (action.type === 'STOP') isPaused = true;
@ -213,7 +255,7 @@ export function connect(preConfig) {
type: 'LIFTED', type: 'LIFTED',
liftedState: { isPaused }, liftedState: { isPaused },
instanceId: id, instanceId: id,
source source,
}); });
} }
} }
@ -243,20 +285,29 @@ export function connect(preConfig) {
}, latency); }, latency);
const send = (action, state) => { const send = (action, state) => {
if (isPaused || isFiltered(action, localFilter) || predicate && !predicate(state, action)) { if (
isPaused ||
isFiltered(action, localFilter) ||
(predicate && !predicate(state, action))
) {
return; return;
} }
let amendedAction = action; let amendedAction = action;
const amendedState = config.stateSanitizer ? config.stateSanitizer(state) : state; const amendedState = config.stateSanitizer
? config.stateSanitizer(state)
: state;
if (action) { if (action) {
if (config.getActionType) { if (config.getActionType) {
amendedAction = config.getActionType(action); amendedAction = config.getActionType(action);
if (typeof amendedAction !== 'object') { if (typeof amendedAction !== 'object') {
amendedAction = { action: { type: amendedAction }, timestamp: Date.now() }; amendedAction = {
action: { type: amendedAction },
timestamp: Date.now(),
};
} }
} } else if (config.actionSanitizer)
else if (config.actionSanitizer) amendedAction = config.actionSanitizer(action); amendedAction = config.actionSanitizer(action);
amendedAction = amendActionType(amendedAction, config, send); amendedAction = amendActionType(amendedAction, config, send);
if (latency) { if (latency) {
delayedActions.push(amendedAction); delayedActions.push(amendedAction);
@ -273,9 +324,10 @@ export function connect(preConfig) {
type: 'INIT', type: 'INIT',
payload: stringify(state, config.serialize), payload: stringify(state, config.serialize),
instanceId: id, instanceId: id,
source source,
}; };
if (liftedData && Array.isArray(liftedData)) { // Legacy if (liftedData && Array.isArray(liftedData)) {
// Legacy
message.action = stringify(liftedData); message.action = stringify(liftedData);
message.name = config.name; message.name = config.name;
} else { } else {
@ -288,7 +340,7 @@ export function connect(preConfig) {
name: config.name || document.title, name: config.name || document.title,
features: config.features, features: config.features,
serialize: !!config.serialize, serialize: !!config.serialize,
type: config.type type: config.type,
}; };
} }
post(message); post(message);
@ -307,16 +359,18 @@ export function connect(preConfig) {
subscribe, subscribe,
unsubscribe, unsubscribe,
send, send,
error error,
}; };
} }
export function updateStore(stores) { export function updateStore(stores) {
return function(newStore, instanceId) { return function (newStore, instanceId) {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.warn('`__REDUX_DEVTOOLS_EXTENSION__.updateStore` is deprecated, remove it and just use ' + console.warn(
'`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead of the extension\'s store enhancer: ' + '`__REDUX_DEVTOOLS_EXTENSION__.updateStore` is deprecated, remove it and just use ' +
'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup'); "`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead of the extension's store enhancer: " +
'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup'
);
/* eslint-enable no-console */ /* eslint-enable no-console */
const store = stores[instanceId || Object.keys(stores)[0]]; const store = stores[instanceId || Object.keys(stores)[0]];
// Mutate the store in order to keep the reference // Mutate the store in order to keep the reference

View File

@ -3,7 +3,7 @@ let lastTime = 0;
function createExpBackoffTimer(step) { function createExpBackoffTimer(step) {
let count = 1; let count = 1;
return function(reset) { return function (reset) {
// Reset call // Reset call
if (reset) { if (reset) {
count = 1; count = 1;
@ -20,19 +20,24 @@ const nextErrorTimeout = createExpBackoffTimer(5000);
function postError(message) { function postError(message) {
if (handleError && !handleError()) return; if (handleError && !handleError()) return;
window.postMessage({ window.postMessage(
source: '@devtools-page', {
type: 'ERROR', source: '@devtools-page',
message: message type: 'ERROR',
}, '*'); message: message,
},
'*'
);
} }
function catchErrors(e) { function catchErrors(e) {
if ( if (
window.devToolsOptions && !window.devToolsOptions.shouldCatchErrors (window.devToolsOptions && !window.devToolsOptions.shouldCatchErrors) ||
|| e.timeStamp - lastTime < nextErrorTimeout() e.timeStamp - lastTime < nextErrorTimeout()
) return; )
lastTime = e.timeStamp; nextErrorTimeout(true); return;
lastTime = e.timeStamp;
nextErrorTimeout(true);
postError(e.message); postError(e.message);
} }

View File

@ -1,7 +1,10 @@
export default function openWindow(position) { export default function openWindow(position) {
window.postMessage({ window.postMessage(
source: '@devtools-page', {
type: 'OPEN', source: '@devtools-page',
position: position || 'right' type: 'OPEN',
}, '*'); position: position || 'right',
},
'*'
);
} }

View File

@ -41,13 +41,25 @@ class App extends Component {
render() { render() {
const { const {
monitor, position, togglePersist, monitor,
dispatcherIsOpen, sliderIsOpen, options, liftedState position,
togglePersist,
dispatcherIsOpen,
sliderIsOpen,
options,
liftedState,
} = this.props; } = this.props;
if (!position && (!options || !options.features)) { if (!position && (!options || !options.features)) {
return ( return (
<div style={{ padding: '20px', width: '100%', textAlign: 'center' }}> <div style={{ padding: '20px', width: '100%', textAlign: 'center' }}>
No store found. Make sure to follow <a href="https://github.com/zalmoxisus/redux-devtools-extension#usage" target="_blank">the instructions</a>. No store found. Make sure to follow{' '}
<a
href="https://github.com/zalmoxisus/redux-devtools-extension#usage"
target="_blank"
>
the instructions
</a>
.
</div> </div>
); );
} }
@ -55,7 +67,7 @@ class App extends Component {
return ( return (
<div style={styles.container}> <div style={styles.container}>
<div style={styles.buttonBar}> <div style={styles.buttonBar}>
<MonitorSelector selected={monitor}/> <MonitorSelector selected={monitor} />
<Instances selected={this.props.selected} /> <Instances selected={this.props.selected} />
</div> </div>
<DevTools <DevTools
@ -66,7 +78,7 @@ class App extends Component {
lib={options.lib || options.explicitLib} lib={options.lib || options.explicitLib}
/> />
<Notification /> <Notification />
{sliderIsOpen && options.connectionId && options.features.jump && {sliderIsOpen && options.connectionId && options.features.jump && (
<SliderMonitor <SliderMonitor
monitor="SliderMonitor" monitor="SliderMonitor"
liftedState={liftedState} liftedState={liftedState}
@ -77,68 +89,67 @@ class App extends Component {
style={{ padding: '15px 5px' }} style={{ padding: '15px 5px' }}
fillColor="rgb(120, 144, 156)" fillColor="rgb(120, 144, 156)"
/> />
} )}
{dispatcherIsOpen && options.connectionId && options.features.dispatch && {dispatcherIsOpen &&
<Dispatcher options={options} /> options.connectionId &&
} options.features.dispatch && <Dispatcher options={options} />}
<div style={styles.buttonBar}> <div style={styles.buttonBar}>
{!window.isElectron && position !== '#left' && {!window.isElectron && position !== '#left' && (
<Button
Icon={LeftIcon}
onClick={() => { this.openWindow('left'); }}
/>
}
{!window.isElectron && position !== '#right' &&
<Button
Icon={RightIcon}
onClick={() => { this.openWindow('right'); }}
/>
}
{!window.isElectron && position !== '#bottom' &&
<Button
Icon={BottomIcon}
onClick={() => { this.openWindow('bottom'); }}
/>
}
{features.pause &&
<RecordButton paused={liftedState.isPaused} />
}
{features.lock &&
<LockButton locked={liftedState.isLocked} />
}
{features.persist &&
<Button <Button
Icon={PersistIcon} Icon={LeftIcon}
onClick={togglePersist} onClick={() => {
>Persist</Button> this.openWindow('left');
} }}
{features.dispatch && />
<DispatcherButton dispatcherIsOpen={dispatcherIsOpen}/> )}
} {!window.isElectron && position !== '#right' && (
{features.jump && <Button
<SliderButton isOpen={sliderIsOpen}/> Icon={RightIcon}
} onClick={() => {
{features.import && this.openWindow('right');
<ImportButton /> }}
} />
{features.export && )}
<ExportButton /> {!window.isElectron && position !== '#bottom' && (
} <Button
{position && (position !== '#popup' || navigator.userAgent.indexOf('Firefox') !== -1) && Icon={BottomIcon}
<PrintButton /> onClick={() => {
} this.openWindow('bottom');
{!window.isElectron && }}
<Button />
Icon={RemoteIcon} )}
onClick={() => { this.openWindow('remote'); }} {features.pause && <RecordButton paused={liftedState.isPaused} />}
>Remote</Button> {features.lock && <LockButton locked={liftedState.isLocked} />}
} {features.persist && (
{(chrome.runtime.openOptionsPage || navigator.userAgent.indexOf('Firefox') !== -1) && <Button Icon={PersistIcon} onClick={togglePersist}>
<Button Persist
Icon={SettingsIcon} </Button>
onClick={this.openOptionsPage} )}
>Settings</Button> {features.dispatch && (
} <DispatcherButton dispatcherIsOpen={dispatcherIsOpen} />
)}
{features.jump && <SliderButton isOpen={sliderIsOpen} />}
{features.import && <ImportButton />}
{features.export && <ExportButton />}
{position &&
(position !== '#popup' ||
navigator.userAgent.indexOf('Firefox') !== -1) && <PrintButton />}
{!window.isElectron && (
<Button
Icon={RemoteIcon}
onClick={() => {
this.openWindow('remote');
}}
>
Remote
</Button>
)}
{(chrome.runtime.openOptionsPage ||
navigator.userAgent.indexOf('Firefox') !== -1) && (
<Button Icon={SettingsIcon} onClick={this.openOptionsPage}>
Settings
</Button>
)}
</div> </div>
</div> </div>
); );
@ -158,7 +169,7 @@ App.propTypes = {
position: PropTypes.string, position: PropTypes.string,
reports: PropTypes.array.isRequired, reports: PropTypes.array.isRequired,
dispatcherIsOpen: PropTypes.bool, dispatcherIsOpen: PropTypes.bool,
sliderIsOpen: PropTypes.bool sliderIsOpen: PropTypes.bool,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
@ -173,7 +184,7 @@ function mapStateToProps(state) {
dispatcherIsOpen: state.monitor.dispatcherIsOpen, dispatcherIsOpen: state.monitor.dispatcherIsOpen,
sliderIsOpen: state.monitor.sliderIsOpen, sliderIsOpen: state.monitor.sliderIsOpen,
reports: state.reports.data, reports: state.reports.data,
shouldSync: state.instances.sync shouldSync: state.instances.sync,
}; };
} }
@ -181,7 +192,9 @@ function mapDispatchToProps(dispatch) {
return { return {
liftedDispatch: bindActionCreators(liftedDispatch, dispatch), liftedDispatch: bindActionCreators(liftedDispatch, dispatch),
getReport: bindActionCreators(getReport, dispatch), getReport: bindActionCreators(getReport, dispatch),
togglePersist: () => { dispatch({ type: 'TOGGLE_PERSIST' }); } togglePersist: () => {
dispatch({ type: 'TOGGLE_PERSIST' });
},
}; };
} }

View File

@ -1,5 +1,9 @@
import stringifyJSON from 'remotedev-app/lib/utils/stringifyJSON'; import stringifyJSON from 'remotedev-app/lib/utils/stringifyJSON';
import { UPDATE_STATE, REMOVE_INSTANCE, LIFTED_ACTION } from 'remotedev-app/lib/constants/actionTypes'; import {
UPDATE_STATE,
REMOVE_INSTANCE,
LIFTED_ACTION,
} from 'remotedev-app/lib/constants/actionTypes';
import { nonReduxDispatch } from 'remotedev-app/lib/utils/monitorActions'; import { nonReduxDispatch } from 'remotedev-app/lib/utils/monitorActions';
import syncOptions from '../../browser/extension/options/syncOptions'; import syncOptions from '../../browser/extension/options/syncOptions';
import openDevToolsWindow from '../../browser/extension/background/openWindow'; import openDevToolsWindow from '../../browser/extension/background/openWindow';
@ -10,21 +14,22 @@ const DISCONNECTED = 'socket/DISCONNECTED';
const connections = { const connections = {
tab: {}, tab: {},
panel: {}, panel: {},
monitor: {} monitor: {},
}; };
const chunks = {}; const chunks = {};
let monitors = 0; let monitors = 0;
let isMonitored = false; let isMonitored = false;
const getId = (sender, name) => sender.tab ? sender.tab.id : name || sender.id; const getId = (sender, name) =>
sender.tab ? sender.tab.id : name || sender.id;
function toMonitors(action, tabId, verbose) { function toMonitors(action, tabId, verbose) {
Object.keys(connections.monitor).forEach(id => { Object.keys(connections.monitor).forEach((id) => {
connections.monitor[id].postMessage( connections.monitor[id].postMessage(
verbose || action.type === 'ERROR' ? action : { type: UPDATE_STATE } verbose || action.type === 'ERROR' ? action : { type: UPDATE_STATE }
); );
}); });
Object.keys(connections.panel).forEach(id => { Object.keys(connections.panel).forEach((id) => {
connections.panel[id].postMessage(action); connections.panel[id].postMessage(action);
}); });
} }
@ -34,13 +39,13 @@ function toContentScript({ message, action, id, instanceId, state }) {
type: message, type: message,
action, action,
state: nonReduxDispatch(window.store, message, instanceId, action, state), state: nonReduxDispatch(window.store, message, instanceId, action, state),
id: instanceId.toString().replace(/^[^\/]+\//, '') id: instanceId.toString().replace(/^[^\/]+\//, ''),
}); });
} }
function toAllTabs(msg) { function toAllTabs(msg) {
const tabs = connections.tab; const tabs = connections.tab;
Object.keys(tabs).forEach(id => { Object.keys(tabs).forEach((id) => {
tabs[id].postMessage(msg); tabs[id].postMessage(msg);
}); });
} }
@ -67,7 +72,7 @@ function getReducerError() {
function togglePersist() { function togglePersist() {
const state = window.store.getState(); const state = window.store.getState();
if (state.persistStates) { if (state.persistStates) {
Object.keys(state.instances.connections).forEach(id => { Object.keys(state.instances.connections).forEach((id) => {
if (connections.tab[id]) return; if (connections.tab[id]) return;
window.store.dispatch({ type: REMOVE_INSTANCE, id }); window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id }); toMonitors({ type: 'NA', id });
@ -92,7 +97,7 @@ function messaging(request, sender, sendResponse) {
return; return;
} }
if (request.type === 'GET_OPTIONS') { if (request.type === 'GET_OPTIONS') {
window.syncOptions.get(options => { window.syncOptions.get((options) => {
sendResponse({ options }); sendResponse({ options });
}); });
return; return;
@ -103,7 +108,11 @@ function messaging(request, sender, sendResponse) {
} }
if (request.type === 'OPEN') { if (request.type === 'OPEN') {
let position = 'devtools-left'; let position = 'devtools-left';
if (['remote', 'panel', 'left', 'right', 'bottom'].indexOf(request.position) !== -1) { if (
['remote', 'panel', 'left', 'right', 'bottom'].indexOf(
request.position
) !== -1
) {
position = 'devtools-' + request.position; position = 'devtools-' + request.position;
} }
openDevToolsWindow(position); openDevToolsWindow(position);
@ -118,10 +127,12 @@ function messaging(request, sender, sendResponse) {
const reducerError = getReducerError(); const reducerError = getReducerError();
chrome.notifications.create('app-error', { chrome.notifications.create('app-error', {
type: 'basic', type: 'basic',
title: reducerError ? 'An error occurred in the reducer' : 'An error occurred in the app', title: reducerError
? 'An error occurred in the reducer'
: 'An error occurred in the app',
message: reducerError || request.message, message: reducerError || request.message,
iconUrl: 'img/logo/48x48.png', iconUrl: 'img/logo/48x48.png',
isClickable: !!reducerError isClickable: !!reducerError,
}); });
return; return;
} }
@ -134,7 +145,8 @@ function messaging(request, sender, sendResponse) {
return; return;
} }
if (request.split === 'chunk') { if (request.split === 'chunk') {
chunks[instanceId][request.chunk[0]] = (chunks[instanceId][request.chunk[0]] || '') + request.chunk[1]; chunks[instanceId][request.chunk[0]] =
(chunks[instanceId][request.chunk[0]] || '') + request.chunk[1];
return; return;
} }
action.request = chunks[instanceId]; action.request = chunks[instanceId];
@ -180,11 +192,11 @@ function onConnect(port) {
id = getId(port.sender); id = getId(port.sender);
if (port.sender.frameId) id = `${id}-${port.sender.frameId}`; if (port.sender.frameId) id = `${id}-${port.sender.frameId}`;
connections.tab[id] = port; connections.tab[id] = port;
listener = msg => { listener = (msg) => {
if (msg.name === 'INIT_INSTANCE') { if (msg.name === 'INIT_INSTANCE') {
if (typeof id === 'number') { if (typeof id === 'number') {
chrome.pageAction.show(id); chrome.pageAction.show(id);
chrome.pageAction.setIcon({tabId: id, path: 'img/logo/38x38.png'}); chrome.pageAction.setIcon({ tabId: id, path: 'img/logo/38x38.png' });
} }
if (isMonitored) port.postMessage({ type: 'START' }); if (isMonitored) port.postMessage({ type: 'START' });
@ -195,8 +207,12 @@ function onConnect(port) {
if (!persistedState) return; if (!persistedState) return;
toContentScript({ toContentScript({
message: 'IMPORT', message: 'IMPORT',
id, instanceId, id,
state: stringifyJSON(persistedState, state.instances.options[instanceId].serialize) instanceId,
state: stringifyJSON(
persistedState,
state.instances.options[instanceId].serialize
),
}); });
} }
return; return;
@ -213,12 +229,13 @@ function onConnect(port) {
monitorInstances(true); monitorInstances(true);
monitors++; monitors++;
port.onDisconnect.addListener(disconnect('monitor', id)); port.onDisconnect.addListener(disconnect('monitor', id));
} else { // devpanel } else {
// devpanel
id = port.name || port.sender.frameId; id = port.name || port.sender.frameId;
connections.panel[id] = port; connections.panel[id] = port;
monitorInstances(true, port.name); monitorInstances(true, port.name);
monitors++; monitors++;
listener = msg => { listener = (msg) => {
window.store.dispatch(msg); window.store.dispatch(msg);
}; };
port.onMessage.addListener(listener); port.onMessage.addListener(listener);
@ -231,7 +248,7 @@ chrome.runtime.onConnectExternal.addListener(onConnect);
chrome.runtime.onMessage.addListener(messaging); chrome.runtime.onMessage.addListener(messaging);
chrome.runtime.onMessageExternal.addListener(messaging); chrome.runtime.onMessageExternal.addListener(messaging);
chrome.notifications.onClicked.addListener(id => { chrome.notifications.onClicked.addListener((id) => {
chrome.notifications.clear(id); chrome.notifications.clear(id);
openDevToolsWindow('devtools-right'); openDevToolsWindow('devtools-right');
}); });
@ -239,7 +256,7 @@ chrome.notifications.onClicked.addListener(id => {
window.syncOptions = syncOptions(toAllTabs); // Expose to the options page window.syncOptions = syncOptions(toAllTabs); // Expose to the options page
export default function api() { export default function api() {
return next => action => { return (next) => (action) => {
if (action.type === LIFTED_ACTION) toContentScript(action); if (action.type === LIFTED_ACTION) toContentScript(action);
else if (action.type === 'TOGGLE_PERSIST') togglePersist(); else if (action.type === 'TOGGLE_PERSIST') togglePersist();
return next(action); return next(action);

View File

@ -1,4 +1,7 @@
import { SELECT_INSTANCE, UPDATE_STATE } from 'remotedev-app/lib/constants/actionTypes'; import {
SELECT_INSTANCE,
UPDATE_STATE,
} from 'remotedev-app/lib/constants/actionTypes';
function selectInstance(tabId, store, next) { function selectInstance(tabId, store, next) {
const instances = store.getState().instances; const instances = store.getState().instances;
@ -10,24 +13,27 @@ function selectInstance(tabId, store, next) {
} }
function getCurrentTabId(next) { function getCurrentTabId(next) {
chrome.tabs.query({ chrome.tabs.query(
active: true, {
lastFocusedWindow: true active: true,
}, tabs => { lastFocusedWindow: true,
const tab = tabs[0]; },
if (!tab) return; (tabs) => {
next(tab.id); const tab = tabs[0];
}); if (!tab) return;
next(tab.id);
}
);
} }
export default function popupSelector(store) { export default function popupSelector(store) {
return next => action => { return (next) => (action) => {
const result = next(action); const result = next(action);
if (action.type === UPDATE_STATE) { if (action.type === UPDATE_STATE) {
if (chrome.devtools && chrome.devtools.inspectedWindow) { if (chrome.devtools && chrome.devtools.inspectedWindow) {
selectInstance(chrome.devtools.inspectedWindow.tabId, store, next); selectInstance(chrome.devtools.inspectedWindow.tabId, store, next);
} else { } else {
getCurrentTabId(tabId => selectInstance(tabId, store, next)); getCurrentTabId((tabId) => selectInstance(tabId, store, next));
} }
} }
return result; return result;

View File

@ -1,16 +1,19 @@
import { LIFTED_ACTION, UPDATE_STATE, SELECT_INSTANCE } from 'remotedev-app/lib/constants/actionTypes'; import {
LIFTED_ACTION,
UPDATE_STATE,
SELECT_INSTANCE,
} from 'remotedev-app/lib/constants/actionTypes';
import { getActiveInstance } from 'remotedev-app/lib/reducers/instances'; import { getActiveInstance } from 'remotedev-app/lib/reducers/instances';
function panelDispatcher(bgConnection) { function panelDispatcher(bgConnection) {
let autoselected = false; let autoselected = false;
const tabId = chrome.devtools.inspectedWindow.tabId; const tabId = chrome.devtools.inspectedWindow.tabId;
return store => next => action => { return (store) => (next) => (action) => {
const result = next(action); const result = next(action);
if (!autoselected && action.type === UPDATE_STATE && tabId) { if (!autoselected && action.type === UPDATE_STATE && tabId) {
autoselected = true; autoselected = true;
const connections = store.getState() const connections = store.getState().instances.connections[tabId];
.instances.connections[tabId];
if (connections && connections.length === 1) { if (connections && connections.length === 1) {
next({ type: SELECT_INSTANCE, selected: connections[0] }); next({ type: SELECT_INSTANCE, selected: connections[0] });
} }

View File

@ -1,11 +1,14 @@
import { UPDATE_STATE, LIFTED_ACTION } from 'remotedev-app/lib/constants/actionTypes'; import {
UPDATE_STATE,
LIFTED_ACTION,
} from 'remotedev-app/lib/constants/actionTypes';
import { getActiveInstance } from 'remotedev-app/lib/reducers/instances'; import { getActiveInstance } from 'remotedev-app/lib/reducers/instances';
const syncStores = baseStore => store => next => action => { const syncStores = (baseStore) => (store) => (next) => (action) => {
if (action.type === UPDATE_STATE) { if (action.type === UPDATE_STATE) {
return next({ return next({
...action, ...action,
instances: baseStore.getState().instances instances: baseStore.getState().instances,
}); });
} }
if (action.type === LIFTED_ACTION || action.type === 'TOGGLE_PERSIST') { if (action.type === LIFTED_ACTION || action.type === 'TOGGLE_PERSIST') {

View File

@ -4,7 +4,7 @@ import persistStates from './persistStates';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
instances, instances,
persistStates persistStates,
}); });
export default rootReducer; export default rootReducer;

View File

@ -10,7 +10,7 @@ const rootReducer = combineReducers({
monitor, monitor,
test, test,
reports, reports,
notification notification,
}); });
export default rootReducer; export default rootReducer;

View File

@ -12,7 +12,7 @@ const rootReducer = combineReducers({
test, test,
socket, socket,
reports, reports,
notification notification,
}); });
export default rootReducer; export default rootReducer;

View File

@ -1,5 +1,12 @@
import { initialState, dispatchAction } from 'remotedev-app/lib/reducers/instances'; import {
import { UPDATE_STATE, SELECT_INSTANCE, LIFTED_ACTION } from 'remotedev-app/lib/constants/actionTypes'; initialState,
dispatchAction,
} from 'remotedev-app/lib/reducers/instances';
import {
UPDATE_STATE,
SELECT_INSTANCE,
LIFTED_ACTION,
} from 'remotedev-app/lib/constants/actionTypes';
export default function instances(state = initialState, action) { export default function instances(state = initialState, action) {
switch (action.type) { switch (action.type) {

View File

@ -23,13 +23,16 @@ export default class Monitor {
this.active = false; this.active = false;
clearTimeout(this.waitingTimeout); clearTimeout(this.waitingTimeout);
}; };
isHotReloaded = () => this.lastAction && /^@@redux\/(INIT|REPLACE)/.test(this.lastAction); isHotReloaded = () =>
isMonitorAction = () => this.lastAction && this.lastAction !== 'PERFORM_ACTION'; this.lastAction && /^@@redux\/(INIT|REPLACE)/.test(this.lastAction);
isMonitorAction = () =>
this.lastAction && this.lastAction !== 'PERFORM_ACTION';
isTimeTraveling = () => this.lastAction === 'JUMP_TO_STATE'; isTimeTraveling = () => this.lastAction === 'JUMP_TO_STATE';
isPaused = () => { isPaused = () => {
if (this.paused) { if (this.paused) {
if (this.lastAction !== 'BLOCKED') { if (this.lastAction !== 'BLOCKED') {
if (!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__) this.lastAction = 'BLOCKED'; if (!window.__REDUX_DEVTOOLS_EXTENSION_LOCKED__)
this.lastAction = 'BLOCKED';
return false; return false;
} }
return true; return true;

View File

@ -4,7 +4,7 @@ import api from '../middlewares/api';
export default function configureStore(preloadedState) { export default function configureStore(preloadedState) {
return createStore(rootReducer, preloadedState, applyMiddleware(api)); return createStore(rootReducer, preloadedState, applyMiddleware(api));
/* /*
let enhancer; let enhancer;
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
enhancer = applyMiddleware(api); enhancer = applyMiddleware(api);

View File

@ -3,25 +3,24 @@ import instrument from 'redux-devtools-instrument';
import persistState from 'redux-devtools/lib/persistState'; import persistState from 'redux-devtools/lib/persistState';
export function getUrlParam(key) { export function getUrlParam(key) {
const matches = window.location.href.match(new RegExp(`[?&]${key}=([^&#]+)\\b`)); const matches = window.location.href.match(
return (matches && matches.length > 0) ? matches[1] : null; new RegExp(`[?&]${key}=([^&#]+)\\b`)
);
return matches && matches.length > 0 ? matches[1] : null;
} }
export default function configureStore(next, monitorReducer, config) { export default function configureStore(next, monitorReducer, config) {
return compose( return compose(
instrument( instrument(monitorReducer, {
monitorReducer, maxAge: config.maxAge,
{ trace: config.trace,
maxAge: config.maxAge, traceLimit: config.traceLimit,
trace: config.trace, shouldCatchErrors: config.shouldCatchErrors || window.shouldCatchErrors,
traceLimit: config.traceLimit, shouldHotReload: config.shouldHotReload,
shouldCatchErrors: config.shouldCatchErrors || window.shouldCatchErrors, shouldRecordChanges: config.shouldRecordChanges,
shouldHotReload: config.shouldHotReload, shouldStartLocked: config.shouldStartLocked,
shouldRecordChanges: config.shouldRecordChanges, pauseActionType: config.pauseActionType || '@@PAUSED',
shouldStartLocked: config.shouldStartLocked, }),
pauseActionType: config.pauseActionType || '@@PAUSED'
}
),
persistState( persistState(
getUrlParam('debug_session'), getUrlParam('debug_session'),
config.deserializeState, config.deserializeState,

Some files were not shown because too many files have changed in this diff Show More