Skip to content

Commit

Permalink
Implemented library to create a simple form that can be connected via…
Browse files Browse the repository at this point in the history
… redux to react
  • Loading branch information
arnogeurts committed Nov 8, 2016
1 parent 7271161 commit 129c469
Show file tree
Hide file tree
Showing 18 changed files with 475 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
examples/build
11 changes: 11 additions & 0 deletions examples/assets/template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta content="ie=edge" http-equiv="x-ua-compatible">
<title>ReactReduxForm</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
15 changes: 15 additions & 0 deletions examples/src/auth/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import createStore from '../base/create-store';
import AuthForm from './auth-form';
import Layout from './layout';

ReactDOM.render(
<Provider store={createStore(AuthForm.reducer)}>
<Layout/>
</Provider>,
document.getElementById('app')
);
16 changes: 16 additions & 0 deletions examples/src/auth/auth-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import {createForm} from '../../../src/index';
import {isRequired} from '../base/validator';

export default createForm({
name: 'auth',
selector: state => state,
fields: {
username: {
validators: [isRequired]
},
password: {
validators: [isRequired]
}
}
});
33 changes: 33 additions & 0 deletions examples/src/auth/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use strict";

import React, {PureComponent, PropTypes} from 'react';
import AuthForm from './auth-form';
import InputField from '../base/input-field';
import BaseSubmitButton from '../base/submit-button';

const UsernameField = AuthForm.connectors.field('username')(InputField);
const PasswordField = AuthForm.connectors.field('password')(InputField);
const SubmitButton = AuthForm.connectors.validator(BaseSubmitButton);

export default class Layout extends PureComponent {

constructor(...args) {
super(...args);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleSubmit() {
alert('hurray');
}

render() {
return (
<form>
<UsernameField id="username" label="Username"/>
<PasswordField type="password" id="password" label="Password"/>
<SubmitButton handleSubmit={this.handleSubmit} label="Submit"/>
</form>
);
}

}
9 changes: 9 additions & 0 deletions examples/src/base/create-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use strict";

import {applyMiddleware, createStore} from 'redux';
import createLogger from 'redux-logger';

export default rootReducer => createStore(
rootReducer,
applyMiddleware(createLogger())
);
59 changes: 59 additions & 0 deletions examples/src/base/input-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use strict";

import React, {PureComponent, PropTypes} from 'react';

export default class InputFields extends PureComponent {

static propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
type: PropTypes.string,
error: PropTypes.string,
active: PropTypes.bool.isRequired,
touched: PropTypes.bool.isRequired,
handleFocus: PropTypes.func.isRequired,
handleBlur: PropTypes.func.isRequired,
handleChange: PropTypes.func.isRequired,
};

static defaultProps = {
type: 'text'
};

constructor(...args) {
super(...args);
this.handleChange = this.handleChange.bind(this);
}

handleChange(event) {
event.preventDefault();
this.props.handleChange(event.target.value);
}

render() {
const {id, label, error, active, touched, handleFocus, handleBlur, ...rest} = this.props;
delete rest.handleChange;

let className = 'form-group';
if (active) {
className += ' active';
} else if (touched) {
className += error ? ' invalid' : ' valid';
}

return (
<div className={className}>
<label className="control-label" htmlFor={id}>{label}</label>
<input
id={id}
className="form-control"
onFocus={handleFocus}
onBlur={handleBlur}
onChange={this.handleChange}
{...rest}
/>
{error && !active && touched ? <span className="help-block">{error}</span> : null}
</div>
);
}
}
35 changes: 35 additions & 0 deletions examples/src/base/submit-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use strict";

import React, {PureComponent, PropTypes} from 'react';

export default class SubmitButton extends PureComponent {

static propTypes = {
label: PropTypes.string.isRequired,
valid: PropTypes.bool.isRequired,
handleValidate: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired
};

constructor(...args) {
super(...args);
this.handleClick = this.handleClick.bind(this);
}

handleClick(event) {
event.preventDefault();
if (this.props.valid) {
this.props.handleSubmit();
} else {
this.props.handleValidate();
}
}

render() {
return (
<button type="submit" className="btn btn-primary" onClick={this.handleClick}>
{this.props.label}
</button>
);
}
}
3 changes: 3 additions & 0 deletions examples/src/base/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use strict";

export const isRequired = value => !value ? 'This field is required' : undefined;
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "react-redux-form",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.18.2",
"babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-1": "^6.16.0",
"babel-plugin-transform-class-properties": "^6.18.0",
"html-webpack-plugin": "^2.24.1",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"redux-logger": "^2.7.4",
"webpack": "^1.13.3"
},
"scripts": {
"watch-example-auth": "EXAMPLE=auth webpack --watch",
"build-example-auth": "EXAMPLE=auth webpack"
},
"dependencies": {
"reselect": "^2.5.4",
"react-redux": "^4.4.5",
"redux": "^3.6.0"
}
}
8 changes: 8 additions & 0 deletions src/action-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

