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" ],
"plugins": [ "add-module-exports", "transform-decorators-legacy" ]
"presets": ["es2015", "stage-0", "react"],
"plugins": ["add-module-exports", "transform-decorators-legacy"]
}

View File

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

View File

@ -2,7 +2,7 @@ sudo: required
dist: trusty
language: node_js
node_js:
- "6"
- '6'
cache:
directories:
- $HOME/.yarn-cache
@ -19,7 +19,7 @@ addons:
- g++-4.8
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
- 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
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities

View File

@ -10,29 +10,35 @@
## Installation
### 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
- 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`.
- 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
- 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
- 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
- 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
- use [`remote-redux-devtools`](https://github.com/zalmoxisus/remote-redux-devtools).
- use [`remote-redux-devtools`](https://github.com/zalmoxisus/remote-redux-devtools).
## Usage
> Note that starting from v2.7, `window.devToolsExtension` was renamed to `window.__REDUX_DEVTOOLS_EXTENSION__` / `window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`.
## 1. With Redux
### 1.1 Basic store
For a basic [Redux store](https://redux.js.org/api/createstore#createstorereducer-preloadedstate-enhancer) simply add:
```diff
const store = createStore(
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).
> For universal ("isomorphic") apps, prefix it with `typeof window !== 'undefined' &&`.
```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).
```js
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:
```diff
+ /* eslint-disable no-underscore-dangle */
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).
### 1.2 Advanced store setup
If you setup your store with [middleware and enhancers](http://redux.js.org/docs/api/applyMiddleware.html), change:
```diff
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)
));
```
> 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:
```js
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extensions options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extensions options like name, actionsBlacklist, actionsCreators, serialize...
})
: compose;
const enhancer = composeEnhancers(
applyMiddleware(...middleware),
applyMiddleware(...middleware)
// other store enhancers if any
);
const store = createStore(reducer, enhancer);
@ -100,20 +116,28 @@ const store = createStore(reducer, enhancer);
### 1.3 Use `redux-devtools-extension` package from npm
To make things easier, there's an npm package to install:
```
npm install --save redux-devtools-extension
```
and to use like so:
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(reducer, composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
));
const store = createStore(
reducer,
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):
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
@ -121,87 +145,109 @@ import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools({
// Specify name here, actionsBlacklist, actionsCreators and other options if needed
});
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
));
const store = createStore(
reducer,
/* 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.
In case you don't include other enhancers and middlewares, just use `devToolsEnhancer`:
```js
import { createStore } from 'redux';
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
));
);
```
### 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).
If you want to restrict it there, use `redux-devtools-extension/logOnlyInProduction`:
```js
import { createStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension/logOnlyInProduction';
const store = createStore(reducer, /* preloadedState, */ devToolsEnhancer(
const store = createStore(
reducer,
/* preloadedState, */ devToolsEnhancer()
// options like actionSanitizer, stateSanitizer
));
);
```
or with middlewares and enhancers:
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
const composeEnhancers = composeWithDevTools({
// options like actionSanitizer, stateSanitizer
});
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)
```js
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
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.
### 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 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
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
- [Options (arguments)](docs/API/Arguments.md)
- [Methods (advanced API)](docs/API/Methods.md)
- [FAQ](docs/FAQ.md)
- Features
- [Trace actions calls](/docs/Features/Trace.md)
- [Troubleshooting](docs/Troubleshooting.md)
- [Articles](docs/Articles.md)
- [Videos](docs/Videos.md)
- [Feedback](docs/Feedback.md)
- [Options (arguments)](docs/API/Arguments.md)
- [Methods (advanced API)](docs/API/Methods.md)
- [FAQ](docs/FAQ.md)
- Features
- [Trace actions calls](/docs/Features/Trace.md)
- [Troubleshooting](docs/Troubleshooting.md)
- [Articles](docs/Articles.md)
- [Videos](docs/Videos.md)
- [Feedback](docs/Feedback.md)
## Demo
Live demos to use the extension with:
- [Counter](http://zalmoxisus.github.io/examples/counter/)
- [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/)
- [Redux Form](http://redux-form.com/6.5.0/examples/simple/)
- [React Tetris](https://chvin.github.io/react-tetris/?lan=en)
- [Book Collection (Angular ngrx store)](https://ngrx.github.io/platform/example-app/)
- [Counter](http://zalmoxisus.github.io/examples/counter/)
- [TodoMVC](http://zalmoxisus.github.io/examples/todomvc/)
- [Redux Form](http://redux-form.com/6.5.0/examples/simple/)
- [React Tetris](https://chvin.github.io/react-tetris/?lan=en)
- [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).
## Backers
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>
@ -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/29/website" target="_blank"><img src="https://opencollective.com/redux-devtools-extension/backer/29/avatar.svg"></a>
## 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)]
<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'
cache:
- "%LOCALAPPDATA%/Yarn"
- '%LOCALAPPDATA%/Yarn'
- node_modules
install:

View File

@ -1,98 +1,141 @@
# Options
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);
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware),
- `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);
const store = createStore(
reducer,
/* preloadedState, */ composeEnhancers(
applyMiddleware(...middleware)
// other store enhancers if any
));
```
)
);
```
The `options` object is optional, and can include any of the following.
### `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`
*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`
*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`
*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`
*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`
*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`
*boolean* or *object* which contains:
_boolean_ or _object_ which contains:
- **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
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
options: {
undefined: true,
function: function(fn) { return fn.toString() }
}
}
}));
```
- `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
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.
Example of usage with [mori data structures](https://github.com/swannodette/mori):
```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
replacer: (key, value) => value && mori.isMap(value) ? mori.toJs(value) : value
}
}));
const store = Redux.createStore(
reducer,
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):
```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
replacer: (key, value) => {
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
}
}
}
}
}));
const store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
replacer: (key, value) => {
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:
```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
reviver: (key, value) => {
if (typeof value === 'object' && value !== null && '__serializedType__' in value) {
switch (value.__serializedType__) {
case 'ImmutableList': return Immutable.List(value.data);
}
}
}
}
}));
const store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
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:
@ -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/
// ...
// 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' ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
serialize: {
immutable: Immutable
}
}) : compose;
const composeEnhancers =
typeof window === 'object' &&
typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ !== 'undefined'
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
serialize: {
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`.
- **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';
// ...
const ABRecord = Immutable.Record({ a:1, b:2 });
const myRecord = new ABRecord({ b:3 }); // used in the reducers
const ABRecord = Immutable.Record({ a: 1, b: 2 });
const myRecord = new ABRecord({ b: 3 }); // used in the reducers
const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: {
immutable: Immutable,
refs: [ABRecord]
}
}));
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
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:
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
function component(
state = { component: null, toJSON: () => ({ component: '[React]' }) },
action
) {
switch (action.type) {
case 'ADD_COMPONENT': return { component: action.component };
default: return state;
case 'ADD_COMPONENT':
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`):
```js
function counter(
state = { count: 0, toJSON: function (){ return { conter: this.count * 10 }; } },
state = {
count: 0,
toJSON: function () {
return { conter: this.count * 10 };
},
},
action
) {
switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 };
default: return state;
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
}
```
### `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:
```js
const actionSanitizer = (action) => (
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data ?
{ ...action, data: '<<LONG_BLOB>>' } : action
const actionSanitizer = (action) =>
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
? { ...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`
*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:
```js
createStore(reducer, remotedev({
sendTo: 'http://localhost:8000',
actionsBlacklist: 'SOME_ACTION'
// or actionsBlacklist: ['SOME_ACTION', 'SOME_OTHER_ACTION']
// or just actionsBlacklist: 'SOME_' to omit both
}))
createStore(
reducer,
remotedev({
sendTo: 'http://localhost:8000',
actionsBlacklist: 'SOME_ACTION',
// or actionsBlacklist: ['SOME_ACTION', 'SOME_OTHER_ACTION']
// or just actionsBlacklist: 'SOME_' to omit both
})
);
```
### `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:
```js
const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
predicate: (state, action) => state.dev.logLevel === VERBOSE && !action.forwarded
}));
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
predicate: (state, action) =>
state.dev.logLevel === VERBOSE && !action.forwarded,
})
);
```
### `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`
*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`
*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`
*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`
*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`
*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`
If you want to restrict the extension, just specify the features you allow:
```js
const composeEnhancers = composeWithDevTools({
features: {
@ -225,10 +314,11 @@ const composeEnhancers = composeWithDevTools({
skip: true, // skip (cancel) actions
reorder: true, // drag and drop actions in the history list
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
});
```
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.

View File

@ -12,14 +12,16 @@ Use the following methods of `window.__REDUX_DEVTOOLS_EXTENSION__`:
- [notifyErrors](#notifyerrors)
<a id="connect"></a>
### connect([options])
##### Arguments
- [`options`] *Object* - [see the available options](Arguments.md).
- [`options`] _Object_ - [see the available options](Arguments.md).
##### 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.
- `unsubscribe()` - unsubscribes all listeners.
@ -37,7 +39,7 @@ devTools.subscribe((message) => {
}
});
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.
@ -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.
<a id="send"></a>
### 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.
##### Arguments
- `action` *String* (action type) or *Object* with required `type` key.
- `state` *any* - usually object to expand.
- [`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.
- `action` _String_ (action type) or _Object_ with required `type` key.
- `state` _any_ - usually object to expand.
- [`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.
<a id="listen"></a>
### listen(onMessage, instanceId)
Listen for messages dispatched for specific `instanceId`. For most cases it's better to use `subcribe` inside the [`connect`](connect).
##### Arguments
- `onMessage` *Function* to call when there's an action from the monitor.
- `instanceId` *String* - instance id for which to handle actions.
- `onMessage` _Function_ to call when there's an action from the monitor.
- `instanceId` _String_ - instance id for which to handle actions.
<a id="open"></a>
### open([position])
Open the extension's window. This should be conditional (usually you don't need to open extension's window automatically).
##### 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>
### 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.
##### 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
- 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:
- [Log Monitor](https://github.com/gaearon/redux-devtools-log-monitor)
- [Inspector](https://github.com/alexkuz/redux-devtools-inspector)
- [Dispatch](https://github.com/YoruNoHikage/redux-devtools-dispatch)
- [Slider](https://github.com/calesce/redux-slider-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) .
- Examples from [Redux](https://github.com/rackt/redux/tree/master/examples).
- 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:
- [Log Monitor](https://github.com/gaearon/redux-devtools-log-monitor)
- [Inspector](https://github.com/alexkuz/redux-devtools-inspector)
- [Dispatch](https://github.com/YoruNoHikage/redux-devtools-dispatch)
- [Slider](https://github.com/calesce/redux-slider-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) .
- Examples from [Redux](https://github.com/rackt/redux/tree/master/examples).

View File

@ -1,6 +1,7 @@
# Redux DevTools Extension FAQ
## Table of Contents
- [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 persist debug sessions across page reloads](#how-to-persist-debug-sessions-across-page-reloads)
@ -10,22 +11,35 @@
- [Keyboard shortcuts](#keyboard-shortcuts)
#### 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.
- 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).
#### 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).
#### How to persist debug sessions across page reloads
Just click the `Persist` button or add `?debug_session=<session_name>` to the url.
#### How to open DevTools programmatically
```js
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.
#### 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).
#### 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.
#### 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).

View File

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

View File

@ -1,21 +1,21 @@
# Documentation
* [Extension](/README.md)
* [Installation](/README.md#installation)
* [Usage](/README.md#usage)
* [Demo](/README.md#demo)
* [API Reference](/docs/API/README.md)
* [Options (arguments)](/docs/API/Arguments.md)
* [Methods (advanced API)](/docs/API/Methods.md)
* Features
* [Trace actions calls](/docs/Features/Trace.md)
* [Integrations](/docs/Integrations.md)
* [FAQ](/docs/FAQ.md)
* [Troubleshooting](/docs/Troubleshooting.md)
* [Recipes](/docs/Recipes.md)
* [Articles](/docs/Articles.md)
* [Videos](/docs/Videos.md)
* [Credits](/docs/Credits.md)
* [Support us](/README.md#backers)
* [Feedback](/docs/Feedback.md)
* [Change Log](https://github.com/zalmoxisus/redux-devtools-extension/releases)
- [Extension](/README.md)
- [Installation](/README.md#installation)
- [Usage](/README.md#usage)
- [Demo](/README.md#demo)
- [API Reference](/docs/API/README.md)
- [Options (arguments)](/docs/API/Arguments.md)
- [Methods (advanced API)](/docs/API/Methods.md)
- Features
- [Trace actions calls](/docs/Features/Trace.md)
- [Integrations](/docs/Integrations.md)
- [FAQ](/docs/FAQ.md)
- [Troubleshooting](/docs/Troubleshooting.md)
- [Recipes](/docs/Recipes.md)
- [Articles](/docs/Articles.md)
- [Videos](/docs/Videos.md)
- [Credits](/docs/Credits.md)
- [Support us](/README.md#backers)
- [Feedback](/docs/Feedback.md)
- [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__()
);
```
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
casting to any.
```typescript
import { createStore, StoreEnhancer } from "redux";
import { createStore, StoreEnhancer } from 'redux';
// ...
type WindowWithDevTools = Window & {
__REDUX_DEVTOOLS_EXTENSION__: () => StoreEnhancer<unknown, {}>
}
__REDUX_DEVTOOLS_EXTENSION__: () => StoreEnhancer<unknown, {}>;
};
const isReduxDevtoolsExtenstionExist =
(arg: Window | WindowWithDevTools):
arg is WindowWithDevTools => {
return '__REDUX_DEVTOOLS_EXTENSION__' in arg;
}
const isReduxDevtoolsExtenstionExist = (
arg: Window | WindowWithDevTools
): arg is WindowWithDevTools => {
return '__REDUX_DEVTOOLS_EXTENSION__' in arg;
};
// ...
const store = createStore(rootReducer, initialState,
isReduxDevtoolsExtenstionExist(window) ?
window.__REDUX_DEVTOOLS_EXTENSION__() : undefined)
const store = createStore(
rootReducer,
initialState,
isReduxDevtoolsExtenstionExist(window)
? window.__REDUX_DEVTOOLS_EXTENSION__()
: undefined
);
```
### Export from browser console or from application
```js
store.liftedStore.getState()
store.liftedStore.getState();
```
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 { devToolsEnhancer } from 'redux-devtools-extension/logOnly';
const store = createStore(reducer, /* preloadedState, */ compose(
devToolsEnhancer({
instaceID: 1,
name: 'Blacklisted',
actionsBlacklist: '...'
}),
devToolsEnhancer({
instaceID: 2,
name: 'Whitelisted',
actionsWhitelist: '...'
})
));
const store = createStore(
reducer,
/* preloadedState, */ compose(
devToolsEnhancer({
instaceID: 1,
name: 'Blacklisted',
actionsBlacklist: '...',
}),
devToolsEnhancer({
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:
```js
const store = createStore(reducer, preloadedState, compose(
// applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : noop => noop,
batchedSubscribe(/* ... */)
));
const store = createStore(
reducer,
preloadedState,
compose(
// applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__
? window.__REDUX_DEVTOOLS_EXTENSION__()
: (noop) => noop,
batchedSubscribe(/* ... */)
)
);
```
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`):
```js
const actionSanitizer = (action) => (
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data ?
{ ...action, data: '<<LONG_BLOB>>' } : action
const actionSanitizer = (action) =>
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
? { ...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).
@ -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.
1. The best solution is **not to pass the whole event object to reducers, but the data you need**:
```diff
function click(event) {
return {
type: ELEMENT_CLICKED,
- event: event
+ value: event.target.value
};
}
```
```diff
function click(event) {
return {
type: ELEMENT_CLICKED,
- 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.
@ -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`.
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
To get data which cannot be serialized by `JSON.stringify`, set [`serialize` parameter](/docs/API/Arguments.md#serialize):
```js
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: true
}));
const store = Redux.createStore(
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.

View File

@ -13,7 +13,7 @@ var exampleDirs = fs.readdirSync(__dirname).filter((file) => {
// Ordering is important here. `npm install` must come first.
var cmdArgs = [
{ cmd: 'npm', args: ['install'] },
{ cmd: 'webpack', args: ['index.js'] }
{ cmd: 'webpack', args: ['index.js'] },
];
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
const opts = {
cwd: path.join(__dirname, dir),
stdio: 'inherit'
stdio: 'inherit',
};
let result = {};
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() {
return {
type: INCREMENT_COUNTER
type: INCREMENT_COUNTER,
};
}
export function decrement() {
return {
type: DECREMENT_COUNTER
type: DECREMENT_COUNTER,
};
}
export function autoIncrement(delay = 10) {
return dispatch => {
return (dispatch) => {
if (t) {
clearInterval(t);
t = undefined;
@ -29,7 +29,7 @@ export function autoIncrement(delay = 10) {
}
export function incrementAsync(delay = 1000) {
return dispatch => {
return (dispatch) => {
setTimeout(() => {
dispatch(increment());
}, delay);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4001;
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.get("/", function(req, res) {
app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html');
});
app.listen(port, function(error) {
app.listen(port, function (error) {
if (error) {
console.error(error);
} 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';
export default function configureStore(preloadedState) {
const composeEnhancers = composeWithDevTools({ actionCreators, trace: true, traceLimit: 25 });
const store = createStore(reducer, preloadedState, composeEnhancers(
applyMiddleware(invariant(), thunk)
));
const composeEnhancers = composeWithDevTools({
actionCreators,
trace: true,
traceLimit: 25,
});
const store = createStore(
reducer,
preloadedState,
composeEnhancers(applyMiddleware(invariant(), thunk))
);
if (module.hot) {
// Enable Webpack hot module replacement for 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)) {
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.');
}
function mockStoreWithoutMiddleware() {
return {
getState() {
return typeof getState === 'function' ?
getState() :
getState;
return typeof getState === 'function' ? getState() : getState;
},
dispatch(action) {
@ -31,13 +32,13 @@ function mockStore(getState, expectedActions, onLastAction) {
onLastAction();
}
return action;
}
},
};
}
const mockStoreWithMiddleware = applyMiddleware(
...middlewares
)(mockStoreWithoutMiddleware);
const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
mockStoreWithoutMiddleware
);
return mockStoreWithMiddleware();
}
@ -52,9 +53,7 @@ describe('actions', () => {
});
it('incrementIfOdd should create increment action', (done) => {
const expectedActions = [
{ type: actions.INCREMENT_COUNTER }
];
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
const store = mockStore({ counter: 1 }, expectedActions, done);
store.dispatch(actions.incrementIfOdd());
});
@ -67,9 +66,7 @@ describe('actions', () => {
});
it('incrementAsync should create increment action', (done) => {
const expectedActions = [
{ type: actions.INCREMENT_COUNTER }
];
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
const store = mockStore({ counter: 0 }, expectedActions, done);
store.dispatch(actions.incrementAsync(100));
});

View File

@ -8,14 +8,16 @@ function setup() {
increment: expect.createSpy(),
incrementIfOdd: 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 {
component: component,
actions: actions,
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 {
app: app,
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 = {
mode: 'development',
devtool: 'source-map',
entry: [
'webpack-hot-middleware/client',
'./index'
],
entry: ['webpack-hot-middleware/client', './index'],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
publicPath: '/static/',
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
plugins: [new webpack.HotModuleReplacementPlugin()],
module: {
rules: [{
test: /\.js$/,
loaders: ['babel-loader'],
exclude: /node_modules/
}]
}
rules: [
{
test: /\.js$/,
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';
const withDevTools = (
const withDevTools =
// process.env.NODE_ENV === 'development' &&
typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__
);
typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__;
class Counter extends Component {
constructor() {
@ -20,7 +19,10 @@ class Counter extends Component {
this.unsubscribe = this.devTools.subscribe((message) => {
// Implement monitors actions.
// 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);
}
});
@ -50,10 +52,7 @@ class Counter extends Component {
const { counter } = this.state;
return (
<p>
Clicked: {counter} times
{' '}
<button onClick={this.increment}>+</button>
{' '}
Clicked: {counter} times <button onClick={this.increment}>+</button>{' '}
<button onClick={this.decrement}>-</button>
</p>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,14 +8,14 @@ import * as TodoActions from '../actions/todos';
function mapDispatchToProps(dispatch) {
return {
pushState: bindActionCreators(pushState, dispatch),
actions: bindActionCreators(TodoActions, dispatch)
actions: bindActionCreators(TodoActions, dispatch),
};
}
@connect((state) => ({}), mapDispatchToProps)
class Wrapper extends Component {
static propTypes = {
children: PropTypes.node
children: PropTypes.node,
};
constructor(props) {
@ -41,11 +41,23 @@ class Wrapper extends Component {
}
render() {
return (
<div>
<div 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
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>
{this.props.children}
</div>

View File

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

View File

@ -4,7 +4,7 @@ import todos from './todos';
const rootReducer = combineReducers({
todos,
router: routerStateReducer
router: routerStateReducer,
});
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 = [{
text: 'Use Redux',
completed: false,
id: 0
}];
const initialState = [
{
text: 'Use Redux',
completed: false,
id: 0,
},
];
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return [{
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.text
}, ...state];
case ADD_TODO:
return [
{
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.text,
},
...state,
];
case DELETE_TODO:
return state.filter(todo =>
todo.id !== action.id
);
case DELETE_TODO:
return state.filter((todo) => todo.id !== action.id);
case EDIT_TODO:
return state.map(todo =>
todo.id === action.id ?
Object.assign({}, todo, { text: action.text }) :
todo
);
case EDIT_TODO:
return state.map((todo) =>
todo.id === action.id
? Object.assign({}, todo, { text: action.text })
: todo
);
case COMPLETE_TODO:
return state.map(todo =>
todo.id === action.id ?
Object.assign({}, todo, { completed: !todo.completed }) :
todo
);
case COMPLETE_TODO:
return state.map((todo) =>
todo.id === action.id
? Object.assign({}, todo, { completed: !todo.completed })
: todo
);
case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed);
return state.map(todo => Object.assign({}, todo, {
completed: !areAllMarked
}));
case COMPLETE_ALL:
const areAllMarked = state.every((todo) => todo.completed);
return state.map((todo) =>
Object.assign({}, todo, {
completed: !areAllMarked,
})
);
case CLEAR_COMPLETED:
return state.filter(todo => todo.completed === false);
case CLEAR_COMPLETED:
return state.filter((todo) => todo.completed === false);
default:
return state;
default:
return state;
}
}

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4002;
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.get("/", function(req, res) {
app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html');
});
app.listen(port, function(error) {
app.listen(port, function (error) {
if (error) {
console.error(error);
} 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 { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router';
import {
reduxReactRouter,
routerStateReducer,
ReduxRouter,
} from 'redux-router';
//import createHistory from 'history/lib/createBrowserHistory';
import createHistory from 'history/lib/createHashHistory';
import rootReducer from '../reducers';
@ -7,7 +11,7 @@ import rootReducer from '../reducers';
export default function configureStore(initialState) {
let finalCreateStore = compose(
reduxReactRouter({ createHistory }),
global.devToolsExtension ? global.devToolsExtension() : f => f
global.devToolsExtension ? global.devToolsExtension() : (f) => f
)(createStore);
const store = finalCreateStore(rootReducer, initialState);

View File

@ -6,14 +6,14 @@ describe('todo actions', () => {
it('addTodo should create ADD_TODO action', () => {
expect(actions.addTodo('Use Redux')).toEqual({
type: types.ADD_TODO,
text: 'Use Redux'
text: 'Use Redux',
});
});
it('deleteTodo should create DELETE_TODO action', () => {
expect(actions.deleteTodo(1)).toEqual({
type: types.DELETE_TODO,
id: 1
id: 1,
});
});
@ -21,26 +21,26 @@ describe('todo actions', () => {
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
type: types.EDIT_TODO,
id: 1,
text: 'Use Redux everywhere'
text: 'Use Redux everywhere',
});
});
it('completeTodo should create COMPLETE_TODO action', () => {
expect(actions.completeTodo(1)).toEqual({
type: types.COMPLETE_TODO,
id: 1
id: 1,
});
});
it('completeAll should create COMPLETE_ALL action', () => {
expect(actions.completeAll()).toEqual({
type: types.COMPLETE_ALL
type: types.COMPLETE_ALL,
});
});
it('clearCompleted should create CLEAR_COMPLETED action', () => {
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';
function setup(propOverrides) {
const props = Object.assign({
completedCount: 0,
activeCount: 0,
filter: SHOW_ALL,
onClearCompleted: expect.createSpy(),
onShow: expect.createSpy()
}, propOverrides);
const props = Object.assign(
{
completedCount: 0,
activeCount: 0,
filter: SHOW_ALL,
onClearCompleted: expect.createSpy(),
onShow: expect.createSpy(),
},
propOverrides
);
const renderer = TestUtils.createRenderer();
renderer.render(<Footer {...props} />);
@ -19,13 +22,14 @@ function setup(propOverrides) {
return {
props: props,
output: output
output: output,
};
}
function getTextContent(elem) {
const children = Array.isArray(elem.props.children) ?
elem.props.children : [elem.props.children];
const children = Array.isArray(elem.props.children)
? elem.props.children
: [elem.props.children];
return children.reduce(function concatText(out, child) {
// Children are either elements or text strings
@ -63,11 +67,13 @@ describe('components', () => {
expect(filter.type).toBe('li');
const a = filter.props.children;
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
expect(a.props.children).toBe({
0: 'All',
1: 'Active',
2: 'Completed'
}[i]);
expect(a.props.children).toBe(
{
0: 'All',
1: 'Active',
2: 'Completed',
}[i]
);
});
});
@ -81,20 +87,20 @@ describe('components', () => {
it('shouldnt show clear button when no completed todos', () => {
const { output } = setup({ completedCount: 0 });
const [,, clear] = output.props.children;
const [, , clear] = output.props.children;
expect(clear).toBe(undefined);
});
it('should render clear button when completed todos', () => {
const { output } = setup({ completedCount: 1 });
const [,, clear] = output.props.children;
const [, , clear] = output.props.children;
expect(clear.type).toBe('button');
expect(clear.props.children).toBe('Clear completed');
});
it('should call onClearCompleted on clear button click', () => {
const { output, props } = setup({ completedCount: 1 });
const [,, clear] = output.props.children;
const [, , clear] = output.props.children;
clear.props.onClick({});
expect(props.onClearCompleted).toHaveBeenCalled();
});

View File

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

View File

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

View File

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

View File

@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
describe('todos reducer', () => {
it('should handle initial state', () => {
expect(
todos(undefined, {})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
expect(todos(undefined, {})).toEqual([
{
text: 'Use Redux',
completed: false,
id: 0,
},
]);
});
it('should handle ADD_TODO', () => {
expect(
todos([], {
type: types.ADD_TODO,
text: 'Run the tests'
text: 'Run the tests',
})
).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([{
).toEqual([
{
text: 'Run the tests',
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',
completed: false,
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
}]);
id: 0,
},
]);
expect(
todos(
[
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
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', () => {
expect(
todos([{
text: 'Run the tests',
completed: false,
id: 1
}, {
todos(
[
{
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',
completed: false,
id: 0
}], {
type: types.DELETE_TODO,
id: 1
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should handle EDIT_TODO', () => {
expect(
todos([{
text: 'Run the tests',
todos(
[
{
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,
id: 1
}, {
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,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should handle COMPLETE_TODO', () => {
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',
completed: false,
id: 1
}, {
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.COMPLETE_TODO,
id: 1
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should handle COMPLETE_ALL', () => {
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',
completed: true,
id: 1
}, {
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.COMPLETE_ALL
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: true,
id: 0
}]);
completed: true,
id: 0,
},
]);
// Unmark if all todos are currently completed
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',
completed: true,
id: 1
}, {
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: true,
id: 0
}], {
type: types.COMPLETE_ALL
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
completed: false,
id: 0,
},
]);
});
it('should handle CLEAR_COMPLETED', () => {
expect(
todos([{
text: 'Run the tests',
completed: true,
id: 1
}, {
todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.CLEAR_COMPLETED,
}
)
).toEqual([
{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.CLEAR_COMPLETED
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
expect(
[{
type: types.COMPLETE_TODO,
id: 0
}, {
type: types.CLEAR_COMPLETED
}, {
type: types.ADD_TODO,
text: 'Write more tests'
}].reduce(todos, [{
id: 0,
[
{
type: types.COMPLETE_TODO,
id: 0,
},
{
type: types.CLEAR_COMPLETED,
},
{
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,
text: 'Use Redux'
}, {
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 = {
devtool: 'cheap-module-eval-source-map',
entry: [
'webpack-hot-middleware/client',
'./index'
],
entry: ['webpack-hot-middleware/client', './index'],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
publicPath: '/static/',
},
plugins: [
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
new webpack.NoErrorsPlugin(),
],
module: {
loaders: [{
test: /\.js$/,
loaders: ['babel'],
exclude: /node_modules/,
include: __dirname
}, {
test: /\.css?$/,
loaders: ['style', 'raw'],
include: __dirname
}]
}
loaders: [
{
test: /\.js$/,
loaders: ['babel'],
exclude: /node_modules/,
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>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redux Saga Counter example</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="/static/bundle.js"></script>
<script type="text/javascript" src="/static/bundle.js"></script>
</body>
</html>

View File

@ -1,33 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import React from 'react';
import PropTypes from 'prop-types';
const Counter = ({ value, onIncrement, onIncrementAsync, onDecrement, onIncrementIfOdd }) =>
<p>
Clicked: {value} times
{' '}
<button onClick={onIncrement}>
+
</button>
{' '}
<button onClick={onDecrement}>
-
</button>
{' '}
<button onClick={onIncrementIfOdd}>
Increment if odd
</button>
{' '}
<button onClick={onIncrementAsync}>
Increment async
</button>
</p>
const Counter = ({
value,
onIncrement,
onIncrementAsync,
onDecrement,
onIncrementIfOdd,
}) => (
<p>
Clicked: {value} times <button onClick={onIncrement}>+</button>{' '}
<button onClick={onDecrement}>-</button>{' '}
<button onClick={onIncrementIfOdd}>Increment if odd</button>{' '}
<button onClick={onIncrementAsync}>Increment async</button>
</p>
);
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: 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 ReactDOM from 'react-dom'
import { createStore, applyMiddleware, compose } from 'redux'
import createSagaMiddleware from 'redux-saga'
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
// import sagaMonitor from './sagaMonitor'
import Counter from './components/Counter'
import reducer from './reducers'
import rootSaga from './sagas'
import Counter from './components/Counter';
import reducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware(/* {sagaMonitor} */)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, traceLimit: 25 }) || compose;
const sagaMiddleware = createSagaMiddleware(/* {sagaMonitor} */);
const composeEnhancers =
(window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
trace: true,
traceLimit: 25,
})) ||
compose;
const store = createStore(
reducer,
composeEnhancers(applyMiddleware(sagaMiddleware))
)
sagaMiddleware.run(rootSaga)
);
sagaMiddleware.run(rootSaga);
const action = type => store.dispatch({type})
const action = (type) => store.dispatch({ type });
function render() {
ReactDOM.render(
@ -29,10 +33,11 @@ function render() {
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementIfOdd={() => action('INCREMENT_IF_ODD')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
onIncrementAsync={() => action('INCREMENT_ASYNC')}
/>,
document.getElementById('root')
)
);
}
render()
store.subscribe(render)
render();
store.subscribe(render);

View File

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

View File

@ -1,14 +1,14 @@
/* eslint-disable no-constant-condition */
import { takeEvery } from 'redux-saga'
import { put, call } from 'redux-saga/effects'
import { delay } from 'redux-saga'
import { takeEvery } from 'redux-saga';
import { put, call } from 'redux-saga/effects';
import { delay } from 'redux-saga';
export function* incrementAsync() {
yield call(delay, 1000)
yield put({type: 'INCREMENT'})
yield call(delay, 1000);
yield put({ type: 'INCREMENT' });
}
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 = {
mode: 'development',
devtool: 'source-map',
entry: [
path.join(__dirname, 'src', 'main')
],
entry: [path.join(__dirname, 'src', 'main')],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
publicPath: '/static/',
},
module: {
rules: [{
test: /\.js$/,
loaders: ['babel-loader'],
exclude: /node_modules/
}]
rules: [
{
test: /\.js$/,
loaders: ['babel-loader'],
exclude: /node_modules/,
},
],
},
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 PropTypes from 'prop-types';
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 = {
[SHOW_ALL]: 'All',
[SHOW_ACTIVE]: 'Active',
[SHOW_COMPLETED]: 'Completed'
[SHOW_COMPLETED]: 'Completed',
};
class Footer extends Component {
@ -26,9 +30,11 @@ class Footer extends Component {
const { filter: selectedFilter, onShow } = this.props;
return (
<a className={classnames({ selected: filter === selectedFilter })}
style={{ cursor: 'pointer' }}
onClick={() => onShow(filter)}>
<a
className={classnames({ selected: filter === selectedFilter })}
style={{ cursor: 'pointer' }}
onClick={() => onShow(filter)}
>
{title}
</a>
);
@ -38,8 +44,7 @@ class Footer extends Component {
const { completedCount, onClearCompleted } = this.props;
if (completedCount > 0) {
return (
<button className="clear-completed"
onClick={onClearCompleted} >
<button className="clear-completed" onClick={onClearCompleted}>
Clear completed
</button>
);
@ -51,11 +56,9 @@ class Footer extends Component {
<footer className="footer">
{this.renderTodoCount()}
<ul className="filters">
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter =>
<li key={filter}>
{this.renderFilterLink(filter)}
</li>
)}
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
<li key={filter}>{this.renderFilterLink(filter)}</li>
))}
</ul>
{this.renderClearButton()}
</footer>
@ -68,7 +71,7 @@ Footer.propTypes = {
activeCount: PropTypes.number.isRequired,
filter: PropTypes.string.isRequired,
onClearCompleted: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired
onShow: PropTypes.func.isRequired,
};
export default Footer;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,17 +7,26 @@ var app = new require('express')();
var port = 4002;
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.get("/", function(req, res) {
app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html');
});
app.listen(port, function(error) {
app.listen(port, function (error) {
if (error) {
console.error(error);
} 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';
export default function configureStore(preloadedState) {
const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({ actionCreators, serialize: true, trace: true });
const enhancer =
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
actionCreators,
serialize: true,
trace: true,
});
if (!enhancer) {
console.warn('Install Redux DevTools Extension to inspect the app state: ' +
'https://github.com/zalmoxisus/redux-devtools-extension#installation')
console.warn(
'Install Redux DevTools Extension to inspect the app state: ' +
'https://github.com/zalmoxisus/redux-devtools-extension#installation'
);
}
const store = createStore(rootReducer, preloadedState, enhancer);
@ -15,7 +22,7 @@ export default function configureStore(preloadedState) {
if (module.hot) {
// Enable Webpack hot module replacement for 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', () => {
expect(actions.addTodo('Use Redux')).toEqual({
type: types.ADD_TODO,
text: 'Use Redux'
text: 'Use Redux',
});
});
it('deleteTodo should create DELETE_TODO action', () => {
expect(actions.deleteTodo(1)).toEqual({
type: types.DELETE_TODO,
id: 1
id: 1,
});
});
@ -21,26 +21,26 @@ describe('todo actions', () => {
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
type: types.EDIT_TODO,
id: 1,
text: 'Use Redux everywhere'
text: 'Use Redux everywhere',
});
});
it('completeTodo should create COMPLETE_TODO action', () => {
expect(actions.completeTodo(1)).toEqual({
type: types.COMPLETE_TODO,
id: 1
id: 1,
});
});
it('completeAll should create COMPLETE_ALL action', () => {
expect(actions.completeAll()).toEqual({
type: types.COMPLETE_ALL
type: types.COMPLETE_ALL,
});
});
it('clearCompleted should create CLEAR_COMPLETED action', () => {
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';
function setup(propOverrides) {
const props = Object.assign({
completedCount: 0,
activeCount: 0,
filter: SHOW_ALL,
onClearCompleted: expect.createSpy(),
onShow: expect.createSpy()
}, propOverrides);
const props = Object.assign(
{
completedCount: 0,
activeCount: 0,
filter: SHOW_ALL,
onClearCompleted: expect.createSpy(),
onShow: expect.createSpy(),
},
propOverrides
);
const renderer = TestUtils.createRenderer();
renderer.render(<Footer {...props} />);
@ -19,13 +22,14 @@ function setup(propOverrides) {
return {
props: props,
output: output
output: output,
};
}
function getTextContent(elem) {
const children = Array.isArray(elem.props.children) ?
elem.props.children : [elem.props.children];
const children = Array.isArray(elem.props.children)
? elem.props.children
: [elem.props.children];
return children.reduce(function concatText(out, child) {
// Children are either elements or text strings
@ -63,11 +67,13 @@ describe('components', () => {
expect(filter.type).toBe('li');
const a = filter.props.children;
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
expect(a.props.children).toBe({
0: 'All',
1: 'Active',
2: 'Completed'
}[i]);
expect(a.props.children).toBe(
{
0: 'All',
1: 'Active',
2: 'Completed',
}[i]
);
});
});
@ -81,20 +87,20 @@ describe('components', () => {
it('shouldnt show clear button when no completed todos', () => {
const { output } = setup({ completedCount: 0 });
const [,, clear] = output.props.children;
const [, , clear] = output.props.children;
expect(clear).toBe(undefined);
});
it('should render clear button when completed todos', () => {
const { output } = setup({ completedCount: 1 });
const [,, clear] = output.props.children;
const [, , clear] = output.props.children;
expect(clear.type).toBe('button');
expect(clear.props.children).toBe('Clear completed');
});
it('should call onClearCompleted on clear button click', () => {
const { output, props } = setup({ completedCount: 1 });
const [,, clear] = output.props.children;
const [, , clear] = output.props.children;
clear.props.onClick({});
expect(props.onClearCompleted).toHaveBeenCalled();
});

View File

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

View File

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

View File

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

View File

@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
describe('todos reducer', () => {
it('should handle initial state', () => {
expect(
todos(undefined, {})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
expect(todos(undefined, {})).toEqual([
{
text: 'Use Redux',
completed: false,
id: 0,
},
]);
});
it('should handle ADD_TODO', () => {
expect(
todos([], {
type: types.ADD_TODO,
text: 'Run the tests'
text: 'Run the tests',
})
).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([{
).toEqual([
{
text: 'Run the tests',
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',
completed: false,
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
}]);
id: 0,
},
]);
expect(
todos(
[
{
text: 'Run the tests',
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: false,
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', () => {
expect(
todos([{
text: 'Run the tests',
completed: false,
id: 1
}, {
todos(
[
{
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',
completed: false,
id: 0
}], {
type: types.DELETE_TODO,
id: 1
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should handle EDIT_TODO', () => {
expect(
todos([{
text: 'Run the tests',
todos(
[
{
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,
id: 1
}, {
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,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should handle COMPLETE_TODO', () => {
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',
completed: false,
id: 1
}, {
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.COMPLETE_TODO,
id: 1
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should handle COMPLETE_ALL', () => {
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',
completed: true,
id: 1
}, {
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.COMPLETE_ALL
})
).toEqual([{
text: 'Run the tests',
completed: true,
id: 1
}, {
text: 'Use Redux',
completed: true,
id: 0
}]);
completed: true,
id: 0,
},
]);
// Unmark if all todos are currently completed
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',
completed: true,
id: 1
}, {
completed: false,
id: 1,
},
{
text: 'Use Redux',
completed: true,
id: 0
}], {
type: types.COMPLETE_ALL
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
completed: false,
id: 0,
},
]);
});
it('should handle CLEAR_COMPLETED', () => {
expect(
todos([{
text: 'Run the tests',
completed: true,
id: 1
}, {
todos(
[
{
text: 'Run the tests',
completed: true,
id: 1,
},
{
text: 'Use Redux',
completed: false,
id: 0,
},
],
{
type: types.CLEAR_COMPLETED,
}
)
).toEqual([
{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.CLEAR_COMPLETED
})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
id: 0,
},
]);
});
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
expect(
[{
type: types.COMPLETE_TODO,
id: 0
}, {
type: types.CLEAR_COMPLETED
}, {
type: types.ADD_TODO,
text: 'Write more tests'
}].reduce(todos, [{
id: 0,
[
{
type: types.COMPLETE_TODO,
id: 0,
},
{
type: types.CLEAR_COMPLETED,
},
{
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,
text: 'Use Redux'
}, {
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 = {
mode: 'development',
devtool: 'source-map',
entry: [
'webpack-hot-middleware/client',
'./index'
],
entry: ['webpack-hot-middleware/client', './index'],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
publicPath: '/static/',
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
plugins: [new webpack.HotModuleReplacementPlugin()],
module: {
rules: [{
test: /\.js$/,
loaders: ['babel-loader'],
exclude: /node_modules/
}, {
test: /\.css?$/,
loaders: ['style-loader', 'raw-loader'],
include: __dirname
}]
}
rules: [
{
test: /\.js$/,
loaders: ['babel-loader'],
exclude: /node_modules/,
},
{
test: /\.css?$/,
loaders: ['style-loader', 'raw-loader'],
include: __dirname,
},
],
},
};

View File

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

View File

@ -3,55 +3,72 @@ import mapValues from 'lodash/mapValues';
export const FilterState = {
DO_NOT_FILTER: 'DO_NOT_FILTER',
BLACKLIST_SPECIFIC: 'BLACKLIST_SPECIFIC',
WHITELIST_SPECIFIC: 'WHITELIST_SPECIFIC'
WHITELIST_SPECIFIC: 'WHITELIST_SPECIFIC',
};
export function getLocalFilter(config) {
if (config.actionsBlacklist || config.actionsWhitelist) {
return {
whitelist: Array.isArray(config.actionsWhitelist) ? config.actionsWhitelist.join('|') : config.actionsWhitelist,
blacklist: Array.isArray(config.actionsBlacklist) ? config.actionsBlacklist.join('|') : config.actionsBlacklist
whitelist: Array.isArray(config.actionsWhitelist)
? config.actionsWhitelist.join('|')
: config.actionsWhitelist,
blacklist: Array.isArray(config.actionsBlacklist)
? config.actionsBlacklist.join('|')
: config.actionsBlacklist,
};
}
return undefined;
}
export const noFiltersApplied = (localFilter) => (
export const noFiltersApplied = (localFilter) =>
// !predicate &&
!localFilter && (!window.devToolsOptions || !window.devToolsOptions.filter ||
window.devToolsOptions.filter === FilterState.DO_NOT_FILTER)
);
!localFilter &&
(!window.devToolsOptions ||
!window.devToolsOptions.filter ||
window.devToolsOptions.filter === FilterState.DO_NOT_FILTER);
export function isFiltered(action, localFilter) {
if (
noFiltersApplied(localFilter) ||
typeof action !== 'string' && typeof action.type.match !== 'function'
) return false;
(typeof action !== 'string' && typeof action.type.match !== 'function')
)
return false;
const { whitelist, blacklist } = localFilter || window.devToolsOptions || {};
const actionType = action.type || action;
return (
whitelist && !actionType.match(whitelist) ||
blacklist && actionType.match(blacklist)
(whitelist && !actionType.match(whitelist)) ||
(blacklist && actionType.match(blacklist))
);
}
function filterActions(actionsById, actionSanitizer) {
if (!actionSanitizer) return actionsById;
return mapValues(actionsById, (action, id) => (
{ ...action, action: actionSanitizer(action.action, id) }
));
return mapValues(actionsById, (action, id) => ({
...action,
action: actionSanitizer(action.action, id),
}));
}
function filterStates(computedStates, stateSanitizer) {
if (!stateSanitizer) return computedStates;
return computedStates.map((state, idx) => (
{ ...state, state: stateSanitizer(state.state, idx) }
));
return computedStates.map((state, idx) => ({
...state,
state: stateSanitizer(state.state, idx),
}));
}
export function filterState(state, type, localFilter, stateSanitizer, actionSanitizer, nextActionId, predicate) {
if (type === 'ACTION') return !stateSanitizer ? state : stateSanitizer(state, nextActionId - 1);
export function filterState(
state,
type,
localFilter,
stateSanitizer,
actionSanitizer,
nextActionId,
predicate
) {
if (type === 'ACTION')
return !stateSanitizer ? state : stateSanitizer(state, nextActionId - 1);
else if (type !== 'STATE') return state;
if (predicate || !noFiltersApplied(localFilter)) {
@ -74,11 +91,14 @@ export function filterState(state, type, localFilter, stateSanitizer, actionSani
filteredStagedActionIds.push(id);
filteredComputedStates.push(
stateSanitizer ? { ...liftedState, state: stateSanitizer(currState, idx) } : liftedState
stateSanitizer
? { ...liftedState, state: stateSanitizer(currState, idx) }
: liftedState
);
if (actionSanitizer) {
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,
actionsById: sanitizedActionsById || actionsById,
stagedActionIds: filteredStagedActionIds,
computedStates: filteredComputedStates
computedStates: filteredComputedStates,
};
}
@ -95,12 +115,17 @@ export function filterState(state, type, localFilter, stateSanitizer, actionSani
return {
...state,
actionsById: filterActions(state.actionsById, actionSanitizer),
computedStates: filterStates(state.computedStates, stateSanitizer)
computedStates: filterStates(state.computedStates, stateSanitizer),
};
}
export function startingFrom(
sendingActionId, state, localFilter, stateSanitizer, actionSanitizer, predicate
sendingActionId,
state,
localFilter,
stateSanitizer,
actionSanitizer,
predicate
) {
const stagedActionIds = state.stagedActionIds;
if (sendingActionId <= stagedActionIds[1]) return state;
@ -124,17 +149,21 @@ export function startingFrom(
if (shouldFilter) {
if (
predicate && !predicate(currState.state, currAction.action) ||
(predicate && !predicate(currState.state, currAction.action)) ||
isFiltered(currAction.action, localFilter)
) continue;
)
continue;
filteredStagedActionIds.push(key);
if (i < index) continue;
}
newActionsById[key] = !actionSanitizer ? currAction :
{ ...currAction, action: actionSanitizer(currAction.action, key) };
newActionsById[key] = !actionSanitizer
? currAction
: { ...currAction, action: actionSanitizer(currAction.action, key) };
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,
stagedActionIds: filteredStagedActionIds,
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';
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;
let parse = jsan.parse;
if (serialize) {
if (serialize.immutable) {
parse = v => jsan.parse(v, seralizeImmutable(
serialize.immutable, serialize.refs, serialize.replacer, serialize.reviver
).reviver);
parse = (v) =>
jsan.parse(
v,
seralizeImmutable(
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
).reviver
);
} else if (serialize.reviver) {
parse = v => jsan.parse(v, serialize.reviver);
parse = (v) => jsan.parse(v, serialize.reviver);
}
}
let preloadedState;
let nextLiftedState = parse(state);
if (nextLiftedState.payload) {
if (nextLiftedState.preloadedState) preloadedState = parse(nextLiftedState.preloadedState);
if (nextLiftedState.preloadedState)
preloadedState = parse(nextLiftedState.preloadedState);
nextLiftedState = parse(nextLiftedState.payload);
}
if (deserializeState) {
deprecate('deserializeState');
if (typeof nextLiftedState.computedStates !== 'undefined') {
nextLiftedState.computedStates = nextLiftedState.computedStates.map(computedState => ({
...computedState,
state: deserializeState(computedState.state)
}));
nextLiftedState.computedStates = nextLiftedState.computedStates.map(
(computedState) => ({
...computedState,
state: deserializeState(computedState.state),
})
);
}
if (typeof nextLiftedState.committedState !== 'undefined') {
nextLiftedState.committedState = deserializeState(nextLiftedState.committedState);
nextLiftedState.committedState = deserializeState(
nextLiftedState.committedState
);
}
if (typeof preloadedState !== 'undefined') {
preloadedState = deserializeState(preloadedState);
@ -42,10 +59,13 @@ export default function importState(state, { deserializeState, deserializeAction
}
if (deserializeAction) {
deprecate('deserializeAction');
nextLiftedState.actionsById = mapValues(nextLiftedState.actionsById, liftedAction => ({
...liftedAction,
action: deserializeAction(liftedAction.action)
}));
nextLiftedState.actionsById = mapValues(
nextLiftedState.actionsById,
(liftedAction) => ({
...liftedAction,
action: deserializeAction(liftedAction.action),
})
);
}
return { nextLiftedState, preloadedState };

View File

@ -21,20 +21,29 @@ function tryCatchStringify(obj) {
return JSON.stringify(obj);
} catch (err) {
/* 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 */
return jsan.stringify(obj, windowReplacer, null, { circular: '[CIRCULAR]', date: true });
return jsan.stringify(obj, windowReplacer, null, {
circular: '[CIRCULAR]',
date: true,
});
}
}
let stringifyWarned;
function stringify(obj, serialize) {
const str = typeof serialize === 'undefined' ? tryCatchStringify(obj) :
jsan.stringify(obj, serialize.replacer, null, serialize.options);
const str =
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 */
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 */
stringifyWarned = true;
}
@ -48,22 +57,34 @@ export function getSeralizeParameter(config, param) {
if (serialize === true) return { options: true };
if (serialize.immutable) {
const immutableSerializer = seralizeImmutable(
serialize.immutable, serialize.refs, serialize.replacer, serialize.reviver
serialize.immutable,
serialize.refs,
serialize.replacer,
serialize.reviver
);
return {
replacer: immutableSerializer.replacer,
reviver: immutableSerializer.reviver,
options: typeof serialize.options === 'object' ?
{ ...immutableSerializer.options, ...serialize.options } : immutableSerializer.options
options:
typeof serialize.options === 'object'
? { ...immutableSerializer.options, ...serialize.options }
: immutableSerializer.options,
};
}
if (!serialize.replacer && !serialize.reviver) return { options: serialize.options };
return { replacer: serialize.replacer, reviver: serialize.reviver, options: serialize.options || true };
if (!serialize.replacer && !serialize.reviver)
return { options: serialize.options };
return {
replacer: serialize.replacer,
reviver: serialize.reviver,
options: serialize.options || true,
};
}
const value = config[param];
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 === 'function') return { replacer: value };
@ -94,10 +115,16 @@ function getStackTrace(config, toExcludeFromTrace) {
}
stack = error.stack;
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');
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;
@ -106,7 +133,8 @@ function getStackTrace(config, toExcludeFromTrace) {
function amendActionType(action, config, toExcludeFromTrace) {
let timestamp = Date.now();
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.action) return stack ? { stack, ...action } : action;
return { action, timestamp, stack };
@ -117,7 +145,12 @@ export function toContentScript(message, serializeState, serializeAction) {
message.action = stringify(message.action, serializeAction);
message.payload = stringify(message.payload, serializeState);
} 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.actionsById = stringify(actionsById, serializeAction);
message.computedStates = stringify(computedStates, serializeState);
@ -125,7 +158,10 @@ export function toContentScript(message, serializeState, serializeAction) {
} else if (message.type === 'EXPORT') {
message.payload = stringify(message.payload, serializeAction);
if (typeof message.committedState !== 'undefined') {
message.committedState = stringify(message.committedState, serializeState);
message.committedState = stringify(
message.committedState,
serializeState
);
}
}
post(message);
@ -145,19 +181,23 @@ export function sendMessage(action, state, config, instanceId, name) {
maxAge: config.maxAge,
source,
name: config.name || name,
instanceId: config.instanceId || instanceId || 1
instanceId: config.instanceId || instanceId || 1,
};
toContentScript(message, config.serialize, config.serialize);
}
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;
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 (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);
}
const liftListener = (listener, config) => message => {
const liftListener = (listener, config) => (message) => {
let data = {};
if (message.type === 'IMPORT') {
data.type = 'DISPATCH';
data.payload = {
type: 'IMPORT_STATE',
...importState(message.state, config)
...importState(message.state, config),
};
} else {
data = message;
@ -189,7 +229,9 @@ export function connect(preConfig) {
const config = preConfig || {};
const id = generateId(config.instanceId);
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);
const actionCreators = config.actionCreators || {};
const latency = config.latency;
@ -200,7 +242,7 @@ export function connect(preConfig) {
let delayedActions = [];
let delayedStates = [];
const rootListiner = action => {
const rootListiner = (action) => {
if (autoPause) {
if (action.type === 'START') isPaused = false;
else if (action.type === 'STOP') isPaused = true;
@ -213,7 +255,7 @@ export function connect(preConfig) {
type: 'LIFTED',
liftedState: { isPaused },
instanceId: id,
source
source,
});
}
}
@ -243,20 +285,29 @@ export function connect(preConfig) {
}, latency);
const send = (action, state) => {
if (isPaused || isFiltered(action, localFilter) || predicate && !predicate(state, action)) {
if (
isPaused ||
isFiltered(action, localFilter) ||
(predicate && !predicate(state, action))
) {
return;
}
let amendedAction = action;
const amendedState = config.stateSanitizer ? config.stateSanitizer(state) : state;
const amendedState = config.stateSanitizer
? config.stateSanitizer(state)
: state;
if (action) {
if (config.getActionType) {
amendedAction = config.getActionType(action);
if (typeof amendedAction !== 'object') {
amendedAction = { action: { type: amendedAction }, timestamp: Date.now() };
amendedAction = {
action: { type: amendedAction },
timestamp: Date.now(),
};
}
}
else if (config.actionSanitizer) amendedAction = config.actionSanitizer(action);
} else if (config.actionSanitizer)
amendedAction = config.actionSanitizer(action);
amendedAction = amendActionType(amendedAction, config, send);
if (latency) {
delayedActions.push(amendedAction);
@ -273,9 +324,10 @@ export function connect(preConfig) {
type: 'INIT',
payload: stringify(state, config.serialize),
instanceId: id,
source
source,
};
if (liftedData && Array.isArray(liftedData)) { // Legacy
if (liftedData && Array.isArray(liftedData)) {
// Legacy
message.action = stringify(liftedData);
message.name = config.name;
} else {
@ -288,7 +340,7 @@ export function connect(preConfig) {
name: config.name || document.title,
features: config.features,
serialize: !!config.serialize,
type: config.type
type: config.type,
};
}
post(message);
@ -307,16 +359,18 @@ export function connect(preConfig) {
subscribe,
unsubscribe,
send,
error
error,
};
}
export function updateStore(stores) {
return function(newStore, instanceId) {
return function (newStore, instanceId) {
/* eslint-disable no-console */
console.warn('`__REDUX_DEVTOOLS_EXTENSION__.updateStore` is deprecated, remove it and just use ' +
'`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__` instead of the extension\'s store enhancer: ' +
'https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup');
console.warn(
'`__REDUX_DEVTOOLS_EXTENSION__.updateStore` is deprecated, remove it and just use ' +
"`__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 */
const store = stores[instanceId || Object.keys(stores)[0]];
// Mutate the store in order to keep the reference

View File

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

View File

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

View File

@ -41,13 +41,25 @@ class App extends Component {
render() {
const {
monitor, position, togglePersist,
dispatcherIsOpen, sliderIsOpen, options, liftedState
monitor,
position,
togglePersist,
dispatcherIsOpen,
sliderIsOpen,
options,
liftedState,
} = this.props;
if (!position && (!options || !options.features)) {
return (
<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>
);
}
@ -55,7 +67,7 @@ class App extends Component {
return (
<div style={styles.container}>
<div style={styles.buttonBar}>
<MonitorSelector selected={monitor}/>
<MonitorSelector selected={monitor} />
<Instances selected={this.props.selected} />
</div>
<DevTools
@ -66,7 +78,7 @@ class App extends Component {
lib={options.lib || options.explicitLib}
/>
<Notification />
{sliderIsOpen && options.connectionId && options.features.jump &&
{sliderIsOpen && options.connectionId && options.features.jump && (
<SliderMonitor
monitor="SliderMonitor"
liftedState={liftedState}
@ -77,68 +89,67 @@ class App extends Component {
style={{ padding: '15px 5px' }}
fillColor="rgb(120, 144, 156)"
/>
}
{dispatcherIsOpen && options.connectionId && options.features.dispatch &&
<Dispatcher options={options} />
}
)}
{dispatcherIsOpen &&
options.connectionId &&
options.features.dispatch && <Dispatcher options={options} />}
<div style={styles.buttonBar}>
{!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 &&
{!window.isElectron && position !== '#left' && (
<Button
Icon={PersistIcon}
onClick={togglePersist}
>Persist</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>
}
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 Icon={PersistIcon} onClick={togglePersist}>
Persist
</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>
);
@ -158,7 +169,7 @@ App.propTypes = {
position: PropTypes.string,
reports: PropTypes.array.isRequired,
dispatcherIsOpen: PropTypes.bool,
sliderIsOpen: PropTypes.bool
sliderIsOpen: PropTypes.bool,
};
function mapStateToProps(state) {
@ -173,7 +184,7 @@ function mapStateToProps(state) {
dispatcherIsOpen: state.monitor.dispatcherIsOpen,
sliderIsOpen: state.monitor.sliderIsOpen,
reports: state.reports.data,
shouldSync: state.instances.sync
shouldSync: state.instances.sync,
};
}
@ -181,7 +192,9 @@ function mapDispatchToProps(dispatch) {
return {
liftedDispatch: bindActionCreators(liftedDispatch, 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 { 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 syncOptions from '../../browser/extension/options/syncOptions';
import openDevToolsWindow from '../../browser/extension/background/openWindow';
@ -10,21 +14,22 @@ const DISCONNECTED = 'socket/DISCONNECTED';
const connections = {
tab: {},
panel: {},
monitor: {}
monitor: {},
};
const chunks = {};
let monitors = 0;
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) {
Object.keys(connections.monitor).forEach(id => {
Object.keys(connections.monitor).forEach((id) => {
connections.monitor[id].postMessage(
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);
});
}
@ -34,13 +39,13 @@ function toContentScript({ message, action, id, instanceId, state }) {
type: message,
action,
state: nonReduxDispatch(window.store, message, instanceId, action, state),
id: instanceId.toString().replace(/^[^\/]+\//, '')
id: instanceId.toString().replace(/^[^\/]+\//, ''),
});
}
function toAllTabs(msg) {
const tabs = connections.tab;
Object.keys(tabs).forEach(id => {
Object.keys(tabs).forEach((id) => {
tabs[id].postMessage(msg);
});
}
@ -67,7 +72,7 @@ function getReducerError() {
function togglePersist() {
const state = window.store.getState();
if (state.persistStates) {
Object.keys(state.instances.connections).forEach(id => {
Object.keys(state.instances.connections).forEach((id) => {
if (connections.tab[id]) return;
window.store.dispatch({ type: REMOVE_INSTANCE, id });
toMonitors({ type: 'NA', id });
@ -92,7 +97,7 @@ function messaging(request, sender, sendResponse) {
return;
}
if (request.type === 'GET_OPTIONS') {
window.syncOptions.get(options => {
window.syncOptions.get((options) => {
sendResponse({ options });
});
return;
@ -103,7 +108,11 @@ function messaging(request, sender, sendResponse) {
}
if (request.type === 'OPEN') {
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;
}
openDevToolsWindow(position);
@ -118,10 +127,12 @@ function messaging(request, sender, sendResponse) {
const reducerError = getReducerError();
chrome.notifications.create('app-error', {
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,
iconUrl: 'img/logo/48x48.png',
isClickable: !!reducerError
isClickable: !!reducerError,
});
return;
}
@ -134,7 +145,8 @@ function messaging(request, sender, sendResponse) {
return;
}
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;
}
action.request = chunks[instanceId];
@ -180,11 +192,11 @@ function onConnect(port) {
id = getId(port.sender);
if (port.sender.frameId) id = `${id}-${port.sender.frameId}`;
connections.tab[id] = port;
listener = msg => {
listener = (msg) => {
if (msg.name === 'INIT_INSTANCE') {
if (typeof id === 'number') {
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' });
@ -195,8 +207,12 @@ function onConnect(port) {
if (!persistedState) return;
toContentScript({
message: 'IMPORT',
id, instanceId,
state: stringifyJSON(persistedState, state.instances.options[instanceId].serialize)
id,
instanceId,
state: stringifyJSON(
persistedState,
state.instances.options[instanceId].serialize
),
});
}
return;
@ -213,12 +229,13 @@ function onConnect(port) {
monitorInstances(true);
monitors++;
port.onDisconnect.addListener(disconnect('monitor', id));
} else { // devpanel
} else {
// devpanel
id = port.name || port.sender.frameId;
connections.panel[id] = port;
monitorInstances(true, port.name);
monitors++;
listener = msg => {
listener = (msg) => {
window.store.dispatch(msg);
};
port.onMessage.addListener(listener);
@ -231,7 +248,7 @@ chrome.runtime.onConnectExternal.addListener(onConnect);
chrome.runtime.onMessage.addListener(messaging);
chrome.runtime.onMessageExternal.addListener(messaging);
chrome.notifications.onClicked.addListener(id => {
chrome.notifications.onClicked.addListener((id) => {
chrome.notifications.clear(id);
openDevToolsWindow('devtools-right');
});
@ -239,7 +256,7 @@ chrome.notifications.onClicked.addListener(id => {
window.syncOptions = syncOptions(toAllTabs); // Expose to the options page
export default function api() {
return next => action => {
return (next) => (action) => {
if (action.type === LIFTED_ACTION) toContentScript(action);
else if (action.type === 'TOGGLE_PERSIST') togglePersist();
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) {
const instances = store.getState().instances;
@ -10,24 +13,27 @@ function selectInstance(tabId, store, next) {
}
function getCurrentTabId(next) {
chrome.tabs.query({
active: true,
lastFocusedWindow: true
}, tabs => {
const tab = tabs[0];
if (!tab) return;
next(tab.id);
});
chrome.tabs.query(
{
active: true,
lastFocusedWindow: true,
},
(tabs) => {
const tab = tabs[0];
if (!tab) return;
next(tab.id);
}
);
}
export default function popupSelector(store) {
return next => action => {
return (next) => (action) => {
const result = next(action);
if (action.type === UPDATE_STATE) {
if (chrome.devtools && chrome.devtools.inspectedWindow) {
selectInstance(chrome.devtools.inspectedWindow.tabId, store, next);
} else {
getCurrentTabId(tabId => selectInstance(tabId, store, next));
getCurrentTabId((tabId) => selectInstance(tabId, store, next));
}
}
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';
function panelDispatcher(bgConnection) {
let autoselected = false;
const tabId = chrome.devtools.inspectedWindow.tabId;
return store => next => action => {
return (store) => (next) => (action) => {
const result = next(action);
if (!autoselected && action.type === UPDATE_STATE && tabId) {
autoselected = true;
const connections = store.getState()
.instances.connections[tabId];
const connections = store.getState().instances.connections[tabId];
if (connections && connections.length === 1) {
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';
const syncStores = baseStore => store => next => action => {
const syncStores = (baseStore) => (store) => (next) => (action) => {
if (action.type === UPDATE_STATE) {
return next({
...action,
instances: baseStore.getState().instances
instances: baseStore.getState().instances,
});
}
if (action.type === LIFTED_ACTION || action.type === 'TOGGLE_PERSIST') {

View File

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

View File

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

View File

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

View File

@ -1,5 +1,12 @@
import { initialState, dispatchAction } from 'remotedev-app/lib/reducers/instances';
import { UPDATE_STATE, SELECT_INSTANCE, LIFTED_ACTION } from 'remotedev-app/lib/constants/actionTypes';
import {
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) {
switch (action.type) {

View File

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

View File

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

View File

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

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