Skip to content

Commit 247f78b

Browse files
committed
implemented inline widget editing with redux-form 0.2.4
1 parent f71b0fc commit 247f78b

18 files changed

+311
-53
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"react-redux": "0.9.0",
7676
"react-router": "v1.0.0-beta3",
7777
"redux": "^1.0.1",
78-
"redux-form": "^0.2.1",
78+
"redux-form": "^0.2.4",
7979
"serialize-javascript": "^1.0.0",
8080
"serve-favicon": "^2.3.0",
8181
"serve-static": "^1.10.0",

src/actions/actionTypes.js

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ export const INFO_LOAD_FAIL = 'INFO_LOAD_FAIL';
44
export const WIDGET_LOAD = 'WIDGET_LOAD';
55
export const WIDGET_LOAD_SUCCESS = 'WIDGET_LOAD_SUCCESS';
66
export const WIDGET_LOAD_FAIL = 'WIDGET_LOAD_FAIL';
7+
export const WIDGET_EDIT_START = 'WIDGET_EDIT_START';
8+
export const WIDGET_EDIT_STOP = 'WIDGET_EDIT_STOP';
9+
export const WIDGET_SAVE = 'WIDGET_SAVE';
10+
export const WIDGET_SAVE_SUCCESS = 'WIDGET_SAVE_SUCCESS';
11+
export const WIDGET_SAVE_FAIL = 'WIDGET_SAVE_FAIL';
712
export const AUTH_LOAD = 'AUTH_LOAD';
813
export const AUTH_LOAD_SUCCESS = 'AUTH_LOAD_SUCCESS';
914
export const AUTH_LOAD_FAIL = 'AUTH_LOAD_FAIL';

src/actions/widgetActions.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import {
22
WIDGET_LOAD,
33
WIDGET_LOAD_SUCCESS,
4-
WIDGET_LOAD_FAIL
4+
WIDGET_LOAD_FAIL,
5+
WIDGET_EDIT_START,
6+
WIDGET_EDIT_STOP,
7+
WIDGET_SAVE,
8+
WIDGET_SAVE_SUCCESS,
9+
WIDGET_SAVE_FAIL
510
} from './actionTypes';
611

712
export function load() {
@@ -10,3 +15,21 @@ export function load() {
1015
promise: (client) => client.get('/loadWidgets')
1116
};
1217
}
18+
19+
export function save(widget) {
20+
return {
21+
types: [WIDGET_SAVE, WIDGET_SAVE_SUCCESS, WIDGET_SAVE_FAIL],
22+
id: widget.id,
23+
promise: (client) => client.post('/updateWidget', {
24+
data: widget
25+
})
26+
};
27+
}
28+
29+
export function editStart(id) {
30+
return { type: WIDGET_EDIT_START, id };
31+
}
32+
33+
export function editStop(id) {
34+
return { type: WIDGET_EDIT_STOP, id };
35+
}

src/api/routes/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export loadWidgets from './loadWidgets';
33
export loadAuth from './loadAuth';
44
export login from './login';
55
export logout from './logout';
6+
export updateWidget from './updateWidget';

