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

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

Related

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) => ....)

Passing a 'label' prop into Formik <Field /> when using alongside Material UI <TextField />

I'm building a form using Formik and Material UI.
I'm leveraging the Formik component the following way :
My Input component :
const Input = ({ field, form: { errors } }) => {
const errorMessage = getIn(errors, field.name);
return <TextField {...field} />;
};
And down into my rendered form, here's how I do it :
<Field
component={Input}
name={`patients[${index}].firstName`}
/>
The problem is that Material UI uses a label prop to display label on an input field so the label should be a prop passed to .
It works if I "hard-code" it into my "Input" component which defeats the purpose of using a reusable comp.
So that works but inconvenient :
const Input = ({ field, form: { errors } }) => {
console.log(field.label);
const errorMessage = getIn(errors, field.name);
return <TextField {...field} label="first name" />;
};
What I hoped for was using it one level above such as :
<Field
component={Input}
name={`patients[${index}].firstName`}
label="first name"
/>
but the above doesn't work as "label" is not recognised as a prop by Formik (or that's how I understand it but I might be wrong).
Has anyone come across that issue ?
I know I could use my "name" value as a label but it's not great UX as it would leave me with a label such as "patients[0].firstName" aha
Here is a good solution, that is essentially yours, but you can provide anything, not only label.
Create the field and put the label on the like so:
<Field name={`patients[${index}].firstName`} label='First Name' component={MuiTextFieldFormik} />
The trick is to use the spread operator, then the custom component becomes:
import React from 'react';
import { TextField } from "#material-ui/core"
import { get } from "lodash"
export const MuiTextFieldFormik = ({ field, form: { touched, errors }, ...props }) => {
const error = get(touched, field.name) && !!get(errors, field.name)
const helperText = get(touched, field.name) && get(errors, field.name)
return (
<TextField fullWidth variant="outlined" {...field} {...props} error={error} helperText={helperText} />
)
}
disappointing that their docs do not have such simple example
Ok so I think I found the solution. The way I was destructing my arguments, I was only passing field and form which are holding most of the data from so passing a label prop this way fixes that:
const Input = ({ field, label, form: { errors } }) => {
const errorMessage = getIn(errors, field.name);
return <TextField {...field} label={label} />;
};
Then when I use the Formik component this way, the correct label gets passed :
<Field
label="first name"
component={Input}
name={`patients[${index}].firstName`}
/>

How can I improve the react performance if I have many text inputs (Hooks) in a form?

The Problem
I have a form to send data through a api rest in React, the render and the writing on the form is very slow when I have about 80 text fields.
I'm using functional components with hooks to handle the input texts and Material-UI as UI framework.
In a first try, I had a currying function to handle the values:
setValue = (setter) => (e) => { setter(e.target.value) }
But the render process was really slow (because I was creating a function in every render), So I send the setter function as a prop, then it improves a little but not enough.
Actually the input response when I write a key in any input, it's about 500 ms.
What can I do to get a better performance?
The code was simplified for understanding purposes.
Sample code below:
const [input1, setInput1] = useState('')
const [input2, setInput2] = useState('')
const [input3, setInput3] = useState('')
.
.
.
const [input80, setInput80] = useState('')
// render the Inputs
<Input value={input1} setter={setInput1} text="Some label text" />
<Input value={input2} setter={setInput2} text="Some label text" />
<Input value={input3} setter={setInput3} text="Some label text" />
.
.
.
<Input value={input80} setter={setInput80} text="Some label text" />
My Input components:
const Input = ({
value, setter, text, type = 'text'
}) => {
const handleChange = (e) => {
const { value: inputValue } = e.target
setter(inputValue)
}
return (
<Grid>
<TextField
fullWidth
type={type}
label={text}
value={value}
onChange={handleChange}
multiline={multiline}
/>
</Grid>
)
}
All input values are must be in a component because I'm need to send them to a server with axios.
It looks like the Material-UI Input component is a bit heavy.
I have a sample codesandbox here where I initialised around 1000 inputs. Initially it lagged and crashed.
To begin with I added a memo to the Input component. This memoizes all the Input components, triggering a new render only if one of its props has changed.
For a start just add a memo to your input component.
import React, { memo } from 'react';
const Input = memo(({
value, setter, text, type = 'text'
}) => {
const handleChange = (e) => {
const { value: inputValue } = e.target
setter(inputValue)
}
return (
<Grid>
<TextField
fullWidth
type={type}
label={text}
value={value}
onChange={handleChange}
multiline={multiline}
/>
</Grid>
)
})
Note: If you have a custom setter like in your first case setValue = (setter) => (e) => { setter(e.target.value) }, you can wrap that in a useCallback to prevent multiple functions to be created for every render.

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.

Material ui textfield ith redux form issue

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

Categories

Resources