Material ui textfield ith redux form issue - javascript

i'm having an issue here which i can't handle
I have a textfield which i made compatible with redux form
like this:
const renderTextField = props => (
<TextField {...props} />
);
and i'm using it like this:
<Field
id="searchCif"
name="searchCif"
component={renderTextField}
floatingLabelText={SEARCHVIEW_HINT_CIF}
floatingLabelFixed={false}
value
/>
Then i writing this in my container:
import { reduxForm } from 'redux-form/immutable';
import { connect } from 'react-redux';
// import { injectIntl } from 'react-intl';
import SearchDefaultView from './views/searchDefaultView';
import { requestCustomerInfo } from './actions/customerActions';
export const mapDispatchToProps = dispatch => (
{
requestCustomerInfo: formData =>
dispatch(requestCustomerInfo(formData))
}
);
const SearchDefaultReduxForm = reduxForm({
form: 'customerInfo', // a unique identifier for this form
})(SearchDefaultView);
const SearchDefaultContainer = connect(
null,
mapDispatchToProps
)(SearchDefaultReduxForm);
export default SearchDefaultContainer;
But when i'm writing the value and submit my form the form has NO VALUES. what am i missing?
From the dicumentation i used this:
const renderTextField = ({
input,
label,
meta: { touched, error },
...custom
}) =>
<TextField
hintText={label}
floatingLabelText={label}
errorText={touched && error}
{...input}
{...custom}
/>
const SearchDefaultView = (props) => {
const { requestCustomerInfo, handleSubmit } = props;
return (
<form onSubmit={handleSubmit(requestCustomerInfo)}>
<Menu
autoWidth={false}
style={styleSearchMenu}
>
<Divider />
<Field
id="searchCif"
name="searchCif"
component={renderTextField}
floatingLabelText={SEARCHVIEW_HINT_CIF}
floatingLabelFixed={false}
/>
<br />
<Field
id="searchAFM"
name="searchAFM"
component={renderTextField}
floatingLabelText={SEARCHVIEW_HINT_AFM}
floatingLabelFixed={false}
/>
<br />
<RaisedButton
type="submit"
fullWidth
primary
label={SEARCHVIEW_SEARCH}
/>
</Menu>
</form>
);
};
But it is showing me an error in compilation at ...custom

When you want to use a custom field for Redux-Form, Redux-form gives you access to both props like onChange etc, but also other meta-data (like if the form has been touched or not). These different kinds of props are grouped depending on type. The interesting part for you is that all the attributes associated with a normal input element (like onChange, value, type) are grouped in props.input. So to pass those down to the <TextField /> component you can't use the spread operator (... ) on props directly. You must use it on props.input.
const renderTextField = props => (
<TextField {...props.input} />
);
You may also have to deal with the fact that the onChange method that <TextField /> expects doesn't necessarily have the same signature as the onChange method that Redux-form provides you. So you may have to do some manual work to make them work together, similar to what I've outlined in this post. You'd have to read up on the documentation of both the onChange of Redux-Form and Material-UI TextField respectively.
You may also be interested to know that for material-ui components, there actually already exists a library that has done that manual work for you: redux-form-material-ui.

I think you are not using the onChange prop of the component.
onChange: Callback function that is fired when the textfield's value changes.
You should dispatch the change and update the data in redux container.
http://www.material-ui.com/#/components/text-field

Related

How to auto tab between Formik Field?

I have a component for 4 digit code of phone validation. By itself it works fine and looks good as well. The only issue I am facing - I can't autotab between numbers. I have to go to each input manually and write the number. Is it possible to do with Formik Field?
This is my piece of code:
<Formik
onSubmit={values =>
VerifyGarageFunc({ code: values.code.join(''), requestId: PhoneCodeData.data }, data.showModal)
}>
{({ values, handleChange, handleSubmit }) => (
<form onSubmit={handleSubmit}>
<FieldArray
name="code"
render={arrayHelpers => (
<div className={styles.inputWrapper}>
{values.code.map((item, index) => (
<div key={index}>
<Field
name={`code.${index}`}
type="text"
component={CustomInput}
onChange={handleChange}
value={values.code[index]}
/>
</div>
))}
</div>
)}
/>
<LoginActionButton onSubmit={handleSubmit} text={'Send'} />
<FieldArray />
</form>
)}
</Formik>
I tried https://www.npmjs.com/package/react-auto-tab but it works only with <input/>, for some reason it doesn't work at all with Formik Field.
P.S. I am using Next.js with React.js
You'll likely have more luck with a hook based solution. Install https://github.com/Romr1ch/react-pin-input-hook, which just does the logic without being opinionated about display.
Create a new component called PinInput and create a new field using the formik hook primitives.
Ive setup an example codesandbox: https://codesandbox.io/s/react-pin-input-hook-custom-input-1ze5dv?file=/src/App.js, but note this doesn't use your exact components as I don't have them. The below code should match closer your exact case.
import React from 'react'
import { useField } from 'formik'
import { usePinInput } from 'react-pin-input-hook'
export const PinInput = (props) => {
const [field, meta, helpers] = useField(props)
const { fields } = usePinInput({
values: field.value,
onChange: (values) => {
helpers.setValue(values)
},
})
return fields.map((fieldProps, index) =>
<CustomInput key={index} type="text" {...fieldProps} />
)
}
Then in your main file do this (by the way if you use Form component from Formik you dont need to do any of the onSubmit binding yourself, so I changed that along the way -- the button can just be "submit" type):
<Formik
onSubmit={values =>
VerifyGarageFunc({ code: values.code.join(''), requestId: PhoneCodeData.data }, data.showModal)
}>
<Form>
<PinInput name="code" />
<LoginActionButton type="submit" text={'Send'} />
</Form>
</Formik>
Note that this lib requires your CustomComponent to attach a ref to the underlying thing that needs focusing so you'll need to use forwardRef on that component and attach it to the underlying input. It also needs to support onBlur, onFocus, onChange and onKeyDown.

Formik: target is undefined when passing Material UI Date Picker as prop to React Component

I'm trying to create a reusable component. where I am passing the form fields as prop. when I click on datepicker field. I'm getting this error:
TypeError: target is undefined
How can I fix this?
Here's how my input component looks like:
import { useField } from 'formik';
import { DatePicker } from '#material-ui/pickers';
export const DatePickerField = ({ label, ...props }) => {
const [field, meta] = useField(props);
return (
<DatePicker label={label} fullWidth {...field} {...props} />
);
};
Here's how the reusable component looks like:
import { Form } from 'formik'
export const ReusableComp = ({ fields }) => (
<Form noValidate>
{fields}
</Form>
)
Here's where I am using this component:
export const App = () => (
<ReusableComp fields={
<div className='mb-3'>
<DateTimePickerField
label='Start DateTime'
name='start_date_time'
/>
</div>
} />
)
Result of console.log(fields)
In my case I was using the Material UI DesktopDatePicker (with Typescript), and defining the onChange prop like this solved it:
onChange={(value): void => {
formik.setFieldValue("dueDate", value);
}}
You have to replace "dueDate" with your formik value name.
After lots of research and thanks #Rosen Tsankov for pointing my attention to the onChange function.
I have seen questions about the same error on SO which are not answered. so this may help them and anyone in future facing this error.
As #Rosen Tsankov have said the material ui DatePicker component returns the date value as the first argument of the onChange function.
the field returned from useField have the following: name, value, onChange, onBlur.
the onChange function returned from useField expects the first argument to be an event which in this case is the date value. that's why we get the error:
TypeError: target is undefined
Because formik is trying to access target property and date has no property target. something like this date.target and this is undefined
so here's how I have fixed it. instead of spreading the field. I have added name and value from field to DatePicker. then I have used the setFieldValue from formik to manually update the input value like so.
import { DatePicker } from '#material-ui/pickers';
import { useField, useFormikContext } from 'formik';
export const DatePickerField = ({ label, ...props }) => {
const [field, meta] = useField(props);
const { setFieldValue } = useFormikContext();
return (
<DatePicker
fullWidth
{...props}
label={label}
name={field.name}
value={field.value}
helperText={meta.error}
error={meta.touched && Boolean(meta.error)}
onChange={(value) => setFieldValue(field.name, value)}
/>
);
};
DatePicker expects onChange((date) => ...) but you are passing formik handler wich expects onChange((event) => ....)

Material UI + React Form Hook + multiple checkboxes + default selected

I am trying to build a form that accommodates multiple 'grouped' checkboxes using react-form-hook Material UI.
The checkboxes are created async from an HTTP Request.
I want to provide an array of the objects IDs as the default values:
defaultValues: { boat_ids: trip?.boats.map(boat => boat.id.toString()) || [] }
Also, when I select or deselect a checkbox, I want to add/remove the ID of the object to the values of react-hook-form.
ie. (boat_ids: [25, 29, 4])
How can I achieve that?
Here is a sample that I am trying to reproduce the issue.
Bonus point, validation of minimum selected checkboxes using Yup
boat_ids: Yup.array() .min(2, "")
I've been struggling with this as well, here is what worked for me.
Updated solution for react-hook-form v6, it can also be done without useState(sandbox link below):
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const defaultNames = ["bill", "Manos"];
const { control, handleSubmit } = useForm({
defaultValues: { names: defaultNames }
});
const [checkedValues, setCheckedValues] = useState(defaultNames);
function handleSelect(checkedName) {
const newNames = checkedValues?.includes(checkedName)
? checkedValues?.filter(name => name !== checkedName)
: [...(checkedValues ?? []), checkedName];
setCheckedValues(newNames);
return newNames;
}
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{["bill", "luo", "Manos", "user120242"].map(name => (
<FormControlLabel
control={
<Controller
name="names"
render={({ onChange: onCheckChange }) => {
return (
<Checkbox
checked={checkedValues.includes(name)}
onChange={() => onCheckChange(handleSelect(name))}
/>
);
}}
control={control}
/>
}
key={name}
label={name}
/>
))}
<button>Submit</button>
</form>
);
}
Codesandbox link: https://codesandbox.io/s/material-demo-54nvi?file=/demo.js
Another solution with default selected items done without useState:
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
Breaking API changes made in 6.X:
validation option has been changed to use a resolver function wrapper and a different configuration property name
Note: Docs were just fixed for validationResolver->resolver, and code examples for validation in repo haven't been updated yet (still uses validationSchema for tests). It feels as if they aren't sure what they want to do with the code there, and it is in a state of limbo. I would avoid their Controller entirely until it settles down, or use Controller as a thin wrapper for your own form Controller HOC, which appears to be the direction they want to go in.
see official sandbox demo and the unexpected behavior of "false" value as a string of the Checkbox for reference
import { yupResolver } from "#hookform/resolvers";
const { register, handleSubmit, control, getValues, setValue } = useForm({
resolver: yupResolver(schema),
defaultValues: Object.fromEntries(
boats.map((boat, i) => [
`boat_ids[${i}]`,
preselectedBoats.some(p => p.id === boats[i].id)
])
)
});
Controller no longer handles Checkbox natively (type="checkbox"), or to better put it, handles values incorrectly. It does not detect boolean values for checkboxes, and tries to cast it to a string value. You have a few choices:
Don't use Controller. Use uncontrolled inputs
Use the new render prop to use a custom render function for your Checkbox and add a setValue hook
Use Controller like a form controller HOC and control all the inputs manually
Examples avoiding the use of Controller:
https://codesandbox.io/s/optimistic-paper-h39lq
https://codesandbox.io/s/silent-mountain-wdiov
Same as first original example but using yupResolver wrapper
Description for 5.X:
Here is a simplified example that doesn't require Controller. Uncontrolled is the recommendation in the docs. It is still recommended that you give each input its own name and transform/filter on the data to remove unchecked values, such as with yup and validatorSchema in the latter example, but for the purpose of your example, using the same name causes the values to be added to an array that fits your requirements.
https://codesandbox.io/s/practical-dijkstra-f1yox
Anyways, the problem is that your defaultValues doesn't match the structure of your checkboxes. It should be {[name]: boolean}, where names as generated is the literal string boat_ids[${boat.id}], until it passes through the uncontrolled form inputs which bunch up the values into one array. eg: form_input1[0] form_input1[1] emits form_input1 == [value1, value2]
https://codesandbox.io/s/determined-paper-qb0lf
Builds defaultValues: { "boat_ids[0]": false, "boat_ids[1]": true ... }
Controller expects boolean values for toggling checkbox values and as the default values it will feed to the checkboxes.
const { register, handleSubmit, control, getValues, setValue } = useForm({
validationSchema: schema,
defaultValues: Object.fromEntries(
preselectedBoats.map(boat => [`boat_ids[${boat.id}]`, true])
)
});
Schema used for the validationSchema, that verifies there are at least 2 chosen as well as transforms the data to the desired schema before sending it to onSubmit. It filters out false values, so you get an array of string ids:
const schema = Yup.object().shape({
boat_ids: Yup.array()
.transform(function(o, obj) {
return Object.keys(obj).filter(k => obj[k]);
})
.min(2, "")
});
Here is a working version:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const { control, handleSubmit } = useForm({
defaultValues: {
bill: "bill",
luo: ""
}
});
return (
<form onSubmit={handleSubmit(e => console.log(e))}>
{["bill", "luo"].map(name => (
<Controller
key={name}
name={name}
as={
<FormControlLabel
control={<Checkbox value={name} />}
label={name}
/>
}
valueName="checked"
type="checkbox"
onChange={([e]) => {
return e.target.checked ? e.target.value : "";
}}
control={control}
/>
))}
<button>Submit</button>
</form>
);
}
codesandbox link: https://codesandbox.io/s/material-demo-65rjy?file=/demo.js:0-932
However, I do not recommend doing so, because Checkbox in material UI probably should return checked (boolean) instead of (value).
Here's my solution, which is not using all the default components from Material UI cause at my interface each radio will have an icon and text, besides the default bullet point not be showed:
const COMPANY = "company";
const INDIVIDUAL = "individual";
const [scope, setScope] = useState(context.scope || COMPANY);
const handleChange = (event) => {
event.preventDefault();
setScope(event.target.value);
};
<Controller
as={
<FormControl component="fieldset">
<RadioGroup
aria-label="scope"
name="scope"
value={scope}
onChange={handleChange}
>
<FormLabel>
{/* Icon from MUI */}
<Business />
<Radio value={COMPANY} />
<Typography variant="body1">Company</Typography>
</FormLabel>
<FormLabel>
{/* Icon from MUI */}
<Personal />
<Radio value={INDIVIDUAL} />
<Typography variant="body1">Individual</Typography>
</FormLabel>
</RadioGroup>
</FormControl>
}
name="scope"
control={methods.control}
/>;
Observation: At this example I use React Hook Form without destruct:
const methods = useForm({...})
This is my solution with react hook form 7, the other solutions don't work with reset or setValue.
<Controller
name={"test"}
control={control}
render={({ field }) => (
<FormControl>
<FormLabel id={"test"}>{"label"}</FormLabel>
<FormGroup>
{items.map((item, index) => {
const value = Object.values(item);
return (
<FormControlLabel
key={index}
control={
<Checkbox
checked={field.value.includes(value[0])}
onChange={() =>
field.onChange(handleSelect(value[0],field.value))
}
size="small"
/>
}
label={value[1]}
/>
);
})}
</FormGroup>
</FormControl>
)}
/>
link to codesandbox: Mui multiple checkbox

