Skip to content

Instantly share code, notes, and snippets.

@clindsey
Created January 21, 2017 02:45
Show Gist options
  • Save clindsey/6046173db3a3a9d9066e5b2c420efbaa to your computer and use it in GitHub Desktop.
Save clindsey/6046173db3a3a9d9066e5b2c420efbaa to your computer and use it in GitHub Desktop.
redux-form-simple-1.0.0
<div id="js-app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/dedupe.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.20.0/polyfill.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-form/6.4.3/redux-form.js"></script>

redux-form-simple-1.0.0

This is a very basic form with validations and error messages, built using redux-form.

  • Attempting to submit displays validation errors
  • Fields can be populated with initial values
  • The Materials field is parsed and formatted
  • When valid values are submitted, the form values are returned in a callback

Containers

Components which have been connected to the redux store act as a bridge to the forms.

  • defines onSubmit callback
  • provides initialValues
  • includes a Form component

Forms

An aggregate of form inputs along with custom behavior definitions.

  • decorated by redux-form
  • implements the actual <form> element
  • defines validations
  • knows when the form is invalid
  • composites generic Section components under desired namespaces

Sections

A group of inputs coupled together.

  • these are related groups of fields which can be reused inside of <FormSection>
  • the use other Sections and Controls

Controls

The basic building blocks of a form.

  • receives input value to display
  • knows about validation errors
  • calls onChange when value needs to change

Designs by @sbelous

A Pen by not important on CodePen.

License.

/*
* REDUCERS
* ACTIONS
* CONTAINERS
* COMPONENTS
* FORMS
* SECTIONS
* CONTROLS
* VALIDATORS
* UTILITIES
*/
const {Provider} = ReactRedux;
const {
Field,
FormSection,
reduxForm
} = ReduxForm;
setTimeout(() => {
const store = configureStore({});
ReactDOM.render((
<Provider {...{store}}>
<AppContainer />
</Provider>
), document.getElementById('js-app'));
}, 0);
function configureStore (initialState) {
const reducers = Redux.combineReducers({
shipments: shipmentsReducer,
form: ReduxForm.reducer // this is how redux-form gets into the reducers
});
return Redux.createStore(reducers, initialState);
}
// BEGIN REDUCERS
function shipmentsReducer (state = [], action) {
if (action.type === 'SHIPMENT_ADD') {
return [
...state,
action.shipment
];
}
return state;
}
// END REDUCERS
// BEGIN ACTIONS
const shipmentActions = {
add: shipment => ({type: 'SHIPMENT_ADD', shipment})
};
// END ACTIONS
// BEGIN CONTAINERS
class AppComponent extends React.Component {
static propTypes = {
resetForm: React.PropTypes.func.isRequired,
shipmentAdd: React.PropTypes.func.isRequired,
shipments: React.PropTypes.array.isRequired
};
constructor (props) {
super(props);
this.handleSubmit = this.onSubmit();
}
onSubmit () {
return values => {
this.props.shipmentAdd(values);
this.props.resetForm('shipments');
};
}
render () {
const {shipments} = this.props;
const initialValues = {
senderDetails: {
name: 'ACME Co.',
address: '123 Fake Ln.'
}
};
return (
<div className="o-wrapper">
<div className="o-layout">
<div className="o-layout__item u-1/2@tablet">
<h1 className="c-h1">{'A simple redux-form demo'}</h1>
<p>{'This is a very basic form with validations and error messages.'}</p>
<ul className="o-list">
<li className="u-margin-bottom-small">{'Attempting to submit displays validation errors.'}</li>
<li className="u-margin-bottom-small">{'Fields can be populated with initial values.'}</li>
<li className="u-margin-bottom-small">{'The Material field is parsed and formatted.'}</li>
<li className="u-margin-bottom-small">{'When valid values are submitting, the form values are returned in a callback.'}</li>
</ul>
</div>
<div className="o-layout__item u-1/2@tablet">
<h1 className="c-h1">{'Create a shipment'}</h1>
<p>{'Use the form below to create shipment records'}</p>
<ShipmentForm
onSubmit={this.handleSubmit}
{...{initialValues}}
/>
<ShipmentsList {...{shipments}} />
</div>
</div>
</div>
);
}
}
const AppContainer = ReactRedux.connect(state => ({
shipments: state.shipments
}), {
resetForm: ReduxForm.reset,
shipmentAdd: shipmentActions.add
})(AppComponent);
// END CONTAINERS
// BEGIN VALIDATORS
const validators = {
personDetails: values => {
const errors = {};
if (!values || !values.name) {
errors.name = 'Required';
}
if (!values || !values.address) {
errors.address = 'Required';
}
return errors;
},
messageDetails: values => {
const errors = {};
if (!values || !values.material) {
errors.material = 'Required';
}
if (!values || !values.quantity) {
errors.quantity = 'Required';
} else if (/^\d+$/.test(values.quantity) === false) {
errors.quantity = 'Must be an integer';
}
if (!values || !values.type) {
errors.type = 'Required';
}
return errors;
}
};
// END VALIDATORS
// BEGIN FORMS
class ShipmentComponent extends React.Component {
render () {
const {handleSubmit} = this.props;
return (
<form onSubmit={handleSubmit}>
<h4 className="c-h4 u-margin-bottom-small">{'Sender Details'}</h4>
<FormSection name="senderDetails">
<PersonDetailsSection />
</FormSection>
<h4 className="c-h4 u-margin-bottom-small">{'Recipient Details'}</h4>
<FormSection name="recipientDetails">
<PersonDetailsSection />
</FormSection>
<h4 className="c-h4 u-margin-bottom-small">{'Order Details'}</h4>
<FormSection name="messageDetails">
<OrderDetailsSection />
<DeliveryOptions />
</FormSection>
<button
className="c-form-button c-form-button--primary c-form-button--block"
type="submit"
>{'Submit'}</button>
<p className="c-text-small c-text-small--muted u-center-text">{'All fields are required'}</p>
</form>
);
}
}
const ShipmentForm = reduxForm({
form: 'shipments',
validate: validateFields({
senderDetails: validators.personDetails,
recipientDetails: validators.personDetails,
messageDetails: validators.messageDetails
})
})(ShipmentComponent);
// END FORMS
// BEGIN SECTIONS
class DeliveryOptions extends React.Component {
render () {
return (
<Field
component={RadioControl}
name="type"
options={[
{
classes: 'u-1/3',
label: 'Standard',
value: 'standard'
}, {
classes: 'u-1/3',
label: 'Express',
value: 'express'
}, {
classes: 'u-1/3',
label: 'Overnight',
value: 'overnight'
}
]}
/>
);
}
}
class PersonDetailsSection extends React.Component {
render () {
return (
<div className="o-layout u-margin-bottom-small">
<div className="o-layout__item u-1/1 u-1/2@tablet">
<Field
component={InputControl}
name="name"
placeholder="Name"
type="text"
/>
</div>
<div className="o-layout__item u-1/1 u-1/2@tablet">
<Field
component={InputControl}
name="address"
placeholder="Address"
type="text"
/>
</div>
</div>
);
}
}
class OrderDetailsSection extends React.Component {
render () {
return (
<div className="o-layout u-margin-bottom-small">
<div className="o-layout__item u-1/1 u-1/2@tablet">
<Field
component={InputControl}
format={formatters.base64}
name="material"
parse={parsers.base64}
placeholder="Material"
type="text"
/>
</div>
<div className="o-layout__item u-1/1 u-1/2@tablet">
<Field
component={InputControl}
name="quantity"
placeholder="Quantity"
type="text"
/>
</div>
</div>
);
}
}
// END SECTIONS
// BEGIN CONTROLS
class RadioControl extends React.Component {
static propTypes = {
options: React.PropTypes.array.isRequired
};
handleChange (value) {
return () => {
this.props.input.onChange(value);
}
}
render () {
const {
options,
input: {
name,
value
},
meta: {
error,
touched
}
} = this.props;
const className = classNames({
'c-radio-control': true,
'c-radio-control--error': touched && error
});
return (
<div {...{className}}>
<div className="o-layout">
{options.map((field, key) => {
const fieldClassName = classNames({
'c-radio-control__item': true,
'o-layout__item': true,
[field.classes]: true
});
return (
<div
className={fieldClassName}
{...{key}}
>
<input
checked={value === field.value}
className="c-radio-control__field u-hidden-visually"
id={`${name}_${key}`}
onChange={this.handleChange(field.value)}
type="radio"
value={field.value}
{...{name}}
/>
<label
className="c-radio-control__label"
htmlFor={`${name}_${key}`}
>{field.label}</label>
</div>
);
})}
<div className="o-layout__item u-1/1 c-radio-control__hint c-text-small">{touched && error}</div>
</div>
</div>
);
}
}
class InputControl extends React.Component {
static propTypes = {
placeholder: React.PropTypes.string,
type: React.PropTypes.string.isRequired,
};
render () {
const {
input,
type,
placeholder,
meta: {
error,
touched
}
} = this.props;
const className = classNames({
'c-input-control': true,
'c-input-control--error': touched && error
});
return (
<div {...{className}}>
<input
className="c-input-control__input"
{...input}
{...{type, placeholder}}
/>
<div className="c-input-control__hint c-text-small">{touched && error}</div>
</div>
);
}
}
// END CONTROLS
// BEGIN COMPONENTS
class ShipmentsList extends React.Component {
static propTypes = {
shipments: React.PropTypes.array.isRequired
};
renderShipment ({senderDetails, recipientDetails, messageDetails}, key) {
return (
<li {...{key}}>
<hr className="c-hr u-margin-bottom" />
<div className="o-media u-margin-top">
<div className="o-media__img">
<p>{key + 1}</p>
</div>
<div className="o-media__body">
<div className="o-layout">
<div className="o-layout__item u-1/2">
<dl>
<dt>{senderDetails.name}</dt>
<dd>{senderDetails.address}</dd>
</dl>
</div>
<div className="o-layout__item u-1/2">
<dl>
<dt>{recipientDetails.name}</dt>
<dd>{recipientDetails.address}</dd>
</dl>
</div>
<div className="o-layout__item u-1/1">
<p>{`"${messageDetails.material}" for `}<strong>{messageDetails.quantity}</strong>{` to be sent by `}<strong>{messageDetails.type}</strong></p>
</div>
</div>
</div>
</div>
</li>
);
}
render () {
const {shipments} = this.props;
return (
<ul className="o-list-bare">
{shipments.map(this.renderShipment)}
</ul>
);
}
}
// END COMPONENTS
// BEGIN UTILITIES
function validateFields (validators, requiredFields = {}) {
return values => {
const validationErrors = Object.keys(validators).map(name => ({
name, // 5625463739
error: validators[name](values[name])
})).reduce((p, {name, error}) => (
Object.keys(name).length ? {...p, [name]: error} : p
), {});
Object.keys(requiredFields).forEach(fieldName => {
Object.assign(validationErrors[fieldName], requiredFields[fieldName](values[fieldName]));
});
return validationErrors;
};
}
const parsers = {
base64: (string = '') => btoa(string)
};
const formatters = {
base64: (base64String = '') => atob(base64String)
};
// END UTILITIES
/*
* FORMBUTTON
* HEADERS
* INPUTCONTROL
* RADIOCONTROL
* TYPOGRAPHY
*/
$border-color: #e0e0e0;
$brand-danger: #d9534f;
$brand-gray: #9b9b9b;
$brand-primary-muted: #a0dae9;
$brand-primary: #60c1da;
$brand-secondary: #0698bd;
$control-radius: 4px;
$text-base-color: #4a4a4a;
$text-muted-color: #9b9b9b;
$white: #fff;
html {
color: $text-base-color;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
@include mq($from: tablet) {
font-size: 16px;
}
}
body {
margin-top: $inuit-global-spacing-unit;
margin-bottom: $inuit-global-spacing-unit;
}
dt {
font-weight: 600;
}
.u-center-text {
text-align: center;
}
.c-hr {
border: 0;
height: 1px;
background-color: $brand-gray;
}
// BEGIN HEADERS
.c-h1 {
font-size: 2rem;
}
.c-h2 {
font-size: 1.5rem;
font-weight: 600;
}
.c-h3 {
font-size: 1.25rem;
}
.c-h4 {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
}
.c-h5 {
font-size: 0.9375rem;
font-weight: 600;
}
// END HEADERS
// BEGIN TYPOGRAPHY
.c-text-strong {
color: $brand-secondary;
}
.c-text-strong--inverse {
color: $white;
}
.c-text-strong--stronger {
font-weight: 600;
color: $text-base-color;
}
.c-text-strong--super-strong {
font-weight: 600;
font-size: 1.25rem;
}
.c-text-small {
font-size: 0.875rem;
}
.c-text-small--muted {
color: $brand-gray;
}
.c-text-small--strong {
color: $brand-secondary;
}
.c-text-small--stronger {
font-weight: 600;
}
// END TYPOGRAPHY
// BEGIN FORMBUTTON
.c-form-button {
border: solid 1px $white;
border-radius: $control-radius;
color: $white;
cursor: pointer;
display: inline-block;
font: inherit;
font-weight: 600;
margin: $inuit-global-spacing-unit-small 0;
padding: round($inuit-global-spacing-unit-small * 0.5) $inuit-global-spacing-unit;
text-align: center;
vertical-align: middle;
text-decoration: none;
}
.c-form-button--primary {
background-color: $brand-primary;
border-color: $brand-primary;
&.c-form-button--disabled,
&:disabled {
background-color: $brand-primary-muted;
border-color: $brand-primary-muted;
}
&.c-form-button--inverse {
background-color: $white;
color: $brand-primary;
border-color: $brand-primary;
}
}
.c-form-button--block {
width: 100%;
}
.c-form-button--destructive {
background-color: $brand-danger;
border-color: $brand-danger;
}
// END FORMBUTTON
// BEGIN INPUTCONTROL
.c-input-control__input {
-webkit-backface-visibility: hidden;
border: none;
border-bottom: solid 1px $border-color;
color: $text-base-color;
font-family: inherit;
font-weight: 300;
margin-bottom: $inuit-global-spacing-unit;
padding: 0;
width: 100%;
}
.c-input-control__hint {
color: $brand-danger;
margin-top: -$inuit-global-spacing-unit;
min-height: $inuit-global-spacing-unit;
}
.c-input-control--error {
.c-input-control__input {
border-bottom: solid 1px $brand-danger;
}
}
// END INPUTCONTROL
// BEGIN FORMCONTROL
.c-radio-control__label {
border-radius: $control-radius;
border: solid 1px $border-color;
color: $text-muted-color;
cursor: pointer;
display: block;
margin: 0 auto;
overflow-x: hidden;
padding: $inuit-global-spacing-unit-tiny;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.c-radio-control__item {
margin-bottom: $inuit-global-spacing-unit-tiny;
text-align: center;
}
.c-radio-control__field:checked + label {
border-color: $brand-primary;
color: $brand-primary;
}
.c-radio-control--error {
.c-radio-control__hint {
color: $brand-danger;
}
}
.c-radio-control__hint {
min-height: $inuit-global-spacing-unit;
}
// END FORMCONTROL
<link href="//codepen.io/clindsey/pen/XNKwXY" rel="stylesheet" />
<link href="//codepen.io/clindsey/pen/bggKdg" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,600" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment