mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-27 08:30:02 +03:00
Format
This commit is contained in:
parent
39b0fe377e
commit
e972239059
|
@ -3,5 +3,5 @@ module.exports = {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: ['./tsconfig.json'],
|
project: ['./tsconfig.json'],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
Remote Redux DevTools
|
# Remote Redux DevTools
|
||||||
=====================
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -18,11 +17,14 @@ There are 2 ways of usage depending if you're using other store enhancers (middl
|
||||||
#### Add DevTools enhancer to your store
|
#### Add DevTools enhancer to your store
|
||||||
|
|
||||||
If you have a basic [store](http://redux.js.org/docs/api/createStore.html) as described in the official [redux-docs](http://redux.js.org/index.html), simply replace:
|
If you have a basic [store](http://redux.js.org/docs/api/createStore.html) as described in the official [redux-docs](http://redux.js.org/index.html), simply replace:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
const store = createStore(reducer);
|
const store = createStore(reducer);
|
||||||
```
|
```
|
||||||
|
|
||||||
with
|
with
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { createStore } from 'redux';
|
import { createStore } from 'redux';
|
||||||
import devToolsEnhancer from '@redux-devtools/remote';
|
import devToolsEnhancer from '@redux-devtools/remote';
|
||||||
|
@ -37,34 +39,49 @@ If you have a basic [store](http://redux.js.org/docs/api/createStore.html) as de
|
||||||
If you setup your store with [middlewares and enhancers](http://redux.js.org/docs/api/applyMiddleware.html) like [redux-saga](https://github.com/redux-saga/redux-saga) and similar, it is crucial to use `composeWithDevTools` export. Otherwise, actions dispatched from Redux DevTools will not flow to your middlewares.
|
If you setup your store with [middlewares and enhancers](http://redux.js.org/docs/api/applyMiddleware.html) like [redux-saga](https://github.com/redux-saga/redux-saga) and similar, it is crucial to use `composeWithDevTools` export. Otherwise, actions dispatched from Redux DevTools will not flow to your middlewares.
|
||||||
|
|
||||||
In that case change this:
|
In that case change this:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
|
||||||
const store = createStore(reducer, preloadedState, compose(
|
const store = createStore(
|
||||||
applyMiddleware(...middleware),
|
reducer,
|
||||||
|
preloadedState,
|
||||||
|
compose(
|
||||||
|
applyMiddleware(...middleware)
|
||||||
// other store enhancers if any
|
// other store enhancers if any
|
||||||
));
|
)
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
to:
|
to:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { createStore, applyMiddleware } from 'redux';
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
import { composeWithDevTools } from '@redux-devtools/remote';
|
import { composeWithDevTools } from '@redux-devtools/remote';
|
||||||
|
|
||||||
const store = createStore(reducer, /* preloadedState, */ composeWithDevTools(
|
const store = createStore(
|
||||||
applyMiddleware(...middleware),
|
reducer,
|
||||||
|
/* preloadedState, */ composeWithDevTools(
|
||||||
|
applyMiddleware(...middleware)
|
||||||
// other store enhancers if any
|
// other store enhancers if any
|
||||||
));
|
)
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
or with devTools' options:
|
or with devTools' options:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { createStore, applyMiddleware } from 'redux';
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
import { composeWithDevTools } from '@redux-devtools/remote';
|
import { composeWithDevTools } from '@redux-devtools/remote';
|
||||||
|
|
||||||
const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 });
|
const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 });
|
||||||
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(
|
const store = createStore(
|
||||||
applyMiddleware(...middleware),
|
reducer,
|
||||||
|
/* preloadedState, */ composeEnhancers(
|
||||||
|
applyMiddleware(...middleware)
|
||||||
// other store enhancers if any
|
// other store enhancers if any
|
||||||
));
|
)
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Enabling
|
### Enabling
|
||||||
|
@ -72,6 +89,7 @@ If you have a basic [store](http://redux.js.org/docs/api/createStore.html) as de
|
||||||
In order not to allow it in production by default, the enhancer will have effect only when `process.env.NODE_ENV === 'development'`.
|
In order not to allow it in production by default, the enhancer will have effect only when `process.env.NODE_ENV === 'development'`.
|
||||||
|
|
||||||
For Webpack you should add it as following (`webpack.config.dev.js`):
|
For Webpack you should add it as following (`webpack.config.dev.js`):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// ...
|
// ...
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -91,11 +109,12 @@ const store = createStore(reducer, devToolsEnhancer({ realtime: true }));
|
||||||
### Monitoring
|
### Monitoring
|
||||||
|
|
||||||
Use one of our monitor apps to inspect and dispatch actions:
|
Use one of our monitor apps to inspect and dispatch actions:
|
||||||
* [redux-devtools-extension](https://github.com/reduxjs/redux-devtools/tree/main/extension) - Click "Remote" button (or press [`Cmd+Ctrl+Arrow up`](https://github.com/zalmoxisus/redux-devtools-extension#keyboard-shortcuts)) to open remote monitoring.
|
|
||||||
* [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger) - Used in React Native debugger as a dock monitor.
|
- [redux-devtools-extension](https://github.com/reduxjs/redux-devtools/tree/main/extension) - Click "Remote" button (or press [`Cmd+Ctrl+Arrow up`](https://github.com/zalmoxisus/redux-devtools-extension#keyboard-shortcuts)) to open remote monitoring.
|
||||||
* [atom-redux-devtools](https://github.com/zalmoxisus/atom-redux-devtools) - Used in Atom editor.
|
- [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger) - Used in React Native debugger as a dock monitor.
|
||||||
* [redux-dispatch-cli](https://github.com/jhen0409/redux-dispatch-cli) - A CLI tool for Redux remote dispatch.
|
- [atom-redux-devtools](https://github.com/zalmoxisus/atom-redux-devtools) - Used in Atom editor.
|
||||||
* [vscode-redux-devtools](https://github.com/jkzing/vscode-redux-devtools) - Used in Visual Studio Code.
|
- [redux-dispatch-cli](https://github.com/jhen0409/redux-dispatch-cli) - A CLI tool for Redux remote dispatch.
|
||||||
|
- [vscode-redux-devtools](https://github.com/jkzing/vscode-redux-devtools) - Used in Visual Studio Code.
|
||||||
|
|
||||||
Use [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-app) to create your own monitor app.
|
Use [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-app) to create your own monitor app.
|
||||||
|
|
||||||
|
@ -103,57 +122,63 @@ Use [@redux-devtools/app](https://github.com/reduxjs/redux-devtools/tree/main/pa
|
||||||
|
|
||||||
Use [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli).
|
Use [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli).
|
||||||
You can import it in your `server.js` script and start remotedev server together with your development server:
|
You can import it in your `server.js` script and start remotedev server together with your development server:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var reduxDevTools = require('@redux-devtools/cli');
|
var reduxDevTools = require('@redux-devtools/cli');
|
||||||
reduxDevTools({ hostname: 'localhost', port: 8000 });
|
reduxDevTools({ hostname: 'localhost', port: 8000 });
|
||||||
```
|
```
|
||||||
|
|
||||||
See [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli) for more details.
|
See [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli) for more details.
|
||||||
For React Native you can use [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger), which already include `@redux-devtools/cli`.
|
For React Native you can use [remotedev-rn-debugger](https://github.com/jhen0409/remotedev-rn-debugger), which already include `@redux-devtools/cli`.
|
||||||
|
|
||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
|
|
||||||
Name | Description
|
| Name | Description |
|
||||||
------------- | -------------
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
`name` | *String* representing the instance name to be shown on the remote monitor.
|
| `name` | _String_ representing the instance name to be shown on the remote monitor. |
|
||||||
`realtime` | *Boolean* specifies whether to allow remote monitoring. By default is `process.env.NODE_ENV === 'development'`.
|
| `realtime` | _Boolean_ specifies whether to allow remote monitoring. By default is `process.env.NODE_ENV === 'development'`. |
|
||||||
`hostname` | *String* used to specify host for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). If `port` is specified, default value is `localhost`.
|
| `hostname` | _String_ used to specify host for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). If `port` is specified, default value is `localhost`. |
|
||||||
`port` | *Number* used to specify host's port for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli).
|
| `port` | _Number_ used to specify host's port for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). |
|
||||||
`secure` | *Boolean* specifies whether to use `https` protocol for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli).
|
| `secure` | _Boolean_ specifies whether to use `https` protocol for [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-cli). |
|
||||||
`maxAge` | *Number* of maximum allowed actions to be stored on the history tree, the oldest actions are removed once maxAge is reached. Default is `30`.
|
| `maxAge` | _Number_ of maximum allowed actions to be stored on the history tree, the oldest actions are removed once maxAge is reached. Default is `30`. |
|
||||||
`actionsBlacklist` | *array* of actions to be hidden in DevTools. Overwrites corresponding global setting in the options page. See the example bellow.
|
| `actionsBlacklist` | _array_ of actions to be hidden in DevTools. Overwrites corresponding global setting in the options page. See the example bellow. |
|
||||||
`actionsWhitelist` | *array* of actions to be shown. All other actions will be hidden in DevTools.
|
| `actionsWhitelist` | _array_ of actions to be shown. All other actions will be hidden in DevTools. |
|
||||||
`actionSanitizer` | *Function* which takes action object and id number as arguments, and should return action object back. See the example bellow.
|
| `actionSanitizer` | _Function_ which takes action object and id number as arguments, and should return action object back. See the example bellow. |
|
||||||
`stateSanitizer` | *Function* which takes state object and index as arguments, and should return state object back. See the example bellow.
|
| `stateSanitizer` | _Function_ which takes state object and index as arguments, and should return state object back. See the example bellow. |
|
||||||
`startOn` | *String* or *Array of strings* indicating an action or a list of actions, which should start remote monitoring (when `realtime` is `false`).
|
| `startOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should start remote monitoring (when `realtime` is `false`). |
|
||||||
`stopOn` | *String* or *Array of strings* indicating an action or a list of actions, which should stop remote monitoring.
|
| `stopOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should stop remote monitoring. |
|
||||||
`sendOn` | *String* or *Array of strings* indicating an action or a list of actions, which should trigger sending the history to the monitor (without starting it). *Note*: when using it, add a `fetch` polyfill if needed.
|
| `sendOn` | _String_ or _Array of strings_ indicating an action or a list of actions, which should trigger sending the history to the monitor (without starting it). _Note_: when using it, add a `fetch` polyfill if needed. |
|
||||||
`sendOnError` | *Numeric* code: `0` - disabled (default), `1` - send all uncaught exception messages, `2` - send only reducers error messages.
|
| `sendOnError` | _Numeric_ code: `0` - disabled (default), `1` - send all uncaught exception messages, `2` - send only reducers error messages. |
|
||||||
`sendTo` | *String* url of the monitor to send the history when `sendOn` is triggered. By default is `${secure ? 'https' : 'http'}://${hostname}:${port}`.
|
| `sendTo` | _String_ url of the monitor to send the history when `sendOn` is triggered. By default is `${secure ? 'https' : 'http'}://${hostname}:${port}`. |
|
||||||
`actionCreators` | *Array* or *Object* of action creators to dispatch remotely. See [the example](https://github.com/zalmoxisus/remote-redux-devtools/commit/b54652930dfd4e057991df8471c343957fd7bff7).
|
| `actionCreators` | _Array_ or _Object_ of action creators to dispatch remotely. See [the example](https://github.com/zalmoxisus/remote-redux-devtools/commit/b54652930dfd4e057991df8471c343957fd7bff7). |
|
||||||
`shouldHotReload` | *Boolean* - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`.
|
| `shouldHotReload` | _Boolean_ - if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Default to `true`. |
|
||||||
`shouldRecordChanges` | *Boolean* - if specified as `false`, it will not record the changes till clicked on "Start recording" button on the monitor app. Default is `true`.
|
| `shouldRecordChanges` | _Boolean_ - if specified as `false`, it will not record the changes till clicked on "Start recording" button on the monitor app. Default is `true`. |
|
||||||
`shouldStartLocked` | *Boolean* - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`.
|
| `shouldStartLocked` | _Boolean_ - if specified as `true`, it will not allow any non-monitor actions to be dispatched till `lockChanges(false)` is dispatched. Default is `false`. |
|
||||||
`id` | *String* to identify the instance when sending the history triggered by `sendOn`. You can use, for example, user id here, to know who sent the data.
|
| `id` | _String_ to identify the instance when sending the history triggered by `sendOn`. You can use, for example, user id here, to know who sent the data. |
|
||||||
`suppressConnectErrors` | *Boolean* - if set to `false`, all socket errors thrown while trying to connect will be printed to the console, regardless of if they've been thrown before. This is primarily for suppressing `SocketProtocolError` errors, which get repeatedly thrown when trying to make a connection. Default is `true`.
|
| `suppressConnectErrors` | _Boolean_ - if set to `false`, all socket errors thrown while trying to connect will be printed to the console, regardless of if they've been thrown before. This is primarily for suppressing `SocketProtocolError` errors, which get repeatedly thrown when trying to make a connection. Default is `true`. |
|
||||||
|
|
||||||
All parameters are optional. You have to provide at least `port` property to use `localhost` instead of `remotedev.io` server.
|
All parameters are optional. You have to provide at least `port` property to use `localhost` instead of `remotedev.io` server.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default function configureStore(preloadedState) {
|
export default function configureStore(preloadedState) {
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
reducer,
|
reducer,
|
||||||
preloadedState,
|
preloadedState,
|
||||||
devToolsEnhancer({
|
devToolsEnhancer({
|
||||||
name: 'Android app', realtime: true,
|
name: 'Android app',
|
||||||
hostname: 'localhost', port: 8000,
|
realtime: true,
|
||||||
maxAge: 30, actionsBlacklist: ['EFFECT_RESOLVED'],
|
hostname: 'localhost',
|
||||||
actionSanitizer: (action) => (
|
port: 8000,
|
||||||
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data ?
|
maxAge: 30,
|
||||||
{ ...action, data: '<<LONG_BLOB>>' } : action
|
actionsBlacklist: ['EFFECT_RESOLVED'],
|
||||||
),
|
actionSanitizer: (action) =>
|
||||||
stateSanitizer: (state) => state.data ? { ...state, data: '<<LONG_BLOB>>' } : state
|
action.type === 'FILE_DOWNLOAD_SUCCESS' && action.data
|
||||||
|
? { ...action, data: '<<LONG_BLOB>>' }
|
||||||
|
: action,
|
||||||
|
stateSanitizer: (state) =>
|
||||||
|
state.data ? { ...state, data: '<<LONG_BLOB>>' } : state,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return store;
|
return store;
|
||||||
|
@ -161,9 +186,11 @@ export default function configureStore(preloadedState) {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
|
|
||||||
- [Toggle monitoring](http://zalmoxisus.github.io/monitoring/)
|
- [Toggle monitoring](http://zalmoxisus.github.io/monitoring/)
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
- [Web](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-remote/examples)
|
- [Web](https://github.com/reduxjs/redux-devtools/tree/main/packages/redux-devtools-remote/examples)
|
||||||
- [React Native](https://github.com/chentsulin/react-native-counter-ios-android)
|
- [React Native](https://github.com/chentsulin/react-native-counter-ios-android)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ var exampleDirs = fs.readdirSync(__dirname).filter((file) => {
|
||||||
// Ordering is important here. `npm install` must come first.
|
// Ordering is important here. `npm install` must come first.
|
||||||
var cmdArgs = [
|
var cmdArgs = [
|
||||||
{ cmd: 'npm', args: ['install'] },
|
{ cmd: 'npm', args: ['install'] },
|
||||||
{ cmd: 'webpack', args: ['index.js'] }
|
{ cmd: 'webpack', args: ['index.js'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const dir of exampleDirs) {
|
for (const dir of exampleDirs) {
|
||||||
|
@ -21,7 +21,7 @@ for (const dir of exampleDirs) {
|
||||||
// declare opts in this scope to avoid https://github.com/joyent/node/issues/9158
|
// declare opts in this scope to avoid https://github.com/joyent/node/issues/9158
|
||||||
const opts = {
|
const opts = {
|
||||||
cwd: path.join(__dirname, dir),
|
cwd: path.join(__dirname, dir),
|
||||||
stdio: 'inherit'
|
stdio: 'inherit',
|
||||||
};
|
};
|
||||||
let result = {};
|
let result = {};
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
|
|
|
@ -3,13 +3,13 @@ export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
|
||||||
|
|
||||||
export function increment() {
|
export function increment() {
|
||||||
return {
|
return {
|
||||||
type: INCREMENT_COUNTER
|
type: INCREMENT_COUNTER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrement() {
|
export function decrement() {
|
||||||
return {
|
return {
|
||||||
type: DECREMENT_COUNTER
|
type: DECREMENT_COUNTER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export function incrementIfOdd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function incrementAsync(delay = 1000) {
|
export function incrementAsync(delay = 1000) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dispatch(increment());
|
dispatch(increment());
|
||||||
}, delay);
|
}, delay);
|
||||||
|
|
|
@ -2,17 +2,13 @@ import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props;
|
const { increment, incrementIfOdd, incrementAsync, decrement, counter } =
|
||||||
|
this.props;
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
Clicked: {counter} times
|
Clicked: {counter} times <button onClick={increment}>+</button>{' '}
|
||||||
{' '}
|
<button onClick={decrement}>-</button>{' '}
|
||||||
<button onClick={increment}>+</button>
|
<button onClick={incrementIfOdd}>Increment if odd</button>{' '}
|
||||||
{' '}
|
|
||||||
<button onClick={decrement}>-</button>
|
|
||||||
{' '}
|
|
||||||
<button onClick={incrementIfOdd}>Increment if odd</button>
|
|
||||||
{' '}
|
|
||||||
<button onClick={() => incrementAsync()}>Increment async</button>
|
<button onClick={() => incrementAsync()}>Increment async</button>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +20,7 @@ Counter.propTypes = {
|
||||||
incrementIfOdd: PropTypes.func.isRequired,
|
incrementIfOdd: PropTypes.func.isRequired,
|
||||||
incrementAsync: PropTypes.func.isRequired,
|
incrementAsync: PropTypes.func.isRequired,
|
||||||
decrement: PropTypes.func.isRequired,
|
decrement: PropTypes.func.isRequired,
|
||||||
counter: PropTypes.number.isRequired
|
counter: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Counter;
|
export default Counter;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as CounterActions from '../actions/counter';
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
counter: state.counter
|
counter: state.counter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
<title>Redux counter example</title>
|
<title>Redux counter example</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root">
|
<div id="root"></div>
|
||||||
</div>
|
|
||||||
<script src="/static/bundle.js"></script>
|
<script src="/static/bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
|
||||||
import counter from './counter';
|
import counter from './counter';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
counter
|
counter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|
|
@ -7,10 +7,15 @@ var app = new require('express')();
|
||||||
var port = 4001;
|
var port = 4001;
|
||||||
|
|
||||||
var compiler = webpack(config);
|
var compiler = webpack(config);
|
||||||
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
app.use(webpackHotMiddleware(compiler));
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
app.get("/", function(req, res) {
|
app.get('/', function (req, res) {
|
||||||
res.sendFile(__dirname + '/index.html');
|
res.sendFile(__dirname + '/index.html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,6 +23,10 @@ app.listen(port, function(error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} else {
|
} else {
|
||||||
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port);
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,11 +6,16 @@ import reducer from '../reducers';
|
||||||
import * as actionCreators from '../actions/counter';
|
import * as actionCreators from '../actions/counter';
|
||||||
|
|
||||||
export default function configureStore(initialState) {
|
export default function configureStore(initialState) {
|
||||||
|
const composeEnhancers = composeWithDevTools({
|
||||||
const composeEnhancers = composeWithDevTools({ realtime: true, actionCreators, trace: true });
|
realtime: true,
|
||||||
const store = createStore(reducer, initialState, composeEnhancers(
|
actionCreators,
|
||||||
applyMiddleware(invariant(), thunk)
|
trace: true,
|
||||||
));
|
});
|
||||||
|
const store = createStore(
|
||||||
|
reducer,
|
||||||
|
initialState,
|
||||||
|
composeEnhancers(applyMiddleware(invariant(), thunk))
|
||||||
|
);
|
||||||
|
|
||||||
if (module.hot) {
|
if (module.hot) {
|
||||||
// Enable Webpack hot module replacement for reducers
|
// Enable Webpack hot module replacement for reducers
|
||||||
|
|
|
@ -12,16 +12,17 @@ function mockStore(getState, expectedActions, onLastAction) {
|
||||||
if (!Array.isArray(expectedActions)) {
|
if (!Array.isArray(expectedActions)) {
|
||||||
throw new Error('expectedActions should be an array of expected actions.');
|
throw new Error('expectedActions should be an array of expected actions.');
|
||||||
}
|
}
|
||||||
if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') {
|
if (
|
||||||
|
typeof onLastAction !== 'undefined' &&
|
||||||
|
typeof onLastAction !== 'function'
|
||||||
|
) {
|
||||||
throw new Error('onLastAction should either be undefined or function.');
|
throw new Error('onLastAction should either be undefined or function.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockStoreWithoutMiddleware() {
|
function mockStoreWithoutMiddleware() {
|
||||||
return {
|
return {
|
||||||
getState() {
|
getState() {
|
||||||
return typeof getState === 'function' ?
|
return typeof getState === 'function' ? getState() : getState;
|
||||||
getState() :
|
|
||||||
getState;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
dispatch(action) {
|
dispatch(action) {
|
||||||
|
@ -31,13 +32,13 @@ function mockStore(getState, expectedActions, onLastAction) {
|
||||||
onLastAction();
|
onLastAction();
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockStoreWithMiddleware = applyMiddleware(
|
const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
|
||||||
...middlewares
|
mockStoreWithoutMiddleware
|
||||||
)(mockStoreWithoutMiddleware);
|
);
|
||||||
|
|
||||||
return mockStoreWithMiddleware();
|
return mockStoreWithMiddleware();
|
||||||
}
|
}
|
||||||
|
@ -52,9 +53,7 @@ describe('actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('incrementIfOdd should create increment action', (done) => {
|
it('incrementIfOdd should create increment action', (done) => {
|
||||||
const expectedActions = [
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
{ type: actions.INCREMENT_COUNTER }
|
|
||||||
];
|
|
||||||
const store = mockStore({ counter: 1 }, expectedActions, done);
|
const store = mockStore({ counter: 1 }, expectedActions, done);
|
||||||
store.dispatch(actions.incrementIfOdd());
|
store.dispatch(actions.incrementIfOdd());
|
||||||
});
|
});
|
||||||
|
@ -67,9 +66,7 @@ describe('actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('incrementAsync should create increment action', (done) => {
|
it('incrementAsync should create increment action', (done) => {
|
||||||
const expectedActions = [
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
{ type: actions.INCREMENT_COUNTER }
|
|
||||||
];
|
|
||||||
const store = mockStore({ counter: 0 }, expectedActions, done);
|
const store = mockStore({ counter: 0 }, expectedActions, done);
|
||||||
store.dispatch(actions.incrementAsync(100));
|
store.dispatch(actions.incrementAsync(100));
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,14 +8,16 @@ function setup() {
|
||||||
increment: expect.createSpy(),
|
increment: expect.createSpy(),
|
||||||
incrementIfOdd: expect.createSpy(),
|
incrementIfOdd: expect.createSpy(),
|
||||||
incrementAsync: expect.createSpy(),
|
incrementAsync: expect.createSpy(),
|
||||||
decrement: expect.createSpy()
|
decrement: expect.createSpy(),
|
||||||
};
|
};
|
||||||
const component = TestUtils.renderIntoDocument(<Counter counter={1} {...actions} />);
|
const component = TestUtils.renderIntoDocument(
|
||||||
|
<Counter counter={1} {...actions} />
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
component: component,
|
component: component,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
|
||||||
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p')
|
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ function setup(initialState) {
|
||||||
return {
|
return {
|
||||||
app: app,
|
app: app,
|
||||||
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
|
||||||
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p')
|
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,28 +3,27 @@ var webpack = require('webpack');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
entry: [
|
entry: ['webpack-hot-middleware/client', './index'],
|
||||||
'webpack-hot-middleware/client',
|
|
||||||
'./index'
|
|
||||||
],
|
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
publicPath: '/static/'
|
publicPath: '/static/',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
new webpack.NoErrorsPlugin()
|
new webpack.NoErrorsPlugin(),
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
loaders: [{
|
loaders: [
|
||||||
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
include: __dirname
|
include: __dirname,
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var src = path.join(__dirname, '..', '..', 'src');
|
var src = path.join(__dirname, '..', '..', 'src');
|
||||||
|
@ -37,6 +36,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
|
||||||
module.exports.module.loaders.push({
|
module.exports.module.loaders.push({
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
include: src
|
include: src,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
var createStore = require('redux').createStore
|
var createStore = require('redux').createStore;
|
||||||
var devTools = require('remote-redux-devtools').default
|
var devTools = require('remote-redux-devtools').default;
|
||||||
|
|
||||||
function counter(state, action) {
|
function counter(state, action) {
|
||||||
if (state === undefined) state = 0
|
if (state === undefined) state = 0;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'INCREMENT':
|
case 'INCREMENT':
|
||||||
return state + 1
|
return state + 1;
|
||||||
case 'DECREMENT':
|
case 'DECREMENT':
|
||||||
return state - 1
|
return state - 1;
|
||||||
default:
|
default:
|
||||||
return state
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = createStore(counter, devTools({realtime: true}))
|
var store = createStore(counter, devTools({ realtime: true }));
|
||||||
store.subscribe(function() { console.log(store.getState()) })
|
store.subscribe(function () {
|
||||||
|
console.log(store.getState());
|
||||||
|
});
|
||||||
|
|
||||||
function incrementer() {
|
function incrementer() {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
store.dispatch({ type: 'INCREMENT' })
|
store.dispatch({ type: 'INCREMENT' });
|
||||||
incrementer()
|
incrementer();
|
||||||
}, 1000)
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementer()
|
incrementer();
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { PropTypes, Component } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters';
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
const FILTER_TITLES = {
|
const FILTER_TITLES = {
|
||||||
[SHOW_ALL]: 'All',
|
[SHOW_ALL]: 'All',
|
||||||
[SHOW_ACTIVE]: 'Active',
|
[SHOW_ACTIVE]: 'Active',
|
||||||
[SHOW_COMPLETED]: 'Completed'
|
[SHOW_COMPLETED]: 'Completed',
|
||||||
};
|
};
|
||||||
|
|
||||||
class Footer extends Component {
|
class Footer extends Component {
|
||||||
|
@ -25,9 +29,11 @@ class Footer extends Component {
|
||||||
const { filter: selectedFilter, onShow } = this.props;
|
const { filter: selectedFilter, onShow } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className={classnames({ selected: filter === selectedFilter })}
|
<a
|
||||||
|
className={classnames({ selected: filter === selectedFilter })}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => onShow(filter)}>
|
onClick={() => onShow(filter)}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -37,8 +43,7 @@ class Footer extends Component {
|
||||||
const { completedCount, onClearCompleted } = this.props;
|
const { completedCount, onClearCompleted } = this.props;
|
||||||
if (completedCount > 0) {
|
if (completedCount > 0) {
|
||||||
return (
|
return (
|
||||||
<button className="clear-completed"
|
<button className="clear-completed" onClick={onClearCompleted}>
|
||||||
onClick={onClearCompleted} >
|
|
||||||
Clear completed
|
Clear completed
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -50,11 +55,9 @@ class Footer extends Component {
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
{this.renderTodoCount()}
|
{this.renderTodoCount()}
|
||||||
<ul className="filters">
|
<ul className="filters">
|
||||||
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter =>
|
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
|
||||||
<li key={filter}>
|
<li key={filter}>{this.renderFilterLink(filter)}</li>
|
||||||
{this.renderFilterLink(filter)}
|
))}
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
{this.renderClearButton()}
|
{this.renderClearButton()}
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -67,7 +70,7 @@ Footer.propTypes = {
|
||||||
activeCount: PropTypes.number.isRequired,
|
activeCount: PropTypes.number.isRequired,
|
||||||
filter: PropTypes.string.isRequired,
|
filter: PropTypes.string.isRequired,
|
||||||
onClearCompleted: PropTypes.func.isRequired,
|
onClearCompleted: PropTypes.func.isRequired,
|
||||||
onShow: PropTypes.func.isRequired
|
onShow: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|
|
@ -13,16 +13,18 @@ class Header extends Component {
|
||||||
return (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<h1 style={{ fontSize: 80 }}>{path}</h1>
|
<h1 style={{ fontSize: 80 }}>{path}</h1>
|
||||||
<TodoTextInput newTodo
|
<TodoTextInput
|
||||||
|
newTodo
|
||||||
onSave={this.handleSave.bind(this)}
|
onSave={this.handleSave.bind(this)}
|
||||||
placeholder="What needs to be done?" />
|
placeholder="What needs to be done?"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.propTypes = {
|
Header.propTypes = {
|
||||||
addTodo: PropTypes.func.isRequired
|
addTodo: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import TodoItem from './TodoItem';
|
import TodoItem from './TodoItem';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters';
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
const TODO_FILTERS = {
|
const TODO_FILTERS = {
|
||||||
[SHOW_ALL]: () => true,
|
[SHOW_ALL]: () => true,
|
||||||
[SHOW_ACTIVE]: todo => !todo.completed,
|
[SHOW_ACTIVE]: (todo) => !todo.completed,
|
||||||
[SHOW_COMPLETED]: todo => todo.completed
|
[SHOW_COMPLETED]: (todo) => todo.completed,
|
||||||
};
|
};
|
||||||
|
|
||||||
class MainSection extends Component {
|
class MainSection extends Component {
|
||||||
|
@ -16,7 +20,7 @@ class MainSection extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClearCompleted() {
|
handleClearCompleted() {
|
||||||
const atLeastOneCompleted = this.props.todos.some(todo => todo.completed);
|
const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
|
||||||
if (atLeastOneCompleted) {
|
if (atLeastOneCompleted) {
|
||||||
this.props.actions.clearCompleted();
|
this.props.actions.clearCompleted();
|
||||||
}
|
}
|
||||||
|
@ -30,10 +34,12 @@ class MainSection extends Component {
|
||||||
const { todos, actions } = this.props;
|
const { todos, actions } = this.props;
|
||||||
if (todos.length > 0) {
|
if (todos.length > 0) {
|
||||||
return (
|
return (
|
||||||
<input className="toggle-all"
|
<input
|
||||||
|
className="toggle-all"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={completedCount === todos.length}
|
checked={completedCount === todos.length}
|
||||||
onChange={actions.completeAll} />
|
onChange={actions.completeAll}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,11 +51,13 @@ class MainSection extends Component {
|
||||||
|
|
||||||
if (todos.length) {
|
if (todos.length) {
|
||||||
return (
|
return (
|
||||||
<Footer completedCount={completedCount}
|
<Footer
|
||||||
|
completedCount={completedCount}
|
||||||
activeCount={activeCount}
|
activeCount={activeCount}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
onClearCompleted={this.handleClearCompleted.bind(this)}
|
onClearCompleted={this.handleClearCompleted.bind(this)}
|
||||||
onShow={this.handleShow.bind(this)} />
|
onShow={this.handleShow.bind(this)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,8 +67,8 @@ class MainSection extends Component {
|
||||||
const { filter } = this.state;
|
const { filter } = this.state;
|
||||||
|
|
||||||
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
|
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
|
||||||
const completedCount = todos.reduce((count, todo) =>
|
const completedCount = todos.reduce(
|
||||||
todo.completed ? count + 1 : count,
|
(count, todo) => (todo.completed ? count + 1 : count),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -68,9 +76,9 @@ class MainSection extends Component {
|
||||||
<section className="main">
|
<section className="main">
|
||||||
{this.renderToggleAll(completedCount)}
|
{this.renderToggleAll(completedCount)}
|
||||||
<ul className="todo-list">
|
<ul className="todo-list">
|
||||||
{filteredTodos.map(todo =>
|
{filteredTodos.map((todo) => (
|
||||||
<TodoItem key={todo.id} todo={todo} {...actions} />
|
<TodoItem key={todo.id} todo={todo} {...actions} />
|
||||||
)}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{this.renderFooter(completedCount)}
|
{this.renderFooter(completedCount)}
|
||||||
</section>
|
</section>
|
||||||
|
@ -80,7 +88,7 @@ class MainSection extends Component {
|
||||||
|
|
||||||
MainSection.propTypes = {
|
MainSection.propTypes = {
|
||||||
todos: PropTypes.array.isRequired,
|
todos: PropTypes.array.isRequired,
|
||||||
actions: PropTypes.object.isRequired
|
actions: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MainSection;
|
export default MainSection;
|
||||||
|
|
|
@ -6,7 +6,7 @@ class TodoItem extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
editing: false
|
editing: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,31 +29,36 @@ class TodoItem extends Component {
|
||||||
let element;
|
let element;
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
element = (
|
element = (
|
||||||
<TodoTextInput text={todo.text}
|
<TodoTextInput
|
||||||
|
text={todo.text}
|
||||||
editing={this.state.editing}
|
editing={this.state.editing}
|
||||||
onSave={(text) => this.handleSave(todo.id, text)} />
|
onSave={(text) => this.handleSave(todo.id, text)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
element = (
|
element = (
|
||||||
<div className="view">
|
<div className="view">
|
||||||
<input className="toggle"
|
<input
|
||||||
|
className="toggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={todo.completed}
|
checked={todo.completed}
|
||||||
onChange={() => completeTodo(todo.id)} />
|
onChange={() => completeTodo(todo.id)}
|
||||||
|
/>
|
||||||
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
|
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
|
||||||
{todo.text}
|
{todo.text}
|
||||||
</label>
|
</label>
|
||||||
<button className="destroy"
|
<button className="destroy" onClick={() => deleteTodo(todo.id)} />
|
||||||
onClick={() => deleteTodo(todo.id)} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={classnames({
|
<li
|
||||||
|
className={classnames({
|
||||||
completed: todo.completed,
|
completed: todo.completed,
|
||||||
editing: this.state.editing
|
editing: this.state.editing,
|
||||||
})}>
|
})}
|
||||||
|
>
|
||||||
{element}
|
{element}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -64,7 +69,7 @@ TodoItem.propTypes = {
|
||||||
todo: PropTypes.object.isRequired,
|
todo: PropTypes.object.isRequired,
|
||||||
editTodo: PropTypes.func.isRequired,
|
editTodo: PropTypes.func.isRequired,
|
||||||
deleteTodo: PropTypes.func.isRequired,
|
deleteTodo: PropTypes.func.isRequired,
|
||||||
completeTodo: PropTypes.func.isRequired
|
completeTodo: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TodoItem;
|
export default TodoItem;
|
||||||
|
|
|
@ -5,7 +5,7 @@ class TodoTextInput extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
text: this.props.text || ''
|
text: this.props.text || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +31,10 @@ class TodoTextInput extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<input className={
|
<input
|
||||||
classnames({
|
className={classnames({
|
||||||
edit: this.props.editing,
|
edit: this.props.editing,
|
||||||
'new-todo': this.props.newTodo
|
'new-todo': this.props.newTodo,
|
||||||
})}
|
})}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
|
@ -42,7 +42,8 @@ class TodoTextInput extends Component {
|
||||||
value={this.state.text}
|
value={this.state.text}
|
||||||
onBlur={this.handleBlur.bind(this)}
|
onBlur={this.handleBlur.bind(this)}
|
||||||
onChange={this.handleChange.bind(this)}
|
onChange={this.handleChange.bind(this)}
|
||||||
onKeyDown={this.handleSubmit.bind(this)} />
|
onKeyDown={this.handleSubmit.bind(this)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +53,7 @@ TodoTextInput.propTypes = {
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
editing: PropTypes.bool,
|
editing: PropTypes.bool,
|
||||||
newTodo: PropTypes.bool
|
newTodo: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TodoTextInput;
|
export default TodoTextInput;
|
||||||
|
|
|
@ -19,23 +19,20 @@ class App extends Component {
|
||||||
|
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
todos: PropTypes.array.isRequired,
|
todos: PropTypes.array.isRequired,
|
||||||
actions: PropTypes.object.isRequired
|
actions: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
todos: state.todos,
|
todos: state.todos,
|
||||||
path: state.router.location.pathname
|
path: state.router.location.pathname,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators(TodoActions, dispatch)
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(App);
|
|
||||||
|
|
|
@ -8,14 +8,14 @@ import * as TodoActions from '../actions/todos';
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
pushState: bindActionCreators(pushState, dispatch),
|
pushState: bindActionCreators(pushState, dispatch),
|
||||||
actions: bindActionCreators(TodoActions, dispatch)
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@connect((state) => ({}), mapDispatchToProps)
|
@connect((state) => ({}), mapDispatchToProps)
|
||||||
class Wrapper extends Component {
|
class Wrapper extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -41,11 +41,23 @@ class Wrapper extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ padding: 20, backgroundColor: '#eee', fontWeight: 'bold', textAlign: 'center' }}>
|
<div
|
||||||
<a href="#" onClick={this.handleClick}>Standard Todo</a> | <a href="#" onClick={this.handleClick}>AutoTodo</a>
|
style={{
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#eee',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a href="#" onClick={this.handleClick}>
|
||||||
|
Standard Todo
|
||||||
|
</a>{' '}
|
||||||
|
|{' '}
|
||||||
|
<a href="#" onClick={this.handleClick}>
|
||||||
|
AutoTodo
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
<title>Redux TodoMVC example</title>
|
<title>Redux TodoMVC example</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="todoapp" id="root">
|
<div class="todoapp" id="root"></div>
|
||||||
</div>
|
|
||||||
<script src="/static/bundle.js"></script>
|
<script src="/static/bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import todos from './todos';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
todos,
|
todos,
|
||||||
router: routerStateReducer
|
router: routerStateReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|
|
@ -1,47 +1,59 @@
|
||||||
import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes';
|
import {
|
||||||
|
ADD_TODO,
|
||||||
|
DELETE_TODO,
|
||||||
|
EDIT_TODO,
|
||||||
|
COMPLETE_TODO,
|
||||||
|
COMPLETE_ALL,
|
||||||
|
CLEAR_COMPLETED,
|
||||||
|
} from '../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = [{
|
const initialState = [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function todos(state = initialState, action) {
|
export default function todos(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ADD_TODO:
|
case ADD_TODO:
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
||||||
completed: false,
|
completed: false,
|
||||||
text: action.text
|
text: action.text,
|
||||||
}, ...state];
|
},
|
||||||
|
...state,
|
||||||
|
];
|
||||||
|
|
||||||
case DELETE_TODO:
|
case DELETE_TODO:
|
||||||
return state.filter(todo =>
|
return state.filter((todo) => todo.id !== action.id);
|
||||||
todo.id !== action.id
|
|
||||||
);
|
|
||||||
|
|
||||||
case EDIT_TODO:
|
case EDIT_TODO:
|
||||||
return state.map(todo =>
|
return state.map((todo) =>
|
||||||
todo.id === action.id ?
|
todo.id === action.id
|
||||||
Object.assign({}, todo, { text: action.text }) :
|
? Object.assign({}, todo, { text: action.text })
|
||||||
todo
|
: todo
|
||||||
);
|
);
|
||||||
|
|
||||||
case COMPLETE_TODO:
|
case COMPLETE_TODO:
|
||||||
return state.map(todo =>
|
return state.map((todo) =>
|
||||||
todo.id === action.id ?
|
todo.id === action.id
|
||||||
Object.assign({}, todo, { completed: !todo.completed }) :
|
? Object.assign({}, todo, { completed: !todo.completed })
|
||||||
todo
|
: todo
|
||||||
);
|
);
|
||||||
|
|
||||||
case COMPLETE_ALL:
|
case COMPLETE_ALL:
|
||||||
const areAllMarked = state.every(todo => todo.completed);
|
const areAllMarked = state.every((todo) => todo.completed);
|
||||||
return state.map(todo => Object.assign({}, todo, {
|
return state.map((todo) =>
|
||||||
completed: !areAllMarked
|
Object.assign({}, todo, {
|
||||||
}));
|
completed: !areAllMarked,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
case CLEAR_COMPLETED:
|
case CLEAR_COMPLETED:
|
||||||
return state.filter(todo => todo.completed === false);
|
return state.filter((todo) => todo.completed === false);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -7,10 +7,15 @@ var app = new require('express')();
|
||||||
var port = 4002;
|
var port = 4002;
|
||||||
|
|
||||||
var compiler = webpack(config);
|
var compiler = webpack(config);
|
||||||
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
app.use(webpackHotMiddleware(compiler));
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
app.get("/", function(req, res) {
|
app.get('/', function (req, res) {
|
||||||
res.sendFile(__dirname + '/index.html');
|
res.sendFile(__dirname + '/index.html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,6 +23,10 @@ app.listen(port, function(error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} else {
|
} else {
|
||||||
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port);
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { createStore, compose } from 'redux';
|
import { createStore, compose } from 'redux';
|
||||||
import { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router';
|
import {
|
||||||
|
reduxReactRouter,
|
||||||
|
routerStateReducer,
|
||||||
|
ReduxRouter,
|
||||||
|
} from 'redux-router';
|
||||||
//import createHistory from 'history/lib/createBrowserHistory';
|
//import createHistory from 'history/lib/createBrowserHistory';
|
||||||
import devTools from 'remote-redux-devtools';
|
import devTools from 'remote-redux-devtools';
|
||||||
import createHistory from 'history/lib/createHashHistory';
|
import createHistory from 'history/lib/createHashHistory';
|
||||||
|
|
|
@ -6,14 +6,14 @@ describe('todo actions', () => {
|
||||||
it('addTodo should create ADD_TODO action', () => {
|
it('addTodo should create ADD_TODO action', () => {
|
||||||
expect(actions.addTodo('Use Redux')).toEqual({
|
expect(actions.addTodo('Use Redux')).toEqual({
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Use Redux'
|
text: 'Use Redux',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteTodo should create DELETE_TODO action', () => {
|
it('deleteTodo should create DELETE_TODO action', () => {
|
||||||
expect(actions.deleteTodo(1)).toEqual({
|
expect(actions.deleteTodo(1)).toEqual({
|
||||||
type: types.DELETE_TODO,
|
type: types.DELETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,26 +21,26 @@ describe('todo actions', () => {
|
||||||
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
||||||
type: types.EDIT_TODO,
|
type: types.EDIT_TODO,
|
||||||
id: 1,
|
id: 1,
|
||||||
text: 'Use Redux everywhere'
|
text: 'Use Redux everywhere',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('completeTodo should create COMPLETE_TODO action', () => {
|
it('completeTodo should create COMPLETE_TODO action', () => {
|
||||||
expect(actions.completeTodo(1)).toEqual({
|
expect(actions.completeTodo(1)).toEqual({
|
||||||
type: types.COMPLETE_TODO,
|
type: types.COMPLETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('completeAll should create COMPLETE_ALL action', () => {
|
it('completeAll should create COMPLETE_ALL action', () => {
|
||||||
expect(actions.completeAll()).toEqual({
|
expect(actions.completeAll()).toEqual({
|
||||||
type: types.COMPLETE_ALL
|
type: types.COMPLETE_ALL,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
||||||
expect(actions.clearCompleted('Use Redux')).toEqual({
|
expect(actions.clearCompleted('Use Redux')).toEqual({
|
||||||
type: types.CLEAR_COMPLETED
|
type: types.CLEAR_COMPLETED,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,13 +5,16 @@ import Footer from '../../components/Footer';
|
||||||
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
function setup(propOverrides) {
|
function setup(propOverrides) {
|
||||||
const props = Object.assign({
|
const props = Object.assign(
|
||||||
|
{
|
||||||
completedCount: 0,
|
completedCount: 0,
|
||||||
activeCount: 0,
|
activeCount: 0,
|
||||||
filter: SHOW_ALL,
|
filter: SHOW_ALL,
|
||||||
onClearCompleted: expect.createSpy(),
|
onClearCompleted: expect.createSpy(),
|
||||||
onShow: expect.createSpy()
|
onShow: expect.createSpy(),
|
||||||
}, propOverrides);
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
renderer.render(<Footer {...props} />);
|
renderer.render(<Footer {...props} />);
|
||||||
|
@ -19,13 +22,14 @@ function setup(propOverrides) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output
|
output: output,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextContent(elem) {
|
function getTextContent(elem) {
|
||||||
const children = Array.isArray(elem.props.children) ?
|
const children = Array.isArray(elem.props.children)
|
||||||
elem.props.children : [elem.props.children];
|
? elem.props.children
|
||||||
|
: [elem.props.children];
|
||||||
|
|
||||||
return children.reduce(function concatText(out, child) {
|
return children.reduce(function concatText(out, child) {
|
||||||
// Children are either elements or text strings
|
// Children are either elements or text strings
|
||||||
|
@ -63,11 +67,13 @@ describe('components', () => {
|
||||||
expect(filter.type).toBe('li');
|
expect(filter.type).toBe('li');
|
||||||
const a = filter.props.children;
|
const a = filter.props.children;
|
||||||
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
||||||
expect(a.props.children).toBe({
|
expect(a.props.children).toBe(
|
||||||
|
{
|
||||||
0: 'All',
|
0: 'All',
|
||||||
1: 'Active',
|
1: 'Active',
|
||||||
2: 'Completed'
|
2: 'Completed',
|
||||||
}[i]);
|
}[i]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
const props = {
|
const props = {
|
||||||
addTodo: expect.createSpy()
|
addTodo: expect.createSpy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
|
@ -16,7 +16,7 @@ function setup() {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,24 +7,30 @@ import Footer from '../../components/Footer';
|
||||||
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
function setup(propOverrides) {
|
function setup(propOverrides) {
|
||||||
const props = Object.assign({
|
const props = Object.assign(
|
||||||
todos: [{
|
{
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
actions: {
|
actions: {
|
||||||
editTodo: expect.createSpy(),
|
editTodo: expect.createSpy(),
|
||||||
deleteTodo: expect.createSpy(),
|
deleteTodo: expect.createSpy(),
|
||||||
completeTodo: expect.createSpy(),
|
completeTodo: expect.createSpy(),
|
||||||
completeAll: expect.createSpy(),
|
completeAll: expect.createSpy(),
|
||||||
clearCompleted: expect.createSpy()
|
clearCompleted: expect.createSpy(),
|
||||||
}
|
},
|
||||||
}, propOverrides);
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
renderer.render(<MainSection {...props} />);
|
renderer.render(<MainSection {...props} />);
|
||||||
|
@ -33,7 +39,7 @@ function setup(propOverrides) {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,11 +61,15 @@ describe('components', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be checked if all todos completed', () => {
|
it('should be checked if all todos completed', () => {
|
||||||
const { output } = setup({ todos: [{
|
const { output } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 0
|
id: 0,
|
||||||
}]});
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
const [toggle] = output.props.children;
|
const [toggle] = output.props.children;
|
||||||
expect(toggle.props.checked).toBe(true);
|
expect(toggle.props.checked).toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -99,11 +109,15 @@ describe('components', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
||||||
const { output, props } = setup({ todos: [{
|
const { output, props } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]});
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
const [, , footer] = output.props.children;
|
const [, , footer] = output.props.children;
|
||||||
footer.props.onClearCompleted();
|
footer.props.onClearCompleted();
|
||||||
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
||||||
|
|
|
@ -9,18 +9,16 @@ function setup( editing = false ) {
|
||||||
todo: {
|
todo: {
|
||||||
id: 0,
|
id: 0,
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false
|
completed: false,
|
||||||
},
|
},
|
||||||
editTodo: expect.createSpy(),
|
editTodo: expect.createSpy(),
|
||||||
deleteTodo: expect.createSpy(),
|
deleteTodo: expect.createSpy(),
|
||||||
completeTodo: expect.createSpy()
|
completeTodo: expect.createSpy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
renderer.render(
|
renderer.render(<TodoItem {...props} />);
|
||||||
<TodoItem {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = renderer.getRenderOutput();
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ function setup( editing = false ) {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,19 +4,20 @@ import TestUtils from 'react-addons-test-utils';
|
||||||
import TodoTextInput from '../../components/TodoTextInput';
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
function setup(propOverrides) {
|
function setup(propOverrides) {
|
||||||
const props = Object.assign({
|
const props = Object.assign(
|
||||||
|
{
|
||||||
onSave: expect.createSpy(),
|
onSave: expect.createSpy(),
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
placeholder: 'What needs to be done?',
|
placeholder: 'What needs to be done?',
|
||||||
editing: false,
|
editing: false,
|
||||||
newTodo: false
|
newTodo: false,
|
||||||
}, propOverrides);
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
renderer.render(
|
renderer.render(<TodoTextInput {...props} />);
|
||||||
<TodoTextInput {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = renderer.getRenderOutput();
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ function setup(propOverrides) {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
|
||||||
|
|
||||||
describe('todos reducer', () => {
|
describe('todos reducer', () => {
|
||||||
it('should handle initial state', () => {
|
it('should handle initial state', () => {
|
||||||
expect(
|
expect(todos(undefined, {})).toEqual([
|
||||||
todos(undefined, {})
|
{
|
||||||
).toEqual([{
|
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle ADD_TODO', () => {
|
it('should handle ADD_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([], {
|
todos([], {
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Run the tests'
|
text: 'Run the tests',
|
||||||
})
|
})
|
||||||
).toEqual([{
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Run the tests'
|
text: 'Run the tests',
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Fix the tests'
|
text: 'Fix the tests',
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Fix the tests',
|
text: 'Fix the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 2
|
id: 2,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle DELETE_TODO', () => {
|
it('should handle DELETE_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.DELETE_TODO,
|
type: types.DELETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle EDIT_TODO', () => {
|
it('should handle EDIT_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.EDIT_TODO,
|
type: types.EDIT_TODO,
|
||||||
text: 'Fix the tests',
|
text: 'Fix the tests',
|
||||||
id: 1
|
id: 1,
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Fix the tests',
|
text: 'Fix the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPLETE_TODO', () => {
|
it('should handle COMPLETE_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.COMPLETE_TODO,
|
type: types.COMPLETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPLETE_ALL', () => {
|
it('should handle COMPLETE_ALL', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
type: types.COMPLETE_ALL
|
],
|
||||||
})
|
{
|
||||||
).toEqual([{
|
type: types.COMPLETE_ALL,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
// Unmark if all todos are currently completed
|
// Unmark if all todos are currently completed
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
type: types.COMPLETE_ALL
|
],
|
||||||
})
|
{
|
||||||
).toEqual([{
|
type: types.COMPLETE_ALL,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle CLEAR_COMPLETED', () => {
|
it('should handle CLEAR_COMPLETED', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
type: types.CLEAR_COMPLETED
|
],
|
||||||
})
|
{
|
||||||
).toEqual([{
|
type: types.CLEAR_COMPLETED,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
||||||
expect(
|
expect(
|
||||||
[{
|
[
|
||||||
|
{
|
||||||
type: types.COMPLETE_TODO,
|
type: types.COMPLETE_TODO,
|
||||||
id: 0
|
id: 0,
|
||||||
}, {
|
},
|
||||||
type: types.CLEAR_COMPLETED
|
{
|
||||||
}, {
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Write more tests'
|
text: 'Write more tests',
|
||||||
}].reduce(todos, [{
|
},
|
||||||
|
].reduce(todos, [
|
||||||
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
completed: false,
|
completed: false,
|
||||||
text: 'Use Redux'
|
text: 'Use Redux',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
completed: false,
|
completed: false,
|
||||||
text: 'Write tests'
|
text: 'Write tests',
|
||||||
}])
|
},
|
||||||
).toEqual([{
|
])
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Write more tests',
|
text: 'Write more tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 2
|
id: 2,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Write tests',
|
text: 'Write tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,32 +3,32 @@ var webpack = require('webpack');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devtool: 'cheap-module-eval-source-map',
|
devtool: 'cheap-module-eval-source-map',
|
||||||
entry: [
|
entry: ['webpack-hot-middleware/client', './index'],
|
||||||
'webpack-hot-middleware/client',
|
|
||||||
'./index'
|
|
||||||
],
|
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
publicPath: '/static/'
|
publicPath: '/static/',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
new webpack.NoErrorsPlugin()
|
new webpack.NoErrorsPlugin(),
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
loaders: [{
|
loaders: [
|
||||||
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
include: __dirname
|
include: __dirname,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
test: /\.css?$/,
|
test: /\.css?$/,
|
||||||
loaders: ['style', 'raw'],
|
loaders: ['style', 'raw'],
|
||||||
include: __dirname
|
include: __dirname,
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var src = path.join(__dirname, '..', '..', 'src');
|
var src = path.join(__dirname, '..', '..', 'src');
|
||||||
|
@ -41,6 +41,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
|
||||||
module.exports.module.loaders.push({
|
module.exports.module.loaders.push({
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
include: src
|
include: src,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { PropTypes, Component } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters';
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
const FILTER_TITLES = {
|
const FILTER_TITLES = {
|
||||||
[SHOW_ALL]: 'All',
|
[SHOW_ALL]: 'All',
|
||||||
[SHOW_ACTIVE]: 'Active',
|
[SHOW_ACTIVE]: 'Active',
|
||||||
[SHOW_COMPLETED]: 'Completed'
|
[SHOW_COMPLETED]: 'Completed',
|
||||||
};
|
};
|
||||||
|
|
||||||
class Footer extends Component {
|
class Footer extends Component {
|
||||||
|
@ -25,9 +29,11 @@ class Footer extends Component {
|
||||||
const { filter: selectedFilter, onShow } = this.props;
|
const { filter: selectedFilter, onShow } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className={classnames({ selected: filter === selectedFilter })}
|
<a
|
||||||
|
className={classnames({ selected: filter === selectedFilter })}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => onShow(filter)}>
|
onClick={() => onShow(filter)}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -37,8 +43,7 @@ class Footer extends Component {
|
||||||
const { completedCount, onClearCompleted } = this.props;
|
const { completedCount, onClearCompleted } = this.props;
|
||||||
if (completedCount > 0) {
|
if (completedCount > 0) {
|
||||||
return (
|
return (
|
||||||
<button className="clear-completed"
|
<button className="clear-completed" onClick={onClearCompleted}>
|
||||||
onClick={onClearCompleted} >
|
|
||||||
Clear completed
|
Clear completed
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -50,11 +55,9 @@ class Footer extends Component {
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
{this.renderTodoCount()}
|
{this.renderTodoCount()}
|
||||||
<ul className="filters">
|
<ul className="filters">
|
||||||
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter =>
|
{[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map((filter) => (
|
||||||
<li key={filter}>
|
<li key={filter}>{this.renderFilterLink(filter)}</li>
|
||||||
{this.renderFilterLink(filter)}
|
))}
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
{this.renderClearButton()}
|
{this.renderClearButton()}
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -67,7 +70,7 @@ Footer.propTypes = {
|
||||||
activeCount: PropTypes.number.isRequired,
|
activeCount: PropTypes.number.isRequired,
|
||||||
filter: PropTypes.string.isRequired,
|
filter: PropTypes.string.isRequired,
|
||||||
onClearCompleted: PropTypes.func.isRequired,
|
onClearCompleted: PropTypes.func.isRequired,
|
||||||
onShow: PropTypes.func.isRequired
|
onShow: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|
|
@ -12,16 +12,18 @@ class Header extends Component {
|
||||||
return (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<h1>todos</h1>
|
<h1>todos</h1>
|
||||||
<TodoTextInput newTodo
|
<TodoTextInput
|
||||||
|
newTodo
|
||||||
onSave={this.handleSave.bind(this)}
|
onSave={this.handleSave.bind(this)}
|
||||||
placeholder="What needs to be done?" />
|
placeholder="What needs to be done?"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Header.propTypes = {
|
Header.propTypes = {
|
||||||
addTodo: PropTypes.func.isRequired
|
addTodo: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import TodoItem from './TodoItem';
|
import TodoItem from './TodoItem';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters';
|
import {
|
||||||
|
SHOW_ALL,
|
||||||
|
SHOW_COMPLETED,
|
||||||
|
SHOW_ACTIVE,
|
||||||
|
} from '../constants/TodoFilters';
|
||||||
|
|
||||||
const TODO_FILTERS = {
|
const TODO_FILTERS = {
|
||||||
[SHOW_ALL]: () => true,
|
[SHOW_ALL]: () => true,
|
||||||
[SHOW_ACTIVE]: todo => !todo.completed,
|
[SHOW_ACTIVE]: (todo) => !todo.completed,
|
||||||
[SHOW_COMPLETED]: todo => todo.completed
|
[SHOW_COMPLETED]: (todo) => todo.completed,
|
||||||
};
|
};
|
||||||
|
|
||||||
class MainSection extends Component {
|
class MainSection extends Component {
|
||||||
|
@ -16,7 +20,7 @@ class MainSection extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClearCompleted() {
|
handleClearCompleted() {
|
||||||
const atLeastOneCompleted = this.props.todos.some(todo => todo.completed);
|
const atLeastOneCompleted = this.props.todos.some((todo) => todo.completed);
|
||||||
if (atLeastOneCompleted) {
|
if (atLeastOneCompleted) {
|
||||||
this.props.actions.clearCompleted();
|
this.props.actions.clearCompleted();
|
||||||
}
|
}
|
||||||
|
@ -30,10 +34,12 @@ class MainSection extends Component {
|
||||||
const { todos, actions } = this.props;
|
const { todos, actions } = this.props;
|
||||||
if (todos.length > 0) {
|
if (todos.length > 0) {
|
||||||
return (
|
return (
|
||||||
<input className="toggle-all"
|
<input
|
||||||
|
className="toggle-all"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={completedCount === todos.length}
|
checked={completedCount === todos.length}
|
||||||
onChange={actions.completeAll} />
|
onChange={actions.completeAll}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,11 +51,13 @@ class MainSection extends Component {
|
||||||
|
|
||||||
if (todos.length) {
|
if (todos.length) {
|
||||||
return (
|
return (
|
||||||
<Footer completedCount={completedCount}
|
<Footer
|
||||||
|
completedCount={completedCount}
|
||||||
activeCount={activeCount}
|
activeCount={activeCount}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
onClearCompleted={this.handleClearCompleted.bind(this)}
|
onClearCompleted={this.handleClearCompleted.bind(this)}
|
||||||
onShow={this.handleShow.bind(this)} />
|
onShow={this.handleShow.bind(this)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,8 +67,8 @@ class MainSection extends Component {
|
||||||
const { filter } = this.state;
|
const { filter } = this.state;
|
||||||
|
|
||||||
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
|
const filteredTodos = todos.filter(TODO_FILTERS[filter]);
|
||||||
const completedCount = todos.reduce((count, todo) =>
|
const completedCount = todos.reduce(
|
||||||
todo.completed ? count + 1 : count,
|
(count, todo) => (todo.completed ? count + 1 : count),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -68,9 +76,9 @@ class MainSection extends Component {
|
||||||
<section className="main">
|
<section className="main">
|
||||||
{this.renderToggleAll(completedCount)}
|
{this.renderToggleAll(completedCount)}
|
||||||
<ul className="todo-list">
|
<ul className="todo-list">
|
||||||
{filteredTodos.map(todo =>
|
{filteredTodos.map((todo) => (
|
||||||
<TodoItem key={todo.id} todo={todo} {...actions} />
|
<TodoItem key={todo.id} todo={todo} {...actions} />
|
||||||
)}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{this.renderFooter(completedCount)}
|
{this.renderFooter(completedCount)}
|
||||||
</section>
|
</section>
|
||||||
|
@ -80,7 +88,7 @@ class MainSection extends Component {
|
||||||
|
|
||||||
MainSection.propTypes = {
|
MainSection.propTypes = {
|
||||||
todos: PropTypes.array.isRequired,
|
todos: PropTypes.array.isRequired,
|
||||||
actions: PropTypes.object.isRequired
|
actions: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MainSection;
|
export default MainSection;
|
||||||
|
|
|
@ -6,7 +6,7 @@ class TodoItem extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
editing: false
|
editing: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,31 +29,36 @@ class TodoItem extends Component {
|
||||||
let element;
|
let element;
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
element = (
|
element = (
|
||||||
<TodoTextInput text={todo.text}
|
<TodoTextInput
|
||||||
|
text={todo.text}
|
||||||
editing={this.state.editing}
|
editing={this.state.editing}
|
||||||
onSave={(text) => this.handleSave(todo.id, text)} />
|
onSave={(text) => this.handleSave(todo.id, text)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
element = (
|
element = (
|
||||||
<div className="view">
|
<div className="view">
|
||||||
<input className="toggle"
|
<input
|
||||||
|
className="toggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={todo.completed}
|
checked={todo.completed}
|
||||||
onChange={() => completeTodo(todo.id)} />
|
onChange={() => completeTodo(todo.id)}
|
||||||
|
/>
|
||||||
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
|
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
|
||||||
{todo.text}
|
{todo.text}
|
||||||
</label>
|
</label>
|
||||||
<button className="destroy"
|
<button className="destroy" onClick={() => deleteTodo(todo.id)} />
|
||||||
onClick={() => deleteTodo(todo.id)} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={classnames({
|
<li
|
||||||
|
className={classnames({
|
||||||
completed: todo.completed,
|
completed: todo.completed,
|
||||||
editing: this.state.editing
|
editing: this.state.editing,
|
||||||
})}>
|
})}
|
||||||
|
>
|
||||||
{element}
|
{element}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -64,7 +69,7 @@ TodoItem.propTypes = {
|
||||||
todo: PropTypes.object.isRequired,
|
todo: PropTypes.object.isRequired,
|
||||||
editTodo: PropTypes.func.isRequired,
|
editTodo: PropTypes.func.isRequired,
|
||||||
deleteTodo: PropTypes.func.isRequired,
|
deleteTodo: PropTypes.func.isRequired,
|
||||||
completeTodo: PropTypes.func.isRequired
|
completeTodo: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TodoItem;
|
export default TodoItem;
|
||||||
|
|
|
@ -5,7 +5,7 @@ class TodoTextInput extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
text: this.props.text || ''
|
text: this.props.text || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +31,10 @@ class TodoTextInput extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<input className={
|
<input
|
||||||
classnames({
|
className={classnames({
|
||||||
edit: this.props.editing,
|
edit: this.props.editing,
|
||||||
'new-todo': this.props.newTodo
|
'new-todo': this.props.newTodo,
|
||||||
})}
|
})}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
|
@ -42,7 +42,8 @@ class TodoTextInput extends Component {
|
||||||
value={this.state.text}
|
value={this.state.text}
|
||||||
onBlur={this.handleBlur.bind(this)}
|
onBlur={this.handleBlur.bind(this)}
|
||||||
onChange={this.handleChange.bind(this)}
|
onChange={this.handleChange.bind(this)}
|
||||||
onKeyDown={this.handleSubmit.bind(this)} />
|
onKeyDown={this.handleSubmit.bind(this)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +53,7 @@ TodoTextInput.propTypes = {
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
editing: PropTypes.bool,
|
editing: PropTypes.bool,
|
||||||
newTodo: PropTypes.bool
|
newTodo: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TodoTextInput;
|
export default TodoTextInput;
|
||||||
|
|
|
@ -19,22 +19,19 @@ class App extends Component {
|
||||||
|
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
todos: PropTypes.array.isRequired,
|
todos: PropTypes.array.isRequired,
|
||||||
actions: PropTypes.object.isRequired
|
actions: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
todos: state.todos
|
todos: state.todos,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators(TodoActions, dispatch)
|
actions: bindActionCreators(TodoActions, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(App);
|
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
<title>Redux TodoMVC example</title>
|
<title>Redux TodoMVC example</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="todoapp" id="root">
|
<div class="todoapp" id="root"></div>
|
||||||
</div>
|
|
||||||
<script src="/static/bundle.js"></script>
|
<script src="/static/bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
|
||||||
import todos from './todos';
|
import todos from './todos';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
todos
|
todos,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|
|
@ -1,47 +1,59 @@
|
||||||
import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes';
|
import {
|
||||||
|
ADD_TODO,
|
||||||
|
DELETE_TODO,
|
||||||
|
EDIT_TODO,
|
||||||
|
COMPLETE_TODO,
|
||||||
|
COMPLETE_ALL,
|
||||||
|
CLEAR_COMPLETED,
|
||||||
|
} from '../constants/ActionTypes';
|
||||||
|
|
||||||
const initialState = [{
|
const initialState = [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function todos(state = initialState, action) {
|
export default function todos(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ADD_TODO:
|
case ADD_TODO:
|
||||||
return [{
|
return [
|
||||||
|
{
|
||||||
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
||||||
completed: false,
|
completed: false,
|
||||||
text: action.text
|
text: action.text,
|
||||||
}, ...state];
|
},
|
||||||
|
...state,
|
||||||
|
];
|
||||||
|
|
||||||
case DELETE_TODO:
|
case DELETE_TODO:
|
||||||
return state.filter(todo =>
|
return state.filter((todo) => todo.id !== action.id);
|
||||||
todo.id !== action.id
|
|
||||||
);
|
|
||||||
|
|
||||||
case EDIT_TODO:
|
case EDIT_TODO:
|
||||||
return state.map(todo =>
|
return state.map((todo) =>
|
||||||
todo.id === action.id ?
|
todo.id === action.id
|
||||||
Object.assign({}, todo, { text: action.text }) :
|
? Object.assign({}, todo, { text: action.text })
|
||||||
todo
|
: todo
|
||||||
);
|
);
|
||||||
|
|
||||||
case COMPLETE_TODO:
|
case COMPLETE_TODO:
|
||||||
return state.map(todo =>
|
return state.map((todo) =>
|
||||||
todo.id === action.id ?
|
todo.id === action.id
|
||||||
Object.assign({}, todo, { completed: !todo.completed }) :
|
? Object.assign({}, todo, { completed: !todo.completed })
|
||||||
todo
|
: todo
|
||||||
);
|
);
|
||||||
|
|
||||||
case COMPLETE_ALL:
|
case COMPLETE_ALL:
|
||||||
const areAllMarked = state.every(todo => todo.completed);
|
const areAllMarked = state.every((todo) => todo.completed);
|
||||||
return state.map(todo => Object.assign({}, todo, {
|
return state.map((todo) =>
|
||||||
completed: !areAllMarked
|
Object.assign({}, todo, {
|
||||||
}));
|
completed: !areAllMarked,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
case CLEAR_COMPLETED:
|
case CLEAR_COMPLETED:
|
||||||
return state.filter(todo => todo.completed === false);
|
return state.filter((todo) => todo.completed === false);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -7,10 +7,15 @@ var app = new require('express')();
|
||||||
var port = 4002;
|
var port = 4002;
|
||||||
|
|
||||||
var compiler = webpack(config);
|
var compiler = webpack(config);
|
||||||
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
app.use(webpackHotMiddleware(compiler));
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
app.get("/", function(req, res) {
|
app.get('/', function (req, res) {
|
||||||
res.sendFile(__dirname + '/index.html');
|
res.sendFile(__dirname + '/index.html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,6 +23,10 @@ app.listen(port, function(error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} else {
|
} else {
|
||||||
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port);
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,9 +4,14 @@ import rootReducer from '../reducers';
|
||||||
import * as actionCreators from '../actions/todos';
|
import * as actionCreators from '../actions/todos';
|
||||||
|
|
||||||
export default function configureStore(initialState) {
|
export default function configureStore(initialState) {
|
||||||
const store = createStore(rootReducer, initialState, devTools({
|
const store = createStore(
|
||||||
realtime: true, actionCreators
|
rootReducer,
|
||||||
}));
|
initialState,
|
||||||
|
devTools({
|
||||||
|
realtime: true,
|
||||||
|
actionCreators,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (module.hot) {
|
if (module.hot) {
|
||||||
// Enable Webpack hot module replacement for reducers
|
// Enable Webpack hot module replacement for reducers
|
||||||
|
|
|
@ -6,14 +6,14 @@ describe('todo actions', () => {
|
||||||
it('addTodo should create ADD_TODO action', () => {
|
it('addTodo should create ADD_TODO action', () => {
|
||||||
expect(actions.addTodo('Use Redux')).toEqual({
|
expect(actions.addTodo('Use Redux')).toEqual({
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Use Redux'
|
text: 'Use Redux',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteTodo should create DELETE_TODO action', () => {
|
it('deleteTodo should create DELETE_TODO action', () => {
|
||||||
expect(actions.deleteTodo(1)).toEqual({
|
expect(actions.deleteTodo(1)).toEqual({
|
||||||
type: types.DELETE_TODO,
|
type: types.DELETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,26 +21,26 @@ describe('todo actions', () => {
|
||||||
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
|
||||||
type: types.EDIT_TODO,
|
type: types.EDIT_TODO,
|
||||||
id: 1,
|
id: 1,
|
||||||
text: 'Use Redux everywhere'
|
text: 'Use Redux everywhere',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('completeTodo should create COMPLETE_TODO action', () => {
|
it('completeTodo should create COMPLETE_TODO action', () => {
|
||||||
expect(actions.completeTodo(1)).toEqual({
|
expect(actions.completeTodo(1)).toEqual({
|
||||||
type: types.COMPLETE_TODO,
|
type: types.COMPLETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('completeAll should create COMPLETE_ALL action', () => {
|
it('completeAll should create COMPLETE_ALL action', () => {
|
||||||
expect(actions.completeAll()).toEqual({
|
expect(actions.completeAll()).toEqual({
|
||||||
type: types.COMPLETE_ALL
|
type: types.COMPLETE_ALL,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
it('clearCompleted should create CLEAR_COMPLETED action', () => {
|
||||||
expect(actions.clearCompleted('Use Redux')).toEqual({
|
expect(actions.clearCompleted('Use Redux')).toEqual({
|
||||||
type: types.CLEAR_COMPLETED
|
type: types.CLEAR_COMPLETED,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,13 +5,16 @@ import Footer from '../../components/Footer';
|
||||||
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
function setup(propOverrides) {
|
function setup(propOverrides) {
|
||||||
const props = Object.assign({
|
const props = Object.assign(
|
||||||
|
{
|
||||||
completedCount: 0,
|
completedCount: 0,
|
||||||
activeCount: 0,
|
activeCount: 0,
|
||||||
filter: SHOW_ALL,
|
filter: SHOW_ALL,
|
||||||
onClearCompleted: expect.createSpy(),
|
onClearCompleted: expect.createSpy(),
|
||||||
onShow: expect.createSpy()
|
onShow: expect.createSpy(),
|
||||||
}, propOverrides);
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
renderer.render(<Footer {...props} />);
|
renderer.render(<Footer {...props} />);
|
||||||
|
@ -19,13 +22,14 @@ function setup(propOverrides) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output
|
output: output,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextContent(elem) {
|
function getTextContent(elem) {
|
||||||
const children = Array.isArray(elem.props.children) ?
|
const children = Array.isArray(elem.props.children)
|
||||||
elem.props.children : [elem.props.children];
|
? elem.props.children
|
||||||
|
: [elem.props.children];
|
||||||
|
|
||||||
return children.reduce(function concatText(out, child) {
|
return children.reduce(function concatText(out, child) {
|
||||||
// Children are either elements or text strings
|
// Children are either elements or text strings
|
||||||
|
@ -63,11 +67,13 @@ describe('components', () => {
|
||||||
expect(filter.type).toBe('li');
|
expect(filter.type).toBe('li');
|
||||||
const a = filter.props.children;
|
const a = filter.props.children;
|
||||||
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
expect(a.props.className).toBe(i === 0 ? 'selected' : '');
|
||||||
expect(a.props.children).toBe({
|
expect(a.props.children).toBe(
|
||||||
|
{
|
||||||
0: 'All',
|
0: 'All',
|
||||||
1: 'Active',
|
1: 'Active',
|
||||||
2: 'Completed'
|
2: 'Completed',
|
||||||
}[i]);
|
}[i]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
const props = {
|
const props = {
|
||||||
addTodo: expect.createSpy()
|
addTodo: expect.createSpy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
|
@ -16,7 +16,7 @@ function setup() {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,24 +7,30 @@ import Footer from '../../components/Footer';
|
||||||
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters';
|
||||||
|
|
||||||
function setup(propOverrides) {
|
function setup(propOverrides) {
|
||||||
const props = Object.assign({
|
const props = Object.assign(
|
||||||
todos: [{
|
{
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
actions: {
|
actions: {
|
||||||
editTodo: expect.createSpy(),
|
editTodo: expect.createSpy(),
|
||||||
deleteTodo: expect.createSpy(),
|
deleteTodo: expect.createSpy(),
|
||||||
completeTodo: expect.createSpy(),
|
completeTodo: expect.createSpy(),
|
||||||
completeAll: expect.createSpy(),
|
completeAll: expect.createSpy(),
|
||||||
clearCompleted: expect.createSpy()
|
clearCompleted: expect.createSpy(),
|
||||||
}
|
},
|
||||||
}, propOverrides);
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
renderer.render(<MainSection {...props} />);
|
renderer.render(<MainSection {...props} />);
|
||||||
|
@ -33,7 +39,7 @@ function setup(propOverrides) {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,11 +61,15 @@ describe('components', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be checked if all todos completed', () => {
|
it('should be checked if all todos completed', () => {
|
||||||
const { output } = setup({ todos: [{
|
const { output } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 0
|
id: 0,
|
||||||
}]});
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
const [toggle] = output.props.children;
|
const [toggle] = output.props.children;
|
||||||
expect(toggle.props.checked).toBe(true);
|
expect(toggle.props.checked).toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -99,11 +109,15 @@ describe('components', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
|
||||||
const { output, props } = setup({ todos: [{
|
const { output, props } = setup({
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]});
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
const [, , footer] = output.props.children;
|
const [, , footer] = output.props.children;
|
||||||
footer.props.onClearCompleted();
|
footer.props.onClearCompleted();
|
||||||
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
expect(props.actions.clearCompleted.calls.length).toBe(0);
|
||||||
|
|
|
@ -9,18 +9,16 @@ function setup( editing = false ) {
|
||||||
todo: {
|
todo: {
|
||||||
id: 0,
|
id: 0,
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false
|
completed: false,
|
||||||
},
|
},
|
||||||
editTodo: expect.createSpy(),
|
editTodo: expect.createSpy(),
|
||||||
deleteTodo: expect.createSpy(),
|
deleteTodo: expect.createSpy(),
|
||||||
completeTodo: expect.createSpy()
|
completeTodo: expect.createSpy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
renderer.render(
|
renderer.render(<TodoItem {...props} />);
|
||||||
<TodoItem {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = renderer.getRenderOutput();
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ function setup( editing = false ) {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,19 +4,20 @@ import TestUtils from 'react-addons-test-utils';
|
||||||
import TodoTextInput from '../../components/TodoTextInput';
|
import TodoTextInput from '../../components/TodoTextInput';
|
||||||
|
|
||||||
function setup(propOverrides) {
|
function setup(propOverrides) {
|
||||||
const props = Object.assign({
|
const props = Object.assign(
|
||||||
|
{
|
||||||
onSave: expect.createSpy(),
|
onSave: expect.createSpy(),
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
placeholder: 'What needs to be done?',
|
placeholder: 'What needs to be done?',
|
||||||
editing: false,
|
editing: false,
|
||||||
newTodo: false
|
newTodo: false,
|
||||||
}, propOverrides);
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
const renderer = TestUtils.createRenderer();
|
const renderer = TestUtils.createRenderer();
|
||||||
|
|
||||||
renderer.render(
|
renderer.render(<TodoTextInput {...props} />);
|
||||||
<TodoTextInput {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = renderer.getRenderOutput();
|
let output = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ function setup(propOverrides) {
|
||||||
return {
|
return {
|
||||||
props: props,
|
props: props,
|
||||||
output: output,
|
output: output,
|
||||||
renderer: renderer
|
renderer: renderer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,240 +4,322 @@ import * as types from '../../constants/ActionTypes';
|
||||||
|
|
||||||
describe('todos reducer', () => {
|
describe('todos reducer', () => {
|
||||||
it('should handle initial state', () => {
|
it('should handle initial state', () => {
|
||||||
expect(
|
expect(todos(undefined, {})).toEqual([
|
||||||
todos(undefined, {})
|
{
|
||||||
).toEqual([{
|
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle ADD_TODO', () => {
|
it('should handle ADD_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([], {
|
todos([], {
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Run the tests'
|
text: 'Run the tests',
|
||||||
})
|
})
|
||||||
).toEqual([{
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Run the tests'
|
text: 'Run the tests',
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Fix the tests'
|
text: 'Fix the tests',
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Fix the tests',
|
text: 'Fix the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 2
|
id: 2,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle DELETE_TODO', () => {
|
it('should handle DELETE_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.DELETE_TODO,
|
type: types.DELETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle EDIT_TODO', () => {
|
it('should handle EDIT_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.EDIT_TODO,
|
type: types.EDIT_TODO,
|
||||||
text: 'Fix the tests',
|
text: 'Fix the tests',
|
||||||
id: 1
|
id: 1,
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Fix the tests',
|
text: 'Fix the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPLETE_TODO', () => {
|
it('should handle COMPLETE_TODO', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
type: types.COMPLETE_TODO,
|
type: types.COMPLETE_TODO,
|
||||||
id: 1
|
id: 1,
|
||||||
})
|
}
|
||||||
).toEqual([{
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle COMPLETE_ALL', () => {
|
it('should handle COMPLETE_ALL', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
type: types.COMPLETE_ALL
|
],
|
||||||
})
|
{
|
||||||
).toEqual([{
|
type: types.COMPLETE_ALL,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
// Unmark if all todos are currently completed
|
// Unmark if all todos are currently completed
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
type: types.COMPLETE_ALL
|
],
|
||||||
})
|
{
|
||||||
).toEqual([{
|
type: types.COMPLETE_ALL,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle CLEAR_COMPLETED', () => {
|
it('should handle CLEAR_COMPLETED', () => {
|
||||||
expect(
|
expect(
|
||||||
todos([{
|
todos(
|
||||||
|
[
|
||||||
|
{
|
||||||
text: 'Run the tests',
|
text: 'Run the tests',
|
||||||
completed: true,
|
completed: true,
|
||||||
id: 1
|
id: 1,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}], {
|
},
|
||||||
type: types.CLEAR_COMPLETED
|
],
|
||||||
})
|
{
|
||||||
).toEqual([{
|
type: types.CLEAR_COMPLETED,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Use Redux',
|
text: 'Use Redux',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 0
|
id: 0,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
|
||||||
expect(
|
expect(
|
||||||
[{
|
[
|
||||||
|
{
|
||||||
type: types.COMPLETE_TODO,
|
type: types.COMPLETE_TODO,
|
||||||
id: 0
|
id: 0,
|
||||||
}, {
|
},
|
||||||
type: types.CLEAR_COMPLETED
|
{
|
||||||
}, {
|
type: types.CLEAR_COMPLETED,
|
||||||
|
},
|
||||||
|
{
|
||||||
type: types.ADD_TODO,
|
type: types.ADD_TODO,
|
||||||
text: 'Write more tests'
|
text: 'Write more tests',
|
||||||
}].reduce(todos, [{
|
},
|
||||||
|
].reduce(todos, [
|
||||||
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
completed: false,
|
completed: false,
|
||||||
text: 'Use Redux'
|
text: 'Use Redux',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
completed: false,
|
completed: false,
|
||||||
text: 'Write tests'
|
text: 'Write tests',
|
||||||
}])
|
},
|
||||||
).toEqual([{
|
])
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
text: 'Write more tests',
|
text: 'Write more tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 2
|
id: 2,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
text: 'Write tests',
|
text: 'Write tests',
|
||||||
completed: false,
|
completed: false,
|
||||||
id: 1
|
id: 1,
|
||||||
}]);
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,32 +3,32 @@ var webpack = require('webpack');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devtool: 'cheap-module-eval-source-map',
|
devtool: 'cheap-module-eval-source-map',
|
||||||
entry: [
|
entry: ['webpack-hot-middleware/client', './index'],
|
||||||
'webpack-hot-middleware/client',
|
|
||||||
'./index'
|
|
||||||
],
|
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
publicPath: '/static/'
|
publicPath: '/static/',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
new webpack.NoErrorsPlugin()
|
new webpack.NoErrorsPlugin(),
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
loaders: [{
|
loaders: [
|
||||||
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
include: __dirname
|
include: __dirname,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
test: /\.css?$/,
|
test: /\.css?$/,
|
||||||
loaders: ['style', 'raw'],
|
loaders: ['style', 'raw'],
|
||||||
include: __dirname
|
include: __dirname,
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var src = path.join(__dirname, '..', '..', 'src');
|
var src = path.join(__dirname, '..', '..', 'src');
|
||||||
|
@ -41,6 +41,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
|
||||||
module.exports.module.loaders.push({
|
module.exports.module.loaders.push({
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
include: src
|
include: src,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,13 @@ export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
|
||||||
|
|
||||||
export function increment() {
|
export function increment() {
|
||||||
return {
|
return {
|
||||||
type: INCREMENT_COUNTER
|
type: INCREMENT_COUNTER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrement() {
|
export function decrement() {
|
||||||
return {
|
return {
|
||||||
type: DECREMENT_COUNTER
|
type: DECREMENT_COUNTER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export function incrementIfOdd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function incrementAsync(delay = 1000) {
|
export function incrementAsync(delay = 1000) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dispatch(increment());
|
dispatch(increment());
|
||||||
}, delay);
|
}, delay);
|
||||||
|
|
|
@ -4,18 +4,18 @@ export const SEND_TO_MONITOR = 'SEND_TO_MONITOR';
|
||||||
|
|
||||||
export function startMonitoring() {
|
export function startMonitoring() {
|
||||||
return {
|
return {
|
||||||
type: START_MONITORING
|
type: START_MONITORING,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopMonitoring() {
|
export function stopMonitoring() {
|
||||||
return {
|
return {
|
||||||
type: STOP_MONITORING
|
type: STOP_MONITORING,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendToMonitor() {
|
export function sendToMonitor() {
|
||||||
return {
|
return {
|
||||||
type: SEND_TO_MONITOR
|
type: SEND_TO_MONITOR,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,20 @@ import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { startMonitoring, stopMonitoring, sendToMonitor, increment, decrement, counter } = this.props;
|
const {
|
||||||
|
startMonitoring,
|
||||||
|
stopMonitoring,
|
||||||
|
sendToMonitor,
|
||||||
|
increment,
|
||||||
|
decrement,
|
||||||
|
counter,
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
Clicked: {counter} times
|
Clicked: {counter} times <button onClick={increment}>+</button>{' '}
|
||||||
{' '}
|
<button onClick={decrement}>-</button>{' '}
|
||||||
<button onClick={increment}>+</button>
|
<button onClick={startMonitoring}>Start monitoring</button>{' '}
|
||||||
{' '}
|
<button onClick={stopMonitoring}>Stop monitoring</button>{' '}
|
||||||
<button onClick={decrement}>-</button>
|
|
||||||
{' '}
|
|
||||||
<button onClick={startMonitoring}>Start monitoring</button>
|
|
||||||
{' '}
|
|
||||||
<button onClick={stopMonitoring}>Stop monitoring</button>
|
|
||||||
{' '}
|
|
||||||
<button onClick={sendToMonitor}>Send to the monitor</button>
|
<button onClick={sendToMonitor}>Send to the monitor</button>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
@ -27,7 +28,7 @@ Counter.propTypes = {
|
||||||
sendToMonitor: PropTypes.func.isRequired,
|
sendToMonitor: PropTypes.func.isRequired,
|
||||||
increment: PropTypes.func.isRequired,
|
increment: PropTypes.func.isRequired,
|
||||||
decrement: PropTypes.func.isRequired,
|
decrement: PropTypes.func.isRequired,
|
||||||
counter: PropTypes.number.isRequired
|
counter: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Counter;
|
export default Counter;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import * as MonitorActions from '../actions/monitoring';
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
counter: state.counter
|
counter: state.counter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
<head>
|
<head>
|
||||||
<title>Redux counter example</title>
|
<title>Redux counter example</title>
|
||||||
<style>
|
<style>
|
||||||
html, body, #root, #container {
|
html,
|
||||||
|
body,
|
||||||
|
#root,
|
||||||
|
#container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -26,8 +29,7 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root">
|
<div id="root"></div>
|
||||||
</div>
|
|
||||||
<script src="/static/bundle.js"></script>
|
<script src="/static/bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
|
||||||
import counter from './counter';
|
import counter from './counter';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
counter
|
counter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|
|
@ -7,10 +7,15 @@ var app = new require('express')();
|
||||||
var port = 4001;
|
var port = 4001;
|
||||||
|
|
||||||
var compiler = webpack(config);
|
var compiler = webpack(config);
|
||||||
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
|
app.use(
|
||||||
|
webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
app.use(webpackHotMiddleware(compiler));
|
app.use(webpackHotMiddleware(compiler));
|
||||||
|
|
||||||
app.get("/", function(req, res) {
|
app.get('/', function (req, res) {
|
||||||
res.sendFile(__dirname + '/index.html');
|
res.sendFile(__dirname + '/index.html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,6 +23,10 @@ app.listen(port, function(error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} else {
|
} else {
|
||||||
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port);
|
console.info(
|
||||||
|
'==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.',
|
||||||
|
port,
|
||||||
|
port
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,16 +3,22 @@ import thunk from 'redux-thunk';
|
||||||
import invariant from 'redux-immutable-state-invariant';
|
import invariant from 'redux-immutable-state-invariant';
|
||||||
import devTools from 'remote-redux-devtools';
|
import devTools from 'remote-redux-devtools';
|
||||||
import reducer from '../reducers';
|
import reducer from '../reducers';
|
||||||
import { START_MONITORING, STOP_MONITORING, SEND_TO_MONITOR } from '../actions/monitoring';
|
import {
|
||||||
|
START_MONITORING,
|
||||||
|
STOP_MONITORING,
|
||||||
|
SEND_TO_MONITOR,
|
||||||
|
} from '../actions/monitoring';
|
||||||
|
|
||||||
export default function configureStore(initialState) {
|
export default function configureStore(initialState) {
|
||||||
const enhancer = compose(
|
const enhancer = compose(
|
||||||
applyMiddleware(invariant(), thunk),
|
applyMiddleware(invariant(), thunk),
|
||||||
devTools({
|
devTools({
|
||||||
realtime: false,
|
realtime: false,
|
||||||
startOn: START_MONITORING, stopOn: STOP_MONITORING,
|
startOn: START_MONITORING,
|
||||||
sendOn: SEND_TO_MONITOR, sendOnError: 1,
|
stopOn: STOP_MONITORING,
|
||||||
maxAge: 30
|
sendOn: SEND_TO_MONITOR,
|
||||||
|
sendOnError: 1,
|
||||||
|
maxAge: 30,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -12,16 +12,17 @@ function mockStore(getState, expectedActions, onLastAction) {
|
||||||
if (!Array.isArray(expectedActions)) {
|
if (!Array.isArray(expectedActions)) {
|
||||||
throw new Error('expectedActions should be an array of expected actions.');
|
throw new Error('expectedActions should be an array of expected actions.');
|
||||||
}
|
}
|
||||||
if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') {
|
if (
|
||||||
|
typeof onLastAction !== 'undefined' &&
|
||||||
|
typeof onLastAction !== 'function'
|
||||||
|
) {
|
||||||
throw new Error('onLastAction should either be undefined or function.');
|
throw new Error('onLastAction should either be undefined or function.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockStoreWithoutMiddleware() {
|
function mockStoreWithoutMiddleware() {
|
||||||
return {
|
return {
|
||||||
getState() {
|
getState() {
|
||||||
return typeof getState === 'function' ?
|
return typeof getState === 'function' ? getState() : getState;
|
||||||
getState() :
|
|
||||||
getState;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
dispatch(action) {
|
dispatch(action) {
|
||||||
|
@ -31,13 +32,13 @@ function mockStore(getState, expectedActions, onLastAction) {
|
||||||
onLastAction();
|
onLastAction();
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockStoreWithMiddleware = applyMiddleware(
|
const mockStoreWithMiddleware = applyMiddleware(...middlewares)(
|
||||||
...middlewares
|
mockStoreWithoutMiddleware
|
||||||
)(mockStoreWithoutMiddleware);
|
);
|
||||||
|
|
||||||
return mockStoreWithMiddleware();
|
return mockStoreWithMiddleware();
|
||||||
}
|
}
|
||||||
|
@ -52,9 +53,7 @@ describe('actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('incrementIfOdd should create increment action', (done) => {
|
it('incrementIfOdd should create increment action', (done) => {
|
||||||
const expectedActions = [
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
{ type: actions.INCREMENT_COUNTER }
|
|
||||||
];
|
|
||||||
const store = mockStore({ counter: 1 }, expectedActions, done);
|
const store = mockStore({ counter: 1 }, expectedActions, done);
|
||||||
store.dispatch(actions.incrementIfOdd());
|
store.dispatch(actions.incrementIfOdd());
|
||||||
});
|
});
|
||||||
|
@ -67,9 +66,7 @@ describe('actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('incrementAsync should create increment action', (done) => {
|
it('incrementAsync should create increment action', (done) => {
|
||||||
const expectedActions = [
|
const expectedActions = [{ type: actions.INCREMENT_COUNTER }];
|
||||||
{ type: actions.INCREMENT_COUNTER }
|
|
||||||
];
|
|
||||||
const store = mockStore({ counter: 0 }, expectedActions, done);
|
const store = mockStore({ counter: 0 }, expectedActions, done);
|
||||||
store.dispatch(actions.incrementAsync(100));
|
store.dispatch(actions.incrementAsync(100));
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,14 +8,16 @@ function setup() {
|
||||||
increment: expect.createSpy(),
|
increment: expect.createSpy(),
|
||||||
incrementIfOdd: expect.createSpy(),
|
incrementIfOdd: expect.createSpy(),
|
||||||
incrementAsync: expect.createSpy(),
|
incrementAsync: expect.createSpy(),
|
||||||
decrement: expect.createSpy()
|
decrement: expect.createSpy(),
|
||||||
};
|
};
|
||||||
const component = TestUtils.renderIntoDocument(<Counter counter={1} {...actions} />);
|
const component = TestUtils.renderIntoDocument(
|
||||||
|
<Counter counter={1} {...actions} />
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
component: component,
|
component: component,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'),
|
||||||
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p')
|
p: TestUtils.findRenderedDOMComponentWithTag(component, 'p'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ function setup(initialState) {
|
||||||
return {
|
return {
|
||||||
app: app,
|
app: app,
|
||||||
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
|
buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'),
|
||||||
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p')
|
p: TestUtils.findRenderedDOMComponentWithTag(app, 'p'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,28 +3,27 @@ var webpack = require('webpack');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devtool: 'cheap-module-eval-source-map',
|
devtool: 'cheap-module-eval-source-map',
|
||||||
entry: [
|
entry: ['webpack-hot-middleware/client', './index'],
|
||||||
'webpack-hot-middleware/client',
|
|
||||||
'./index'
|
|
||||||
],
|
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
publicPath: '/static/'
|
publicPath: '/static/',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
new webpack.optimize.OccurenceOrderPlugin(),
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
new webpack.NoErrorsPlugin()
|
new webpack.NoErrorsPlugin(),
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
loaders: [{
|
loaders: [
|
||||||
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
include: __dirname
|
include: __dirname,
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var src = path.join(__dirname, '..', '..', 'src');
|
var src = path.join(__dirname, '..', '..', 'src');
|
||||||
|
@ -37,6 +36,6 @@ if (fs.existsSync(src) && fs.existsSync(nodeModules)) {
|
||||||
module.exports.module.loaders.push({
|
module.exports.module.loaders.push({
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loaders: ['babel'],
|
loaders: ['babel'],
|
||||||
include: src
|
include: src,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,6 @@ export const defaultSocketOptions = {
|
||||||
port: 443,
|
port: 443,
|
||||||
autoReconnect: true,
|
autoReconnect: true,
|
||||||
autoReconnectOptions: {
|
autoReconnectOptions: {
|
||||||
randomness: 30000
|
randomness: 30000,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user