Redux Form: Input stays with 'touched: false'

Wanted to validate my inputs and change the CSS depending of the user interaction.
Starting with a required validation method I wrap all my inputs component with a <Field> and pass to validate an array of func. Just required for now.
But for all my fields the value stay the same touched: false and error: "Required". If I touch or add stuff in the input, those values stay the same.
Validation
export const required = value => (value ? undefined : 'Required')
NameInput
import React from 'react';
import { Field } from 'redux-form'
import InputItem from 'Components/InputsUtils/InputItem';
import { required } from 'Components/InputsUtils/Validation';
const NameInput = () => (
<Field
name={item.spec.inputName}
type={item.spec.type}
component={InputItem}
validate={[required]}
props={item}
/>
);
export default NameInput;
InputItem
import React from 'react';
const InputItem = ({ spec, meta: { touched, error } }) => {
const { type, placeholder } = spec;
return (
<input
className="input"
type={type}
placeholder={placeholder}
/>
);
};
export default InputItem;
There are 2 solutions to solve the "touched is always false" issue.
1) Ensure that input.onBlur is called in your component
For an input:
const { input } = this.props
<input {...input} />
For custom form elements without native onBlur:
const { input: { value, onChange, onBlur } } = this.props
const className = 'checkbox' + (value ? ' checked' : '')
<div
className={className}
onClick={() => {
onChange(!value)
onBlur()
}}
/>
2) Declare your form with touchOnChange
const ReduxFormContainer = reduxForm({
form: 'myForm',
touchOnChange: true,
})(MyForm)
The redux-form controls its own props within your <input /> element as long as you use the spread operator to pass those props into your input.
For example, where you are doing const InputItem = ({ spec, meta: { touched, error } }) => ...
Try destructing the input from the Component: const InputItem = ({ input, spec, meta: { touched, error } }) => ...
And where you have your <input ... />, try doing the following:
<input
{...input}
className="input"
type={type}
placeholder={placeholder}
/>
The redux-form captures any onBlur and onChange events and uses its own methods to change the touched state. You just need to pass those along as shown above.
These are what you need: https://redux-form.com/7.1.2/docs/api/field.md/#input-props
Another point to consider:
I passed {...props} to my custom component, however, touched still remained false.
That is because although props contained the input object, my component couldn't
deduce onBlur from it. When explicitly stating <CustomComponent {...this.props} onBlur={this.props.input.onBlur}, it worked as expected.

Redux-form: display a list of errors on top of a page

I want to use Redux-form in a manner that changes input color & displays the actual error on top of the page. How do I access the list of current field errors outside the fields themselves?
You can't get the list of errors from outside of the render function given to the Field component. This is because errors are not stored in the redux store.
EDIT 26/12/2018 :
This answer is taking some age. ReduxForm now stores errors in the Redux store. Take a look to #nicoqh's answer which is using ReduxForm's selectors to get errors in any Redux connected component.
This answer is not totaly obsolete but it's not anymore the cleanest way to achieve this imho.
Solution 1: Use multiple Field for the same value
The first solution is to use multiple instance of Field for the same value. If multiple Field components have the same name and is connected to the same form name, they will all be connected to the same value and the same error handling.
So you can use a Field component and only render the error.
import React from 'react'
import {reduxForm} from 'redux-form'
const renderError = ({input, meta, ...props}) => (
<span {...props} className='error'>Error : {meta.error}</span>
)
const renderInput = ({input, meta, ...props}) => (
<input {...input} {...props} className={meta.error ? 'error' : null} />
)
const FormWithError = ({handleSubmit}) => (
<div>
<div className='errorContainer'>
<Field name='myInput' component={renderError} />
</div>
<form onSubmit={handleSubmit}>
<Field name='myInput' component={renderInput} />
</form>
</div>
)
const validate = (values, props) => {
const errors = {}
/* calculate errors here by appending theim to errors object */
return errors
}
export default reduxForm({form: 'myForm', validate})(FormWithError)
Solution 2: Use the global error prop
A second solution is to use the global error props, but you will have to display the errors from the container component using reduxForm.
Pay attention that this is a total antipatern ! Global error prop is for field independent errors.
import React from 'react'
import {reduxForm} from 'redux-form'
const renderInput = ({input, meta, ...props}) => (
<input {...input} {...props} className={meta.error ? 'error' : null} />
)
const FormWithError = ({handleSubmit, error}) => (
<div>
<div className='errorContainer'>
<span {...props} className='error'>Error : {error}</span>
</div>
<form onSubmit={handleSubmit}>
<Field name='myInput' component={renderInput} />
</form>
</div>
)
const validate = (values, props) => {
const errors = {}
/* calculate errors here by appending theim to errors object */
if(Object.keys(errors) > 0) {
//You can concatenate each error in a string
for(key in errors) errors._error += key + ': ' + errors[key]
//or juste put the errors object in the global error property
errors._error = {...errors}
}
return errors
}
export default reduxForm({form: 'myForm', validate})(FormWithError)
Solution 3: Get errors from the store
You always can get errors from the store by applying your validate function on the value presents in the store. It can be not performant for heavy validation because it run through validation at each render, so it runs twice when a value change and one for nothing if some other props changes. It can also be dificult to do with async validation.
import React from 'react'
import {reduxForm, formValueSelector} from 'redux-form'
import {connect} from 'redux'
const renderErrors = errors => {
const errorNodes = []
for(key in errors) errorNodes.push(<span className='error'>{key}: {errors[key]}</span>)
return errorNodes
}
const renderInput = ({input, meta, ...props}) => (
<input {...input} {...props} className={meta.error ? 'error' : null} />
)
let FormWithError = ({handleSubmit, values, ...otherProps}) => (
<div>
<div className='errorContainer'>
{renderErrors(validate(values, otherProps))}
</div>
<form onSubmit={handleSubmit}>
<Field name='myInput1' component={renderInput} />
<Field name='myInput2' component={renderInput} />
</form>
</div>
)
const validate = (values, props) => {
const errors = {}
/* calculate errors here by appending theim to errors object */
return errors
}
FormWithError = reduxForm({form: 'myForm', validate})(FormWithError)
FormWithError = connect(
state => formValueSelector('myForm')(state, 'myInput1', 'myInput2')
)(FormWithError)
A last solution can be to store the errors in the store by implementing the componentWillReceiveProps and dispatching an action to update a list of error in the store but i don't think it's really a good idea. It's better to keep simple stateless component to render a Field component.
EDIT 26/12/2018 :
This last "solution" wasn't a good one at the time I've posted it. But since componentWillReceiveProps is deprecated in React, it's not a solution at all. Please don't do this in you application. I don't delete this for history in case this answer was linked somewhere.
You can use the state selectors provided by redux-form.
In particular, getFormSubmitErrors will give you the submit validation errors:
import { getFormSubmitErrors } from 'redux-form';
// ...
const MyFormWithErrors = connect(state => ({
submitErrors: getFormSubmitErrors('my-form')(state)
}))(MyForm);
The original, unconnected MyForm component might look like this:
const MyForm = reduxForm({
form: 'my-form',
})(ManageUserProfile);
If you want to display the synchronous validation errors, you can use the getFormSyncErrors selector instead.

Categories

Resources