Dynamically change value in Formik when a state changes - reactjs

I need to change the value in my form fields every time a state (called selectedProduct, its input field is not included in Formik tag) changes.
I tried:
putting the value I want in the initialValues (does not work obviously because at the first render I haven't choosen my selectedProduct yet)
putting the value I want in the "value" props in every field in formik.
It almost work: Datepicker gets the right value, select box input does not get any value(idk why), text field take the right value but this is covered by the label. And that's because none of the fields get validated.
This is what I've done, with the two points above applied
import React, { useState } from 'react';
import * as Yup from 'yup';
import {
Formik, Form, ErrorMessage, Field,
} from 'formik';
import {
Button, TextField,
MenuItem,
} from '#material-ui/core';
import DateFnsUtils from '#date-io/date-fns';
import {
MuiPickersUtilsProvider,
KeyboardDatePicker,
} from '#material-ui/pickers';
const validationSchema = Yup.object().shape({
startValidityDate: Yup.date().required(),
discount: Yup.string().required(),
days: Yup.string().required(),
});
const MyComponent = ({filteredProduct, daysList}) => {
const [selectedProduct, setSelectedProduct] = useState('');
const onChangeProduct = (product) => {
setSelectedProduct(product.target.value);
};
const handleRunButton = (newExecution) => {
console.log(newExecution);
};
return (
<div>
<div className={classes.productComboWrapper}>
<div id="selectBoxNotIncludedInFormik">
<TextField
margin="normal"
style={{}}
variant="outlined"
name="productId"
id="productId"
fullWidth
select
label="Select product"
value={selectedProduct?.id}
onChange={(product) => onChangeProduct(product)}
>
<MenuItem key="" value="">
{StringsConst.noneSelected}
</MenuItem>
{filteredProduct?.map((el) => (
<MenuItem key={el} value={el}>
{el.isin}
</MenuItem>
))}
</TextField>
</div>
</div>
<Formik
initialValues={{
startValidityDate: selectedProduct?.startValidityDate,
days: selectedProduct?.coupon?.days,
discount: selectedProduct?.discount,
}}
validationSchema={validationSchema}
onSubmit={(values) => {
const newExecution = {
startValidityDate: values.startValidityDate,
days: values.days,
discount: values.discount,
};
handleRunButton(newExecution);
}}
>
{({
errors, dirty, setFieldValue, values,
}) => (
<Form>
<div className={classes.datePicker}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
disableToolbar
label="Start Validity Date"
inputVariant="outlined"
variant="inline"
autoOk
fullWidth
disabled
format="dd/MM/yyyy"
value={selectedProduct?.startValidityDate}
onChange={(dt) => setFieldValue('startValidityDate', dt)}
KeyboardButtonProps={{
'aria-label': 'change date',
}}
/>
</MuiPickersUtilsProvider>
</div>
<div className={classes.fieldWrapper}>
<Field
className={classes.field}
name="discount"
as={TextField}
variant="outlined"
margin="normal"
fullWidth
id="discount"
autoComplete="discount"
placeholder="Discount"
disabled
value={selectedProduct?.discount}
/>
</div>
<div className={classes.textFieldWrapper}>
<TextField
margin="normal"
style={{}}
variant="outlined"
name="days"
id="days"
fullWidth
select
label="Days"
disabled
value={selectedProduct?.coupon?.days}
onChange={(val) => setFieldValue('days', val.target.value)}
>
<MenuItem key="" value="">
{StringsConst.noneSelected}
</MenuItem>
{daysList.map((el) => (
<MenuItem key={el} value={el}>
{el}
</MenuItem>
))}
</TextField>
</div>
<div className={classes.buttonContainer}>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
{StringsConst.Run}
</Button>
</div>
</Form>
)}
</Formik>
</div>
)
}
So, the three input fields in the form are disabled, but I need them to fill when I choose a value in the first select box outside of the form.
Can you suggest me another approach?

You can connect input or button to the form outside the form.
like this code:
<form id="myForm">
<button> click me </button>
</form>
<input type="text" form="myForm"/>
ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefform

Related

Using React Hook Form with other component

I want to create dynamic form with react-hook-form.
Below is my code.
I want to create a dialog for entering a detailed profile in a separate component and use it in MainForm.
I can display the DetailForm, but the values entered in it are not reflected.
Data on the DetailForm component is not included when submitting.
Any guidance on how to do this would be greatly appreciated.
MainForm
import React from 'react';
import {
useForm,
Controller,
useFieldArray
} from 'react-hook-form';
import {
Button,
TextField,
List,
ListItem,
IconButton,
} from '#material-ui/core';
import DetailForm from '#components/DetailForm'
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
function MainForm(props:any) {
const { control, handleSubmit, getValues } = useForm({
mode: 'onBlur',
defaultValues: {
profiles: [
{
firstName: '',
lastName: '',
email: '',
phone: ''
}
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'profiles',
});
const onSubmit = () => {
const data = getValues();
console.log('data: ', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<List>
fields.map((item, index) => {
return (
<ListItem>
<Controller
name={`profiles.${index}.firstName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="First Name"
/>
}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="Last Name"
/>
}
/>
<DetailForm index={index} />
</ListItem>
)
})
</List>
<IconButton onClick={() => append({})}>
<AddCircleOutlineIcon />
</IconButton>
<Button
type='submit'
>
SAVE
</Button>
</form>
)
}
DetailForm
import React from 'react';
import {
Button,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '#material-ui/core';
export default function DetailForm(props: any) {
const [dialogState, setDialogState] = React.useState<boolean>(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button
onClick={handleOpen}
>
Set Detail Profile
</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<TextField
name={`profiles.${props.index}.email`}
label="Email Address"
/>
<TextField
name={`profiles.${props.index}.phone`}
label="Phone Number"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleClose}>
Add
</Button>
</DialogActions>
</Dialog>
</>
)
}
You have to register those fields inside your <Dialog /> component - just pass control to it. It's also important to set an inline defaultValue when using <Controller /> and useFieldArray. From the docs:
inline defaultValue is required when working with useFieldArray by integrating with the value from fields object.
One other minor thing: You should also pass the complete fieldId to your <Dialog /> component instead of just passing the index. This way it would be easier to change the fieldId in one place instead of editing all fields in your <Dialog /> component.
MainForm
<ListItem key={item.id}>
<Controller
name={`profiles.${index}.firstName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="First Name" />
)}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Last Name" />
)}
/>
<DetailForm control={control} fieldId={`profiles.${index}`} />
</ListItem>
DetailForm
export default function DetailForm({ control, fieldId }) {
const [dialogState, setDialogState] = React.useState(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button onClick={handleOpen}>Set Detail Profile</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<Controller
name={`${fieldId}.email`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Email Address" />
)}
/>
<Controller
name={`${fieldId}.phone`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Phone Number" />
)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>Add</Button>
</DialogActions>
</Dialog>
</>
);
}

Fetching the default value of Material UI Date Picker when used with React Hook Forms

I am building a form using Material UI which contains 3 fields
(1) Date (2) Hours (3) Comments. By default, the MUI Date picker field is defaulting to today's date. so when I populate the remaining fields (i.e. hours and comment) and submit the form, I am not seeing the Date value in my DATA object. instead, I have to explicitly select a calendar date and submit then the DATA object is populating with the date picker value.
this is causing an issue for me while submitting the data to database over a API call. anyone faced this type of issue..? how can i capture the date value properly in this scenario.
below is the code
import React, { useState } from "react";
import Grid from "#material-ui/core/Grid";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import DateFnsUtils from "#date-io/date-fns";
import {
MuiPickersUtilsProvider,
KeyboardTimePicker,
KeyboardDatePicker,
} from "#material-ui/pickers";
import { useForm, Controller } from "react-hook-form";
import AddTime from "./AddTime";
export default function Form({ user }) {
const { handleSubmit, control, reset } = useForm({});
const onSubmit = (data) => console.log(data);
return (
<React.Fragment>
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Controller
name="date"
control={control}
render={({ field: { ref, ...rest } }) => (
<KeyboardDatePicker
margin="none"
id="date-picker-dialog"
label="Select date"
format="MM/dd/yyyy"
KeyboardButtonProps={{
"aria-label": "change date",
}}
{...rest}
/>
)}
/>
</MuiPickersUtilsProvider>
</Grid>
<Grid item xs={12} sm={6}>
<Controller
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="Enter Hours"
variant="standard"
size="small"
error={!!error}
helperText={error ? error.message : null}
/>
)}
name="hours"
control={control}
rules={{ required: "Enter Hours" }}
/>
</Grid>
<Grid item xs={12}>
<Controller
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="Enter Comments if any"
variant="standard"
size="small"
multiline
fullWidth
error={!!error}
helperText={error ? error.message : null}
/>
)}
name="comment"
rules={{ required: "Enter Comments" }}
control={control}
/>
</Grid>
<Grid item xs={12}>
<Button
variant="contained"
color="primary"
type="submit"
size="small"
>
submit
</Button>
<br />
<br />
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {
reset({
date: new Date(),
hours: "",
comment: "",
});
}}
>
Reset Form
</Button>
</Grid>
</Grid>{" "}
</form>
</React.Fragment>
);
}
Below is the screenshot of the console log. when I click on submit, date is showing as undefined
when I manually change the date and submit, then it's populating fine.
Thanks In Advance -
Venkata Penumatsa
The problem is you're not setting a defaultValue for your <Controller /> components. This is required for external controlled components like Material UI components.
You need to either provide defaultValue at the field-level or useForm with defaultValues.
Here is the relevant section in the documentation.