export const FORM_FOCUS_FIELD = 'FORM_FOCUS_FIELD';
export const FORM_BLUR_FIELD = 'FORM_BLUR_FIELD';
export const FORM_CHANGE_FIELD = 'FORM_CHANGE_FIELD';
export const FORM_LOAD_VALUES = 'FORM_LOAD_VALUES';
export const FORM_VALIDATE = 'FORM_VALIDATE';
export const FORM_RESET = 'FORM_RESET';
35 changes: 35 additions & 0 deletions src/create-action-creators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use strict";

import * as actionTypes from './action-types';

export default formType => ({
focusField: fieldName => ({
type: actionTypes.FORM_FOCUS_FIELD,
formType,
fieldName
}),
blurField: fieldName => ({
type: actionTypes.FORM_BLUR_FIELD,
formType,
fieldName
}),
changeField: (fieldName, value) => ({
type: actionTypes.FORM_CHANGE_FIELD,
formType,
fieldName,
value
}),
loadValues: values => ({
type: actionTypes.FORM_LOAD_VALUES,
formType,
values
}),
validate: () => ({
type: actionTypes.FORM_VALIDATE,
formType
}),
reset: () => ({
type: actionTypes.FORM_RESET,
formType
})
});
45 changes: 45 additions & 0 deletions src/create-connectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use strict";

import {createStructuredSelector} from 'reselect';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

const createMapStateToPropsForField = (fieldName, selectors) => createStructuredSelector({
value: selectors.fieldValueSelector(fieldName),
touched: selectors.fieldTouchedSelector(fieldName),
active: selectors.fieldActiveSelector(fieldName),
error: selectors.fieldErrorSelector(fieldName)
});

const createActionCreatorsForField = (fieldName, actionCreators) => ({
handleFocus: actionCreators.focusField.bind(undefined, fieldName),
handleBlur: actionCreators.blurField.bind(undefined, fieldName),
handleChange: actionCreators.changeField.bind(undefined, fieldName)
});

const createMapDispatchToPropsForField = (fieldName, actionCreators) => fieldName ?
createActionCreatorsForField(fieldName, actionCreators) :
(dispatch, ownProps) => bindActionCreators(
createActionCreatorsForField(ownProps.fieldName, actionCreators),
dispatch
);

const createMapStateToPropsForValidator = selectors => createStructuredSelector({
valid: selectors.formValidSelector
});

const createMapDispatchToPropsForValidator = actionCreators => ({
handleValidate: actionCreators.validate
});

export default (selectors, actionCreators) => ({
field: fieldName => connect(
createMapStateToPropsForField(fieldName, selectors),
createMapDispatchToPropsForField(fieldName, actionCreators)
),
validator: connect(
createMapStateToPropsForValidator(selectors),
createMapDispatchToPropsForValidator(actionCreators)
)

});
18 changes: 18 additions & 0 deletions src/create-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";

import createSelectors from './create-selectors';
import createActionCreators from './create-action-creators';
import createConnectors from './create-connectors';
import createReducer from './create-reducer';

export default formType => {
const selectors = createSelectors(formType);
const actionCreators = createActionCreators(formType);

return {
selectors,
actionCreators,
connectors: createConnectors(selectors, actionCreators),
reducer: createReducer(formType)
};
};
56 changes: 56 additions & 0 deletions src/create-reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use strict";

import * as actions from './action-types';

const updateField = (state, fieldName, object) => ({
...state,
[fieldName]: {
...state[fieldName],
...object
}
});

const focusField = (state, action) => updateField(state, action.fieldName, {active: true, touched: true});

const blurField = (state, action) => updateField(state, action.fieldName, {active: false, touched: true});

const changeField = (state, action) => updateField(state, action.fieldName, {value: action.value, touched: true});

const loadValues = (state, action) => Object.keys(action.values).reduce(
(state, fieldName) => {
state[fieldName] = {
...state[fieldName],
value: action.values[fieldName]
};

return state;
},
{...state}
);

const validate = (state, action) => Object.keys(action.formType.fields).reduce(
(state, fieldName) => {
state[fieldName] = {
...state[fieldName],
touched: true
};

return state;
},
{...state}
);

const reset = () => ({});

const actionMap = {
[actions.FORM_FOCUS_FIELD]: focusField,
[actions.FORM_BLUR_FIELD]: blurField,
[actions.FORM_CHANGE_FIELD]: changeField,
[actions.FORM_LOAD_VALUES]: loadValues,
[actions.FORM_VALIDATE]: validate,
[actions.FORM_RESET]: reset
};

export default formType => (state = {}, action) => action.type in actionMap && action.formType.name === formType.name ?
actionMap[action.type](state, action) : state;

Loading

0 comments on commit 129c469

Please sign in to comment.