Last active
March 4, 2019 03:50
-
-
Save ccurtin/6486ad7ccc06bf632e74132c6a01e45e to your computer and use it in GitHub Desktop.
Redux-Form Material-UI v1 Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import FormExample from '_helpers/FormExample' | |
/* | |
Add the form to your application. `initialValues` are handled through props as well as form submissions `onSubmit()` | |
*/ | |
<FormExample onSubmit={(values) => {window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`)}} initialValues={{date:null, textbox:'This was generated via `initialValues` prop on the Form!', switchExample:true, poops: true, ethnicity: 'asian' }} /> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const theme = { | |
palette: { | |
textColor: '#FFFFFF', | |
primary: { | |
// light: will be calculated from palette.primary.main, | |
// main: '#36afa2', | |
main: '#47BCA2', | |
// dark: '#6E6462', | |
dark: '#2a9074', | |
light: '#AFDDD0', | |
// dark: will be calculated from palette.primary.main, | |
// contrastText: will be calculated to contast with palette.primary.main | |
contrastText: '#FFFFFF' | |
}, | |
secondary: { | |
light: '#6F9DD9', | |
dark: '#6E6462', | |
main: '#368af1' | |
// dark: will be calculated from palette.secondary.main, | |
// contrastText: '#FFFFFF', | |
}, | |
accents: { | |
accent1: '#639CE6', | |
accent2: '#AACBF6', | |
error: '#FF7878' | |
}, | |
// error: will us the default color | |
}, | |
overrides: { | |
MuiDialogActions:{}, | |
MuiList: { | |
root: { | |
paddingTop: '0!important', | |
paddingBottom: '0!important' | |
} | |
}, | |
MuiTypography: { | |
body1: { | |
fontWeight: 500 | |
} | |
}, | |
MuiIconButton: { | |
root: { | |
textShadow:'none', | |
boxShadow:'none', | |
} | |
}, | |
MuiButtonBase: { | |
// Name of the styleSheet | |
root: { | |
textShadow:'none', | |
boxShadow:'none', | |
} | |
}, | |
MuiButton: { | |
// Name of the styleSheet | |
root: { | |
// Name of the rule | |
// background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)', | |
// borderRadius: 3, | |
// border: 0, | |
// color: 'white', | |
// height: 48, | |
// padding: '0 30px', | |
textShadow:'none!important' | |
// boxShadow: '0 3px 5px 2px rgba(255, 0, 0, .90)', | |
}, | |
raised: { | |
// Name of the rule | |
// background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)', | |
// borderRadius: 3, | |
// border: 0, | |
// color: 'white', | |
// height: 48, | |
// padding: '0 30px', | |
boxShadow: 'none', | |
textShadow: 'none', | |
letterSpacing: 0, | |
fontWeight: 400, | |
'&:active': { | |
boxShadow: 'none', | |
boxShadow: '0 2px 25px 2px rgba(0, 0, 0, .30)' | |
} | |
} | |
} | |
} | |
} | |
export default theme |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
BE AWARE ::::: using any "default" value props here _WILL NOT_ update the redux store. | |
*** the `initialValues` prop needs to contain an object with key(input_name)/value | |
* if you want to update the Redux store with REAL inital values... otherwise it's JUST for display | |
* ex: import FormExample from './FormExample.jsx' | |
<FormExample initialValues={{ textbox:'This was generated via `initialValues` prop on the Form!', switchExample:true, drinks: true, ethnicity: 'asian' }} /> | |
*/ | |
import React from 'react' | |
import Select from 'material-ui/Select' | |
import Checkbox from 'material-ui/Checkbox' | |
import {MenuItem} from 'material-ui/Menu' | |
import {connect} from 'react-redux' | |
import {reduxForm, change, Field} from 'redux-form' | |
import { | |
FormGroup, | |
FormControl, | |
FormControlLabel, | |
FormHelperText | |
} from 'material-ui/Form' | |
import Input, {InputLabel} from 'material-ui/Input' | |
import Radio, {RadioGroup} from 'material-ui/Radio' | |
import Switch from 'material-ui/Switch' | |
import * as Form from '_helpers/Forms' | |
const validate = (values) => { | |
// define the `ERRORS` object | |
const errors = {} | |
// import pre-defined validations | |
Form.Validations.required(values, errors, [ | |
'firstname', | |
'lastname', | |
'email', | |
'station', | |
'role', | |
'eats', | |
'drinks' | |
]) | |
Form.Validations.email(values, errors) | |
// Alternatively, you can define validations in the same file as the <form/> you are working on | |
if (values.date !== "2018/12/22") { | |
errors.date = 'Unfortunately, this date is invalid.' | |
} | |
if (values.switchExample_1 !== true) { | |
errors.switchExample_1 = 'The lights have to be on to continue...' | |
} | |
if (values.ethnicity !== 'asian') { | |
errors.ethnicity = 'Sorry, but you must be asian!' | |
} | |
if (values.drinks !== true) { | |
values.ethnicity = 'latinAmerican' | |
} | |
if (values.employed !== true) { | |
errors.employed = 'Required! Check dat!' | |
} | |
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) { | |
errors.email = 'Invalid email address' | |
} | |
if (!values.firstname) { | |
errors.firstname = 'Required' | |
} else if (values.firstname == 'Chris') { | |
errors.firstname = 'Sorry, this username is already taken!' | |
} else if (values.firstname.length < 8) { | |
errors.firstname = 'Must be at least 8 characters' | |
} | |
// `errors` MUST be returned | |
// **NOTE: Any modification directly to any values, WILL be reflected in the redux store. | |
return errors | |
} | |
class FormExample extends React.Component { | |
constructor(props) { | |
super(props) | |
} | |
render() { | |
// Ensure that the INITIALIZER reducer was triggered and `initialValues` are available before rendering UI | |
if (typeof this.props.theFormData !== 'undefined') { | |
let {windowData, handleSubmit, pristine, submitting} = this.props | |
return ( | |
<form onSubmit={handleSubmit}> | |
<div> | |
<div> | |
<h1>Text/Inputs:</h1> | |
</div> | |
<div | |
style={{ | |
width: (windowData.width > 480 && '47.5%') || '100%', | |
float: 'left', | |
marginRight: windowData.width > 480 && '2.5%' | |
}} | |
> | |
<Field | |
name="firstname" | |
component={Form.Elements.Text} | |
label="first name" | |
type="text" | |
required={true} | |
// multiline | |
// rowsMax="4" | |
/> | |
</div> | |
<div | |
style={{ | |
width: (windowData.width > 480 && '47.5%') || '100%', | |
float: 'left', | |
marginLeft: windowData.width > 480 && '2.5%' | |
}} | |
> | |
<Field | |
name="lastname" | |
label="last name" | |
FormControlProps={{ | |
required: true, | |
style: {width: '100%'}, | |
disabled: true | |
}} | |
component={Form.Elements.Text} | |
/> | |
</div> | |
<div> | |
<Field | |
name="email" | |
label="email address" | |
type="email" | |
component={Form.Elements.Text} | |
normalize={Form.Normalizers.email} | |
/> | |
</div> | |
<div> | |
<Field | |
name="textbox" | |
label="Some Long Text:" | |
type="text" | |
component={Form.Elements.Text} | |
multiline | |
rowsMax="4" | |
/> | |
</div> | |
</div> | |
<div> | |
<h1>Dropdown/Selects:</h1> | |
</div> | |
<div> | |
<Field | |
name="station" | |
label="Assign a Station" | |
// readOnly | |
FormControlProps={{ | |
fullWidth: true, | |
required: true, | |
style: {width: '100%'} | |
}} | |
component={Form.Elements.Select} | |
> | |
<MenuItem value={'station_a'}>Station A</MenuItem> | |
<MenuItem value={'station_b'}>Station B</MenuItem> | |
<MenuItem value={'station_c'}>Station C</MenuItem> | |
<MenuItem value={'station_d'}>Station D</MenuItem> | |
<MenuItem value={'station_e'}>Station E</MenuItem> | |
</Field> | |
<Field | |
name="role" | |
label="Assign a Role" | |
component={Form.Elements.Select} | |
// multiple // ***NOTE***: using `multiple` requires a more advanced `component` than what is in Forms.jsx (check out: https://material-ui-next.com/demos/selects/) | |
> | |
<MenuItem value={'employee'}>Employee</MenuItem> | |
<MenuItem value={'manager'}>Manager</MenuItem> | |
</Field> | |
</div> | |
<div> | |
<h1>Date/Time/DateTime:</h1> | |
</div> | |
{/* DATE (calendat) */} | |
<div> | |
<Field | |
name="date" | |
label="Date" | |
component={Form.Elements.DatePicker} | |
dateFormat="YYYY/MM/DD" | |
// defaultDate={Date()} | |
options={{openToYearSelection: false, autoOk: true}} | |
/> | |
</div> | |
{/* TIME (clock) */} | |
<div> | |
<Field | |
name="time" | |
label="Time" | |
component={Form.Elements.TimePicker} | |
// defaultTime={new Date(new Date().setHours(new Date().getHours() - 2))} | |
/> | |
</div> | |
{/* DATETIME */} | |
<div> | |
<Field | |
name="datetime" | |
label="DateTime" | |
component={Form.Elements.DateTimePicker} | |
// defaultDateTime={new Date(new Date().setDate(new Date().getDate() - 30))} | |
/> | |
</div> | |
<div> | |
<h1>Checkbox:</h1> | |
</div> | |
{ | |
<div> | |
<Field | |
name="eats" | |
label="Eats" | |
component={Form.Elements.Checkbox} | |
/> | |
<Field | |
name="drinks" | |
label="Drinks" | |
component={Form.Elements.Checkbox} | |
defaultValue="I_can_post_this_value" | |
/> | |
</div> | |
} | |
{/* | |
`RadioGroup` is used so that the {children} the group are limited to a SINGLE selection, unlike say checkboxes. | |
*/} | |
<div> | |
<h1>Radio:</h1> | |
</div> | |
<div> | |
<Field name="ethnicity" component={Form.Elements.RadioGroup}> | |
{/* <Radio value="male" label="male" /> | |
<Radio value="female" label="female" /> | |
<Radio value="trans" label="trans" />*/} | |
<FormControlLabel | |
value="caucasion" | |
control={ | |
<Radio | |
disabled={this.props.theFormData.values.drinks === false} | |
/> | |
} | |
label="White" | |
/> | |
<FormControlLabel | |
value="asian" | |
control={ | |
<Radio | |
disabled={this.props.theFormData.values.drinks === false} | |
/> | |
} | |
label="Asian" | |
/> | |
<FormControlLabel | |
value="latinAmerican" | |
control={<Radio />} | |
label="Latin American" | |
/> | |
<FormControlLabel | |
value="none" | |
control={ | |
<Radio | |
disabled={this.props.theFormData.values.drinks === false} | |
/> | |
} | |
label="none of the above" | |
/> | |
</Field> | |
</div> | |
<div> | |
<h1>Switch/Toggle:</h1> | |
</div> | |
<div> | |
<Field | |
name="switchExample_1" | |
component={Form.Elements.Switch} | |
label="Lights" | |
// example of disable fields based on other fields | |
disabled={this.props.theFormData.values.drinks === false} | |
/> | |
<Field | |
name="switchExample_2" | |
component={Form.Elements.Switch} | |
label="Enable Feature A" | |
// example of disable fields based on other fields | |
disabled={this.props.theFormData.values.drinks === false} | |
/> | |
<Field | |
name="switchExample_3" | |
component={Form.Elements.Switch} | |
label="Enable Feature B" | |
// example of disable fields based on other fields | |
disabled={this.props.theFormData.values.drinks === false} | |
/> | |
</div> | |
<button type="submit" disabled={pristine || submitting}>Submit</button> | |
</form> | |
) | |
} else { | |
/* this almost certainly will not be seen but is needed when accessing initalValues from state, ir: `theFormData` */ | |
return (<div>Loading...</div>) | |
} | |
} | |
} | |
FormExample = reduxForm({ | |
// allows the <form> to be PERSISTENT when Modal closes(on re/rendering). (careful!) | |
destroyOnUnmount: false, | |
// a unique name for the form | |
validate, | |
form: 'FormExample' | |
// enableReinitialize: true, // fix issue "Redux Form - initialValues not updating with state" (http://stackoverflow.com/questions/38881324/redux-form-initialvalues-not-updating-with-state) | |
})(FormExample) | |
const mapStateToProps = (state) => ({ | |
windowData: state.windowData.windowData, | |
theFormData: state.form.FormExample | |
}) | |
export default connect(mapStateToProps)(FormExample) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import Select from 'material-ui/Select' | |
import Checkbox from 'material-ui/Checkbox' | |
import {MenuItem} from 'material-ui/Menu' | |
import TextField from 'material-ui/TextField' | |
import {connect} from 'react-redux' | |
import {reduxForm, change, Field} from 'redux-form' | |
import { | |
FormGroup, | |
FormControl, | |
FormControlLabel, | |
FormHelperText, | |
FormLabel | |
} from 'material-ui/Form' | |
import {InputLabel} from 'material-ui/Input' | |
import Radio, {RadioGroup} from 'material-ui/Radio' | |
import Switch from 'material-ui/Switch' | |
import TimePicker from 'material-ui-pickers/TimePicker' | |
import DatePicker from 'material-ui-pickers/DatePicker' | |
import DateTimePicker from 'material-ui-pickers/DateTimePicker' | |
import {MuiThemeProvider, createMuiTheme, withTheme} from 'material-ui/styles' | |
import defaultMaterialTheme from '_helpers/defaultMaterialTheme' | |
import styles from './styles/Forms.scss' | |
import validator from 'validator' | |
// for overriding the "buttons" and elements WITHIN the datepicker | |
const theme = createMuiTheme( | |
Object.assign({}, defaultMaterialTheme, { | |
overrides: { | |
MuiDialogActions: { | |
root: { | |
display: 'none' // `autoOk` prop is active on DatePicker so it automatically closes after selection is made | |
} | |
}, | |
// just as an example.. not shown because MuiDialogActions is {{display:none}} | |
MuiPickersModal: { | |
dialogAction: { | |
color: '#FFF', | |
background: `${defaultMaterialTheme.palette.secondary.main}`, | |
'&:hover': { | |
background: `${defaultMaterialTheme.palette.secondary.dark}` | |
} | |
} | |
} | |
} | |
}) | |
) | |
/* Renders the error messages that appear below input fields */ | |
export const renderInputError = (meta) => { | |
return ( | |
meta.touched && | |
meta.error && ( | |
<FormHelperText className={styles.input_error}> | |
{meta.error} | |
</FormHelperText> | |
) | |
) | |
} | |
export const renderCheckboxError = (message) => { | |
return ( | |
<FormHelperText className={styles.input_error}>{message}</FormHelperText> | |
) | |
} | |
export const isErrorActive = (meta) => { | |
return Boolean(meta.touched && meta.error) | |
} | |
export const Elements = { | |
/* Text inputs. (single-line, multi-line, email) */ | |
Text: (props) => { | |
let { | |
label, | |
input, | |
input: {value}, | |
children, | |
meta, | |
FormControlProps, | |
...custom | |
} = props | |
return ( | |
<MuiThemeProvider theme={theme}> | |
<FormControl | |
margin="normal" | |
fullWidth | |
error={isErrorActive(meta)} | |
{...FormControlProps} | |
> | |
<TextField | |
label={label} | |
value={value} | |
{...input} | |
onBlur={() => input.onBlur(value)} | |
autoComplete="disabled" | |
error={isErrorActive(meta)} | |
{...custom} | |
/> | |
{renderInputError(meta)} | |
</FormControl> | |
</MuiThemeProvider> | |
) | |
}, | |
/* Dropdown <select/> components */ | |
Select: (props) => { | |
let { | |
label, | |
input, | |
input: {value}, | |
children, | |
meta, | |
FormControlProps, | |
...custom | |
} = props | |
return ( | |
<FormControl | |
margin="normal" | |
fullWidth | |
error={isErrorActive(meta)} | |
{...FormControlProps} | |
> | |
<InputLabel htmlFor={input.name}>{label}</InputLabel> | |
<Select | |
value={value} | |
{...input} | |
onBlur={() => input.onBlur(value)} | |
{...custom} | |
> | |
{children} | |
</Select> | |
{renderInputError(meta)} | |
</FormControl> | |
) | |
}, | |
/* Calendar */ | |
// WARNING***: Do _NOT_ use <FormControl/> with <DatePicker/> it's becomes buggy | |
// and date selection does not work when user changes the month. | |
DatePicker: (props) => { | |
let { | |
label, | |
input, | |
input: {value}, | |
children, | |
meta, | |
options, | |
...custom | |
} = props | |
return ( | |
<div> | |
<MuiThemeProvider theme={theme}> | |
<DatePicker | |
{...input} | |
format={props.dateFormat || 'MM/DD/YYYY'} | |
// autoOk | |
value={ | |
(props.input.value && | |
new Date(props.input.value).toISOString()) || | |
(props.defaultDate && | |
new Date(props.defaultDate).toISOString()) || | |
new Date().toISOString() | |
} | |
onChange={props.input.onChange} | |
// ***NOTE::: kind of HACKISH because if field is VALID(doesn't throws an error) and users closes the calendar modal, the error messages will still be displayed until the user blurs the textfield | |
onClose={() => | |
setTimeout( | |
() => document.querySelector(`[name="${input.name}"]`).blur(), | |
350 | |
) | |
} | |
// onBlur={() => input.onBlur(value)} | |
// showTodayButton | |
// disableFuture | |
{...options} | |
/> | |
</MuiThemeProvider> | |
{renderInputError(meta, props)} | |
</div> | |
) | |
}, | |
TimePicker: (props) => { | |
let { | |
label, | |
input, | |
input: {value}, | |
children, | |
meta, | |
options, | |
...custom | |
} = props | |
return ( | |
<div> | |
<TimePicker | |
// autoOk | |
{...input} | |
value={props.input.value || props.defaultTime} | |
onChange={props.input.onChange} | |
onClose={() => | |
setTimeout( | |
() => document.querySelector(`[name="${input.name}"]`).blur(), | |
250 | |
) | |
} | |
{...options} | |
/> | |
{renderInputError(meta)} | |
</div> | |
) | |
}, | |
DateTimePicker: (props) => { | |
let { | |
label, | |
input, | |
input: {value}, | |
children, | |
meta, | |
options, | |
...custom | |
} = props | |
return ( | |
<div> | |
<DateTimePicker | |
{...input} | |
value={props.input.value || props.defaultDateTime} | |
onChange={props.input.onChange} | |
onClose={() => | |
setTimeout( | |
() => document.querySelector(`[name="${input.name}"]`).blur(), | |
250 | |
) | |
} | |
{...options} | |
/> | |
{renderInputError(meta)} | |
</div> | |
) | |
}, | |
// | |
// updates the redux store with a STRING value | |
// NOTE***: Radio buttons are NOT like checkboxes or Switches/Toggles (which return boolean values) | |
// While you "could" programatically make a Radio button a boolean, they return STRING values to the redux store. | |
// | |
RadioGroup: (props) => { | |
let { | |
label, | |
input, | |
input: {value, name}, | |
children, | |
meta, | |
options, | |
...custom | |
} = props | |
return ( | |
<div> | |
<RadioGroup | |
{...input} | |
name={name} | |
value={value} | |
onChange={(event, value) => input.onChange(value)} | |
{...custom} | |
> | |
{children} | |
</RadioGroup> | |
{renderInputError(meta)} | |
</div> | |
) | |
}, | |
////////////////////// | |
// Checkbox element // | |
// | |
// Do NOT use <FormControl/> wrapper since multiple checkboxes could be placed inside one <FormControl/> | |
// | |
// you can use custom icons with the `icon` and `checkedIcon` propsa | |
// NOTE::: The redux store will store CHECKBOX values as `Boolean`, but the UI MUST be string value (hence the `val + ""` coercion) | |
// Checkboxes CAN contain & post values, ie: `value="nissan"` but in the redux store will ALWAYS only be boolean?(be default...) | |
// | |
////////////////////// | |
Checkbox: (props) => { | |
let { | |
label, | |
input, | |
input: {value}, | |
children, | |
meta, | |
FormControlProps, | |
...custom | |
} = props | |
return ( | |
// <FormControl | |
// margin="normal" | |
// fullWidth | |
// error={isErrorActive(meta)} | |
// {...FormControlProps} | |
// > | |
<span> | |
<FormControlLabel | |
control={ | |
<Checkbox | |
{...input} | |
checked={props.input.value ? true : false} | |
onChange={props.input.onChange} | |
value={props.defaultValue || props.input.value + ''} | |
{...custom} | |
/> | |
} | |
label={label} | |
/> | |
{renderInputError(meta)} | |
</span> | |
// </FormControl> | |
) | |
}, | |
// Toggle/Switch: | |
// Updates the redux store with a BOOLEAN value, but form "values" can still be accessed and POST'd if needed | |
Switch: (props) => { | |
let { | |
label, | |
input, | |
input: {value}, | |
children, | |
meta, | |
FormControlProps, | |
...custom | |
} = props | |
return ( | |
<span> | |
<FormControlLabel | |
control={ | |
<Switch | |
{...input} | |
checked={props.input.value ? true : false} | |
onChange={props.input.onChange} | |
onBlur={props.input.onBlur} | |
value={props.defaultValue} | |
{...custom} | |
/> | |
} | |
label={label} | |
/> | |
{renderInputError(meta)} | |
</span> | |
) | |
} | |
} | |
/* validates inputs and outputs error messages to UI when invalid */ | |
export const Validations = { | |
required: (values, errors, inputNames) => { | |
// list the req'd fields by input name, ie: they aren't allowed to be empty | |
let required = inputNames | |
required.map((e, i) => { | |
if (!values[e]) { | |
errors[e] = 'required' | |
} | |
}) | |
}, | |
email: (values, errors) => { | |
if (!values.email) { | |
errors.email = 'Required' | |
} else if (!validator.isEmail(values.email)) { | |
errors.email = 'Invalid email address!' | |
} | |
} | |
} | |
/* Normalizers will modify data BEFORE it is put into the redux store */ | |
export const Normalizers = { | |
email: (val) => { | |
return val.toLowerCase() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
p.input_error { | |
color: #F00!important; | |
display: inline-block; | |
padding-right: 25px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment