Skip to content

Instantly share code, notes, and snippets.

@ccurtin
Last active March 4, 2019 03:50
Show Gist options
  • Save ccurtin/6486ad7ccc06bf632e74132c6a01e45e to your computer and use it in GitHub Desktop.
Save ccurtin/6486ad7ccc06bf632e74132c6a01e45e to your computer and use it in GitHub Desktop.
Redux-Form Material-UI v1 Example
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' }} />
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
/*
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)
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()
}
}
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