How to validate multiple emails using one validation schema field in Formik?

I have a customer form which takes two values.
Customer name
Customer Emails (Which can be multiples)
I've given an add button besides the email field through which user can add more emails to the form. now i want to validate each email that's been added. also if it's added it is required too. empty email is not allowed.
The question is i have only one validation schema to validate email field. how can i use the same field to validate multiple emails?
Even after adding the correct email. it still gives error !
Please see the sandbox link to see the code. code sandbox link
Here is the working code with multiple email validation and errors.
I've used Formik FieldArray to handle multiple emails.
You can replace your code with this in your sandbox to test.
import React from "react";
import { TextField, IconButton } from "#material-ui/core";
import {
AddCircleOutlined as AddCircleOutlinedIcon,
IndeterminateCheckBox as IndeterminateCheckBoxIcon
} from "#material-ui/icons";
//FORMIK
import { Formik, FieldArray, getIn, ErrorMessage } from "formik";
import * as Yup from "yup";
export default function UnitsDrawer(props) {
const callAPI = e => {
console.log(e.name);
console.log(e.email);
};
const testSchema = Yup.object().shape({
name: Yup.string().required("Customer name is required"),
email: Yup.array().of(
Yup.string()
.email("Enter a valid email")
.required("Email is required")
)
});
const initialValues = {
name: "",
email: [""]
};
const formRef = React.useRef();
return (
<div>
<Formik
innerRef={formRef}
validationSchema={testSchema}
initialValues={initialValues}
onSubmit={(values, actions) => {
actions.setSubmitting(false);
callAPI(values);
}}
>
{({
handleChange,
handleBlur,
values,
errors,
touched,
handleSubmit,
isSubmitting,
}) => {
return (
<>
<div>
<TextField
label="Customer Name"
name="name"
margin="normal"
variant="outlined"
error={errors.name && touched.name}
onChange={handleChange}
onBlur={handleBlur}
value={values.name}
fullWidth
/>
<ErrorMessage name="name" component="div" />
</div>
<FieldArray name="email">
{({ push, remove }) =>
values.email.map((field, index) => (
<div key={`${index}`} className="dynamic-fields">
<div>
<TextField
label="Email"
variant="outlined"
className="input-item"
error={
getIn(touched, `email.${index}`) &&
getIn(errors, `email.${index}`)
}
name={`email.${index}`}
value={values.email[index]}
onChange={handleChange}
onBlur={handleBlur}
fullWidth
/>
<ErrorMessage name={`email.${index}`} component="div" />
</div>
<IconButton
aria-label="filter list"
className="add-icon"
onClick={() => {
push("");
}}
>
<AddCircleOutlinedIcon color="primary" />
</IconButton>
{values.email.length > 1 && (
<IconButton
aria-label="filter list"
className="add-icon"
onClick={() => {
remove(index);
}}
>
<IndeterminateCheckBoxIcon />
</IconButton>
)}
</div>
))
}
</FieldArray>
</>
);
}}
</Formik>
</div>
);
}

React - Material-UI - How to use Select with multiple values inside react-hook-form

I am trying to use UI-Material Select with multiple options inside react-hook-form without success.
I had this working before trying to do it with multiple options
<form onSubmit={handleSubmit(onSubmit)}>
<Row className="mb-2">
<Col sm={6}>
<FormControl className="select-language">
<InputLabel> {t('Languages')}</InputLabel>
<Controller
as={
<Select>
{Config.languages.map(m => <MenuItem key={m.code} value={m.text}> {t(m.text)} </MenuItem>)}
</Select>
}
defaultValue={user.language}
name="language"
control={control}
>
</Controller>
</FormControl>
</Col>
</Row>
</form>
I tried to add multiple to the Select element which lead me to another error.
I tried also to keep only the Select element without the Controller wrapper, but then I can't get the language value in onSubmit
Very simple codeSandBox that shows that I can't get value from Select when submitting the form: https://codesandbox.io/s/react-hook-form-example-s7h5p?file=/src/index.js
I would appreciate any help
Thanks
If anyone looking for a easy solution, this might come in handy. Multiple select options become very easy now with Select Component. If you look at the Select component, you just have to set the default value to an array and pass the "multiple" prop.
import {
Button,
FormControl,
InputLabel,
MenuItem,
Select
} from "#mui/material";
import { Controller, useForm } from "react-hook-form";
const FCWidth = {
width: "20rem"
};
export default function App() {
const { control, handleSubmit } = useForm();
const formSubmitHandler = (formData) => {
console.log(formData);
};
const ages = ["10", "20", "30"];
return (
<div className="App">
<form onSubmit={handleSubmit(formSubmitHandler)}>
<Controller
name="age"
control={control}
type="text"
defaultValue={[]}
render={({ field }) => (
<FormControl sx={FCWidth}>
<InputLabel id="age">Age</InputLabel>
<Select
{...field}
labelId="age"
label="age"
multiple
defaultValue={[]}
>
{ages.map((age) => (
<MenuItem value={age} key={age}>
{age}
</MenuItem>
))}
</Select>
</FormControl>
)}
/>
<FormControl sx={FCWidth}>
<Button
type="submit"
variant="contained"
fullWidth
sx={{ marginTop: ".75rem", fontWeight: "bold" }}
>
Submit
</Button>
</FormControl>
</form>
</div>
);
}
Here is the code sandbox link https://codesandbox.io/s/select-multiple-option-with-mui-and-react-hook-form-2kv2o
I know that this old but someone may need it later, maybe this one would work for you,
just check the render Controller props
import React from "react";
import {
TextField,
ListItemText,
Checkbox,
MenuItem,
makeStyles
} from "#material-ui/core";
import { useForm, Controller } from "react-hook-form";
const useStyles = makeStyles({
root: {
width: "200px"
},
center: {
textAlign: "center"
}
});
export default function SelectTextField() {
const classes = useStyles();
const { handleSubmit, control } = useForm();
const nums = [1, 2, 3, 4];
const onSubmit = (e) => {
console.log(e);
};
return (
<form className={classes.center} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="selectedNumbers"
defaultValue={[]}
render={({ onChange, value }) => {
console.log(value);
return (
<TextField
classes={{ root: classes.root }}
select
id="Numbers"
variant="outlined"
label="Numbers"
SelectProps={{
multiple: true,
value: value,
renderValue: (selected) => selected.join(", "),
onChange: onChange
}}
>
{nums.map((n) => (
<MenuItem key={n} value={n}>
<Checkbox checked={value.includes(n)} />
<ListItemText primary={n} />
</MenuItem>
))}
</TextField>
);
}}
/>
<div>
<button type="submit">Submit</button>
</div>
</form>
);
}
also, you can and play with it here
https://codesandbox.io/s/multi-select-input-u0cr3?file=/demo.js
also can check this on the docs
https://react-hook-form.com/get-started#ReactNative
First of all, i think your version is on v3, so you may want to upgrade:
https://codesandbox.io/s/react-hook-form-example-5lrij?file=/src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { useForm, Controller } from "react-hook-form";
import { Select, MenuItem } from "#material-ui/core";
import "./styles.css";
const languages = [
{
code: "en-US",
text: "English"
},
{
code: "zu",
text: "Another Language"
}
];
export default function App() {
const { register, handleSubmit, control } = useForm();
const onSubmit = data => console.log(data);
return (
<div className="container">
<div className="col-sm-12">
<h3>Client Profile</h3>
</div>
<div className="col-sm-12">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-group">
<input
className="form-control"
type="text"
placeholder="Name"
name="Name"
ref={register}
/>
</div>
<div className="form-group">
<Controller
name="Language"
control={control}
as={
<Select>
{languages.map(m => (
<MenuItem key={m.code} value={m.text}>
{" "}
{m.text}{" "}
</MenuItem>
))}
</Select>
}
defaultValue={languages[0].text}
/>
</div>
<div className="form-group">
<input className="btn btn-primary" type="submit" />
</div>
</form>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
here is the fixed version by using Controller

How to use custom radio component with react-final-form?

I am trying to use a custom Radio component with React-final-form but it is not acting as a radio button but as a checkbox, ie, all the buttons are open for selection.
The 3rd party Radio button has the following schema:
checked boolean
Whether or not radio is checked
onChange () => void
Called when the user attempts to change the checked state
name string
The input name, used to reference the element in JavaScript
I created a custom Component for using the Radio Component:
const CustomRadio = (props: any) => (
<Radio
{...props.input}
{...props.rest}
name={props.name}
onChange={() => props.input.onChange()}
/>
)
and I am using it as follows:
<Field name="food"
component={CustomRadio}
value="1"
/>
<Field name="food"
component={CustomRadio}
value="2"
/>
Being very new to RFF and new to React, I may be doing something very wrong, hence any help will be appreciated.
Basically, I want to use RFF with my 3rd party components. Though I have been successful to use my Input component with RFF as expected, Radio Button is the one creating problems.
Here is the correct implementation for Radio with react-final-form:-
https://codesandbox.io/s/vibrant-easley-5n1ek?file=/index.js
/* eslint-disable jsx-a11y/accessible-emoji */
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import RadioGroup from "#material-ui/core/RadioGroup";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import FormControl from "#material-ui/core/FormControl";
import Radio from "#material-ui/core/Radio";
import FormLabel from "#material-ui/core/FormLabel";
const RadioWrapper = (props) => {
const {
input: { checked, value, name, onChange, ...restInput },
} = props;
return (
<Radio
name={name}
inputProps={restInput}
onChange={onChange}
checked={checked}
value={value}
/>
);
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => {
return (
<Styles>
<h1>React Final Form - Simple Example</h1>
<a
href="https://final-form.org/react"
target="_blank"
rel="noopener noreferrer"
>
Read Docs
</a>
<Form
onSubmit={onSubmit}
initialValues={{
employed: false,
all_sub_tenants: "true"
}}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}>
<FormControl component="fieldset">
<FormLabel component="legend">
Select Tenants
</FormLabel>
<RadioGroup aria-label="allSubTenants" name="allSubTenants">
<FormControlLabel
value="true"
control={
<Field
name="all_sub_tenants"
component={RadioWrapper}
type="radio"
value={"true"}
/>
}
label="All Sub-Tenants"
/>
<FormControlLabel
value="false"
control={
<Field
name="all_sub_tenants"
component={RadioWrapper}
type="radio"
value={"false"}
/>
}
label="Select Sub-Tenants"
/>
</RadioGroup>
</FormControl>
<div>
<label>Notes</label>
<Field name="notes" component="textarea" placeholder="Notes" />
</div>
<div className="buttons">
<button type="submit" disabled={submitting || pristine}>
Submit
</button>
<button
type="button"
onClick={form.reset}
disabled={submitting || pristine}
>
Reset
</button>
</div>
<pre>{JSON.stringify(values, 0, 2)}</pre>
</form>
)}
/>
</Styles>
);
};
render(<App />, document.getElementById("root"));

Resources