src/api/routes/loadAuth.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export default function(req) {
1+
export default function loadAuth(req) {
22
return Promise.resolve(req.session.user || null);
33
}

src/api/routes/loadInfo.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default function() {
1+
export default function loadInfo() {
22
return new Promise((resolve) => {
33
resolve({
44
message: 'This came from the api server',

src/api/routes/loadWidgets.js

+18-28
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,27 @@
1-
export default function() {
1+
const initialWidgets = [
2+
{id: 1, color: 'Red', sprocketCount: 7, owner: 'John'},
3+
{id: 2, color: 'Taupe', sprocketCount: 1, owner: 'George'},
4+
{id: 3, color: 'Green', sprocketCount: 8, owner: 'Ringo'},
5+
{id: 4, color: 'Blue', sprocketCount: 2, owner: 'Paul'}
6+
];
7+
8+
export function getWidgets(req) {
9+
let widgets = req.session.widgets;
10+
if (!widgets) {
11+
widgets = initialWidgets;
12+
req.session.widgets = widgets;
13+
}
14+
return widgets;
15+
}
16+
17+
export default function loadWidgets(req) {
218
return new Promise((resolve, reject) => {
319
// make async call to database
420
setTimeout(() => {
521
if (Math.floor(Math.random() * 3) === 0) {
622
reject('Widget load fails 33% of the time. You were unlucky.');
723
} else {
8-
resolve(
9-
[
10-
{
11-
id: 1,
12-
color: 'Red',
13-
sprocketCount: 7,
14-
owner: 'John'
15-
},
16-
{
17-
id: 2,
18-
color: 'Taupe',
19-
sprocketCount: 1,
20-
owner: 'George'
21-
},
22-
{
23-
id: 3,
24-
color: 'Green',
25-
sprocketCount: 8,
26-
owner: 'Ringo'
27-
},
28-
{
29-
id: 4,
30-
color: 'Blue',
31-
sprocketCount: 2,
32-
owner: 'Paul'
33-
}
34-
]);
24+
resolve(getWidgets(req));
3525
}
3626
}, 1000); // simulate async load
3727
});

src/api/routes/login.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default function(req) {
1+
export default function login(req) {
22
const user = {
33
name: req.body.name
44
};

src/api/routes/logout.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default function(req) {
1+
export default function logout(req) {
22
return new Promise((resolve) => {
33
req.session.destroy(() => {
44
req.session = null;

src/api/routes/updateWidget.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {getWidgets} from './loadWidgets';
2+
3+
export default function updateWidget(req) {
4+
return new Promise((resolve, reject) => {
5+
// write to database
6+
setTimeout(() => {
7+
if (Math.floor(Math.random() * 5) === 0) {
8+
reject('Oh no! Widget save fails 20% of the time. Try again.');
9+
} else {
10+
const widgets = getWidgets(req);
11+
const widget = req.body;
12+
if (widget.id) {
13+
widgets[widget.id - 1] = widget; // id is 1-based. please don't code like this in production! :-)
14+
}
15+
resolve(widget);
16+
}
17+
}, 2000); // simulate async db write
18+
});
19+
}

src/components/SurveyForm.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ function asyncValidator(data) {
2121
}
2222

2323
@connect(state => ({
24-
form: state.survey
24+
form: state.surveyForm
2525
}))
26-
@reduxForm('survey', surveyValidation).async(asyncValidator, 'email')
26+
@reduxForm('surveyForm', surveyValidation).async(asyncValidator, 'email')
2727
export default
2828
class SurveyForm extends Component {
2929
static propTypes = {

src/components/WidgetForm.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, {Component, PropTypes} from 'react';
2+
import {connect} from 'react-redux';
3+
import {bindActionCreators} from 'redux';
4+
import reduxForm from 'redux-form';
5+
import widgetValidation, {colors} from '../validation/widgetValidation';
6+
import * as widgetActions from '../actions/widgetActions';
7+
8+
@connect(
9+
state => ({
10+
form: state.widgetForm,
11+
saveError: state.widgets.saveError
12+
}),
13+
dispatch => ({
14+
...bindActionCreators(widgetActions, dispatch),
15+
dispatch
16+
})
17+
)
18+
@reduxForm('widgetForm', widgetValidation)
19+
export default class WidgetForm extends Component {
20+
static propTypes = {
21+
data: PropTypes.object.isRequired,
22+
errors: PropTypes.object.isRequired,
23+
editStop: PropTypes.func.isRequired,
24+
handleBlur: PropTypes.func.isRequired,
25+
handleChange: PropTypes.func.isRequired,
26+
handleSubmit: PropTypes.func.isRequired,
27+
invalid: PropTypes.bool.isRequired,
28+
pristine: PropTypes.bool.isRequired,
29+
save: PropTypes.func.isRequired,
30+
submitting: PropTypes.bool.isRequired,
31+
saveError: PropTypes.string,
32+
sliceKey: PropTypes.string.isRequired,
33+
touched: PropTypes.object.isRequired
34+
};
35+
36+
render() {
37+
const {sliceKey} = this.props;
38+
const { data, editStop, errors, handleBlur, handleChange, handleSubmit, invalid,
39+
pristine, save, submitting, saveError: { [sliceKey]: saveError }, touched } = this.props;
40+
const styles = require('../views/Widgets.scss');
41+
return (
42+
<tr className={submitting ? styles.saving : ''}>
43+
<td className={styles.idCol}>{data.id}</td>
44+
<td className={styles.colorCol}>
45+
<select name="color"
46+
className="form-control"
47+
value={data.color}
48+
onChange={handleChange('color')}
49+
onBlur={handleBlur('color')}>
50+
{colors.map(color => <option value={color} key={color}>{color}</option>)}
51+
</select>
52+
{errors.color && touched.color && <div className="text-danger">{errors.color}</div>}
53+
</td>
54+
<td className={styles.sprocketsCol}>
55+
<input type="text"
56+
className="form-control"
57+
value={data.sprocketCount}
58+
onChange={handleChange('sprocketCount')}
59+
onBlur={handleBlur('sprocketCount')}/>
60+
{errors.sprocketCount && touched.sprocketCount && <div className="text-danger">{errors.sprocketCount}</div>}
61+
</td>
62+
<td className={styles.ownerCol}>
63+
<input type="text"
64+
className="form-control"
65+
value={data.owner}
66+
onChange={handleChange('owner')}
67+
onBlur={handleBlur('owner')}/>
68+
{errors.owner && touched.owner && <div className="text-danger">{errors.owner}</div>}
69+
</td>
70+
<td className={styles.buttonCol}>
71+
<button className="btn btn-default"
72+
onClick={() => editStop(sliceKey)}
73+
disabled={submitting}>
74+
<i className="fa fa-ban"/> Cancel
75+
</button>
76+
<button className="btn btn-success"
77+
onClick={handleSubmit(() => save(data))}
78+
disabled={pristine || invalid || submitting}>
79+
<i className={'fa '+(submitting ? 'fa-cog fa-spin' : 'fa-cloud')}/> Save
80+
</button>
81+
{saveError && <div className="text-danger">{saveError}</div>}
82+
</td>
83+
</tr>
84+
);
85+
}
86+
}

src/reducers/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export info from './info';
33
export widgets from './widgets';
44
export auth from './auth';
55
export counter from './counter';
6-
export const survey = createFormReducer('survey', ['name', 'email', 'occupation']);
6+
export const surveyForm = createFormReducer('surveyForm', ['name', 'email', 'occupation']);
7+
export const widgetForm = createFormReducer('widgetForm', ['color', 'sprocketCount', 'owner']);

src/reducers/widgets.js

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import {
22
WIDGET_LOAD,
33
WIDGET_LOAD_SUCCESS,
4-
WIDGET_LOAD_FAIL
4+
WIDGET_LOAD_FAIL,
5+
WIDGET_EDIT_START,
6+
WIDGET_EDIT_STOP,
7+
WIDGET_SAVE,
8+
WIDGET_SAVE_SUCCESS,
9+
WIDGET_SAVE_FAIL
510
} from '../actions/actionTypes';
611

712
const initialState = {
8-
loaded: false
13+
loaded: false,
14+
editing: {},
15+
saveError: {}
916
};
1017

1118
export default function widgets(state = initialState, action = {}) {
@@ -31,6 +38,47 @@ export default function widgets(state = initialState, action = {}) {
3138
data: null,
3239
error: action.error
3340
};
41+
case WIDGET_EDIT_START:
42+
return {
43+
...state,
44+
editing: {
45+
...state.editing,
46+
[action.id]: true
47+
}
48+
};
49+
case WIDGET_EDIT_STOP:
50+
return {
51+
...state,
52+
editing: {
53+
...state.editing,
54+
[action.id]: false
55+
}
56+
};
57+
case WIDGET_SAVE:
58+
return state; // 'saving' flag handled by redux-form
59+
case WIDGET_SAVE_SUCCESS:
60+
const data = [...state.data];
61+
data[action.result.id - 1] = action.result;
62+
return {
63+
...state,
64+
data: data,
65+
editing: {
66+
...state.editing,
67+
[action.id]: false
68+
},
69+
saveError: {
70+
...state.saveError,
71+
[action.id]: null
72+
}
73+
};
74+
case WIDGET_SAVE_FAIL:
75+
return {
76+
...state,
77+
saveError: {
78+
...state.saveError,
79+
[action.id]: action.error
80+
}
81+
};
3482
default:
3583
return state;
3684
}

src/validation/validation.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,23 @@ export function maxLength(max) {
3030
};
3131
}
3232

33+
export function integer(value) {
34+
if (!Number.isInteger(Number(value))) {
35+
return 'Must be an integer';
36+
}
37+
}
38+
39+
export function oneOf(enumeration) {
40+
return value => {
41+
if (!~enumeration.indexOf(value)) {
42+
return `Must be one of: ${enumeration.join(', ')}`;
43+
}
44+
};
45+
}
46+
3347
export function createValidator(rules) {
3448
return (data = {}) => {
35-
const errors = { valid: true };
49+
const errors = {valid: true};
3650
Object.keys(rules).forEach((key) => {
3751
const rule = join([].concat(rules[key])); // concat enables both functions and arrays of functions
3852
const error = rule(data[key]);

src/validation/widgetValidation.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {createValidator, required, maxLength, integer, oneOf} from './validation';
2+
3+
export const colors = ['Blue', 'Fuchsia', 'Green', 'Orange', 'Red', 'Taupe'];
4+
5+
const widgetValidation = createValidator({
6+
color: [required, oneOf(colors)],
7+
sprocketCount: [required, integer],
8+
owner: [required, maxLength(30)]
9+
});
10+
export default widgetValidation;

0 commit comments

Comments
 (0)