React Hook Form w/ FormProvider and MUI - Not working - reactjs

I have been trying to abstract my form components for some time now. I took the example of RHF JS and rewrote it in Typescript.
Unfortunately, I do not get any output of the validation results. When I submit the form nothing happens either. My understanding is that I need to pass the FormContext through to the last component.
Can anyone with experience help me?
App.tsx
import { yupResolver } from "#hookform/resolvers/yup";
import { Button, Grid, TextField } from "#mui/material";
import { memo } from "react";
import {
FormProvider,
useForm,
useFormContext,
UseFormReturn,
} from "react-hook-form";
import { object, SchemaOf, string } from "yup";
type FormData = {
name: string;
};
const schema: SchemaOf<FormData> = object().shape({
name: string()
.required("Input is Required")
.matches(/^[a-zA-Z0-9]+$/, "Only alphanummeric"),
});
type TextInputProps = {
methods: UseFormReturn;
name: string;
label: string;
};
// we can use React.memo to prevent re-render except isDirty state changed
const TextFieldMemo = memo(
({ methods, name, label }: TextInputProps) => (
<TextField
label={label}
variant="outlined"
error={!!methods.formState.errors[name]}
helperText={
(methods.formState.errors[name]?.message as unknown as string) ?? ""
}
fullWidth
margin="normal"
{...methods.register(name)}
InputLabelProps={{
shrink: true,
}}
FormHelperTextProps={{
sx: {
position: "absolute",
bottom: "-1.5rem",
},
}}
/>
),
(prevProps, nextProps) =>
prevProps.methods.formState.isDirty ===
nextProps.methods.formState.isDirty &&
prevProps.methods.formState.errors !== nextProps.methods.formState.errors
);
export const TextFieldMemoContainer = () => {
const methods = useFormContext();
return <TextFieldMemo methods={methods} name="name" label="Name" />;
};
export default function App() {
const methods = useForm<FormData>({
resolver: yupResolver(schema),
mode: "onChange",
});
const { isDirty } = methods.formState;
const onSubmit = (form: FormData) => {
console.log(isDirty);
console.log(form);
};
return (
<Grid container>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Grid item>
<TextFieldMemoContainer />
</Grid>
<Grid item>
<Button
type="submit"
disabled={
!methods.formState.isDirty || !methods.formState.isValid
}
>
Submit
</Button>
</Grid>
</form>
</FormProvider>
</Grid>
);
}
Thank you all in advance.
StackBlitz MRE

Related

react-number-format doesn't work with Formik and Mui

I tried to use react-number-format with mui TextField and Formik, when I submit the form I get a required error. Its seems that nested TextFiled with react-number-format can't get the values.
I tried to console log the input but whit react-number-format I get empty string.
//App.jsx
import * as React from "react";
import { Typography, InputAdornment, Grid } from "#mui/material";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { NumericFormat } from "react-number-format";
import axios from "axios";
import { FieldWrapper as TextField } from "./InputTem";
import { BtnTemp as Button } from "./Btn";
import "./styles.css";
const FORM_STATE = {
price: ""
};
const priceProps = {
name: "price",
label: "Price",
InputProps: {
startAdornment: <InputAdornment position="start">€</InputAdornment>
},
required: true
};
const FORM_VALIDATION = Yup.object().shape({
price: Yup.number().required("require")
});
const App = () => {
return (
<Grid container flex justifyContent={"center"}>
<Formik
initialValues={{ ...FORM_STATE }}
validationSchema={FORM_VALIDATION}
onSubmit={(values) => {
console.log(values);
}}
>
<Form>
<Grid
container
spacing={2}
rowGap={1}
padding={3}
maxWidth={1000}
justifyContent={"center"}
>
<Grid item xs={12}>
<Typography
variant="h3"
component="h1"
align="center"
color="#280982"
>
Price Validation Error
</Typography>
</Grid>
<Grid item xs={12}>
<Grid item xs={12}>
<NumericFormat
name="price"
id="price"
thousandSeparator=","
allowNegative={false}
decimalScale={2}
decimalSeparator="."
fixedDecimalScale={true}
customInput={TextField}
{...priceProps}
/>
</Grid>
<Grid container marginTop="1rem">
<Button>submit</Button>
</Grid>
</Grid>
</Grid>
</Form>
</Formik>
</Grid>
);
};
export default App;
// Input.jsx
import * as React from "react";
import TextField from "#mui/material/TextField";
import { useField } from "formik";
export const FieldWrapper = ({ name, ...otherProps }) => {
const [field, meta] = useField(name);
if (meta && meta.touched && meta.error) {
otherProps.error = true;
otherProps.helperText = meta.error;
}
const configText = {
...field,
...otherProps,
fullWidth: true,
variant: "outlined"
};
return <TextField {...configText} />;
};
export default FieldWrapper;
//BtnTemplate.jsx
import { Button } from "#mui/material";
import { useFormikContext } from "formik";
export const BtnTemp = ({ children, ...otherProps }) => {
const { submitForm } = useFormikContext();
const formHandler = () => {
submitForm();
};
const btnConfig = {
...otherProps,
fullWidth: true,
variant: "outlined",
onClick: formHandler
};
return <Button {...btnConfig}>{children}</Button>;
};
andhere is a link for CodeSandbox
Thanks in advancce !
Now its works ! just passed the usedField props.
here is the solution in sandbox
import * as React from "react";
import { NumericFormat } from "react-number-format";
import { useField } from "formik";
const PriceTemp = ({ name, ...otherProps }) => {
const [field, meta] = useField(name);
const passwordConfig = {
...field,
...otherProps,
name: "price",
label: "Price",
thousandSeparator: ",",
allowNegative: false,
decimalScale: 2,
decimalSeparator: ".",
fixedDecimalScale: true
};
return <NumericFormat {...passwordConfig} />;
};
export default PriceTemp
;

Textarea input fields in Chakra UI wont submit to react hook form

I am using nextjs (v13), react (v18) chakraUI and react hook form.
If I use Inputs (only), I can submit this form. If I change the description field to be a Textarea (from ChakraUI), the form displays on the page, but will not submit. I get no errors in the console - I can't see what's causing the issue.
Is it possible to submit data from a Textarea via react-hook-form?
import * as React from "react"
import { gql } from "#apollo/client"
import { Button, Stack, Textarea, Text } from "#chakra-ui/react"
import { useRouter } from "next/router"
import { useCreateIssueGroupMutation } from "lib/graphql"
import { useForm } from "lib/hooks/useForm"
import Yup from "lib/yup"
import { ButtonGroup } from "./ButtonGroup"
import { Form } from "./Form"
import { FormError } from "./FormError"
import { Input } from "./Input"
import { Modal } from "antd"
const _ = gql`
mutation CreateIssueGroup($data: IssueGroupInput!) {
createIssueGroup(data: $data) {
id
}
}
`
interface Props {
onClose: () => void
}
const IssueGroupSchema = Yup.object().shape({
title: Yup.string().required(),
description: Yup.string().required(),
})
export function AdminCreateIssueGroupForm(props: Props) {
const router = useRouter()
const [createIssueGroup] = useCreateIssueGroupMutation()
const defaultValues = {
title: "",
description: "",
}
const form = useForm({ defaultValues, schema: IssueGroupSchema })
const handleSubmit = (data: Yup.InferType<typeof IssueGroupSchema>) => {
return form.handler(() => createIssueGroup({ variables: { data: { ...data } } }), {
onSuccess: (res, toast) => {
toast({ description: "Issue group created" })
form.reset()
props.onClose()
},
})
}
return (
<Form {...form} onSubmit={handleSubmit}>
<Stack>
<Input name="title" label="Title" />
// this input works and allows me to submit the form
{/* <Input name="description" label="Description" /> */}
// the next 2 lines do not work. The page renders but the form does not submit
<Text mb='8px' fontWeight="medium" fontSize="sm" > Description</Text>
<Textarea name="description" rows={4} />
<FormError />
<ButtonGroup>
<Button onClick={props.onClose}>Cancel</Button>
<Button
type="submit"
isLoading={form.formState.isSubmitting}
isDisabled={form.formState.isSubmitting}
color="brand.white"
fontWeight="normal"
backgroundColor="brand.orange"
_hover={{
backgroundColor: "brand.green",
color: "brand.white",
}}
>
Create
</Button>
</ButtonGroup>
</Stack>
</Form>
)
}
My Form component has:
import * as React from "react"
import type { FieldValues, UseFormReturn } from "react-hook-form"
import { FormProvider, useFormContext } from "react-hook-form"
import { Box } from "#chakra-ui/react"
import * as Sentry from "#sentry/nextjs"
import { useToast } from "lib/hooks/useToast"
interface FormContainerProps {
onSubmit?: (values: any) => Promise<any> | any
onBlur?: (values: any) => Promise<any> | any
}
const FormContainer: React.FC<FormContainerProps> = (props) => {
const toast = useToast()
const { handleSubmit } = useFormContext()
const onSubmit = async (values: any) => {
try {
if (props.onBlur) {
return await props.onBlur(values)
}
if (props.onSubmit) {
return await props.onSubmit(values)
}
} catch (e) {
console.log(e)
Sentry.captureException(e)
toast({
title: "Application error",
description: "Something went wrong. We have been notified!",
status: "error",
})
return
}
}
return (
<Box
as="form"
w="100%"
{...(props.onSubmit && { onSubmit: handleSubmit(onSubmit) })}
{...(props.onBlur && { onBlur: handleSubmit(onSubmit) })}
>
{props.children}
</Box>
)
}
interface Props<T extends FieldValues> extends UseFormReturn<T>, FormContainerProps {
children: React.ReactNode
isDisabled?: boolean
}
export function Form<T extends FieldValues>({ onSubmit, onBlur, isDisabled, ...props }: Props<T>) {
return (
<FormProvider {...props}>
<fieldset disabled={isDisabled}>
<FormContainer {...{ onSubmit, onBlur }}>{props.children}</FormContainer>
</fieldset>
</FormProvider>
)
}
Input has:
import * as React from "react"
import { useFormContext } from "react-hook-form"
import type { InputProps } from "#chakra-ui/react";
import { FormControl, Input as CInput } from "#chakra-ui/react"
import { InputError } from "./InputError"
import { InputLabel } from "./InputLabel"
interface Props extends InputProps {
name: string
label?: string
subLabel?: string
}
export const Input = ({ label, subLabel, ...props }: Props) => {
const {
register,
formState: { errors },
} = useFormContext()
const fieldError = errors?.[props.name]
return (
<FormControl isInvalid={!!fieldError} isRequired={props.isRequired}>
<InputLabel label={label} subLabel={subLabel} name={props.name} />
<CInput {...register(props.name)} mb={0} {...props} />
<InputError error={fieldError} />
</FormControl>
)
}
Each form component connected to React Hook Form needs to receive a register or be wrapped by a Controller component. Your input component receives this by useFormContext as you mentioned:
<CInput {...register(props.name)} mb={0} {...props} />
However, TextArea component doesn't receive anything from Hook Form, in that case, you need to use the same register('').
An example of this implementation (live on CodeSandbox):
function App() {
const { register, handleSubmit } = useForm({
defaultValues: {
title: "",
description: ""
}
});
return (
<>
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Heading>Welcome to Chakra + TS</Heading>
<p>Title</p>
<Input {...register("title")} />
<p>Description</p>
<Textarea {...register("description")} />
<Button type="submit">Submit</Button>
</form>
</>
);
}
Useful links:
Register
Controller
Live Example

MUI TextField breaks Validation Schema when addind Onchange

this is my first thread I hope i do this right,
I am trying to send data for mysql server and it goes well, however when I use TextField from MUI the validation schema starts looking funny and gives error ("require field") even with the text there, However if i submit the data the mysql receives the data.
and here goes the code...
//uses Express send data to mysql
import { useState, useEffect } from 'react'
import axios from "axios"
import { Container, Grid, Typography} from '#material-ui/core';
import { makeStyles } from '#material-ui/core/styles';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import Textfield from './Forms/IndividualComponents/TextField';
import Button from './Forms/IndividualComponents/Button'
const useStyles = makeStyles((theme) => ({
formWrapper: {
marginTop: theme.spacing(10),
marginBottom: theme.spacing(8),
},
}));
const INITIAL_FORM_STATE = {
firstName: ''
};
const FORM_VALIDATION = Yup.object().shape({
firstName: Yup.string("enter your first name")
.required('Required')
});
export default function ImagesUpload() {
const [firstName, setFirstName] = useState()
const [submitform, setSubmitForm] = useState([])
useEffect(() => {
(async() => {
const result = await axios.get('/submitform')
setSubmitForm(result.data.submitform)
})()
}, [])
const submit = async event => {
event.preventDefault()
const data = new FormData()
data.append('firstName', firstName)
const result = await axios.post('/submitForm', data)
setSubmitForm([result.data, ...submitform])
console.log(firstName)
}
const classes = useStyles();
return (
<Grid item xs={12}>
<Container maxWidth="md">
<div className={classes.formWrapper}>
<Formik
initialValues={{
...INITIAL_FORM_STATE
}}
validationSchema={FORM_VALIDATION}
>
<Form onSubmit={submit}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
Personal details
</Typography>
</Grid>
<Grid item xs={6}>
<Textfield
name="firstName"
label="First Name"
value={firstName}
onChange={(event => setFirstName(event.target.value) )}
type="text"
/>
</Grid>
<button type="submit">Submit</button>
</Grid>
</Form>
</Formik>
<main>
{submitform.map(post => (
<figure key={post.id}>
<figcaption>{post.firstName}</figcaption>
</figure>
))}
</main>
</div>
</Container>
</Grid>
);
}
the Textfield im using is a costum one with the following code:
import React from 'react';
import { TextField } from '#material-ui/core';
import { useField } from 'formik';
const TextfieldWrapper = ({
name,
...otherProps
}) => {
const [field, mata] = useField(name);
const configTextfield = {
...field,
...otherProps,
fullWidth: true,
};
if (mata && mata.touched && mata.error) {
configTextfield.error = true;
configTextfield.helperText = mata.error;
}
return (
<TextField {...configTextfield} />
);
};
export default TextfieldWrapper;
the problem here is the onChange: If i don't use it I cant setFirstName to send the data, but when I use it it breaks the validation... I tried everything i can find online but this is taking away my sleep for real ! :)

Material UI autocomplete with datepicker inside it not popping

I am trying to fit a datetime picker inside the paper of MUI autocomplete.
Tried mui datepicker and tried to force open the datepicker but no luck.
The native seems to work to an extend but it still needs to wire the date selection click to close the poper.
here is demo:
https://codesandbox.io/s/material-demo-forked-pmgzp
Plz advise
I was trying to integrate DatePicker with autocomplete. I did it with useAutoComplete
import * as React from "react";
import { useAutocomplete } from "#mui/base/AutocompleteUnstyled";
import { DateRangePicker } from "mui-daterange-picker";
import { TextField } from "#mui/material";
import InputAdornment from "#mui/material/InputAdornment";
import CalendarMonthOutlinedIcon from "#mui/icons-material/CalendarMonthOutlined";
import moment, { Moment } from "moment";
export interface IDatePickerProps {
name: string;
startDateLabel: string;
endDateLabel: string;
value: [Moment, Moment];
setFieldValue: (
field: string,
value: [Moment, Moment],
shouldValidate?: boolean
) => void;
}
export default function UseAutocomplete({
name = "datepicker",
value = [moment(), moment().add(1, "months")],
startDateLabel = "2022-10-1",
endDateLabel = "2022-12-1",
setFieldValue
}) {
const { getRootProps, getInputLabelProps, getInputProps } = useAutocomplete({
id: "use-autocomplete-demo",
options: []
});
const [open, setOpen] = React.useState(false);
const toggle = () => setOpen(!open);
return (
<div>
<>
<div {...getRootProps()}>
<label {...getInputLabelProps()}>{name}</label>
<TextField
{...getInputProps()}
name={name}
placeholder={`${startDateLabel} - ${endDateLabel}`}
onClick={toggle}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<CalendarMonthOutlinedIcon />
</InputAdornment>
)
}}
value={`${moment(value[0]).format("YYYY-MM-DD")} - ${moment(
value[1]
).format("YYYY-MM-DD")}`}
/>
</div>
{
<DateRangePicker
open={open}
toggle={toggle}
onChange={(range) => {
let selectedValues = [
moment(range.startDate),
moment(range.endDate)
];
setFieldValue(selectedValues);
}}
/>
}
</>
</div>
);
}
Output:

How do I pass state between functional components?

I am currently writing a sign-up page with react-js with react-hooks and I am still learning so please excuse me if this is a very simple question.
I have a signup.js written in functional component with hooks. signup.js imports 'EmailTextField', 'PasswordTextField', 'NameTextField', 'CellPhoneTextField' ... components of which are also written in functional components with hooks.
I made all these textfields as separate components to simplify the code as I have a requirement to have many different checks on each text fields. (and having all these fields in signup.js page makes very long code)
At the end of the process in signup.js, I would like to get state of all it's sub-components (all those textfields) status (whether the user is good to sign in or not.) but I am not sure how to pass a state (or variable) from these textfields up to signup.js.
I know redux can manage state but is there anyway to achieve this without redux?
Thank you.
I've created a CodeSandbox Sample with a minimal sample code to work with.
In here, I use EmailTextfield component in apptest.js. I would like to get isValid state on EmailTextfield from apptest.js so that I can make sure all fields are validated before the user signs up.
'./components/UI/Textfield/EmailTextField.js'
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
export const EmailTextField = props => {
const [value, setValue] = useState("");
const [helperText, setHelperText] = useState(
"Email address will be used as your username."
);
const [isValid, setIsValid] = useState("true");
const handleOnChangeEmailAddress = event => {
// Email Validation logic
if (true) {
setIsValid(true);
} else {
setIsValid(false);
}
};
return (
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="email address"
error={!isValid}
helperText={helperText}
name="email"
autoComplete="email"
margin="dense"
onBlur={handleOnChangeEmailAddress}
/>
</Grid>
);
};
export default EmailTextField;
'aptest.js'
import React from "react";
import CssBaseline from "#material-ui/core/CssBaseline";
import Grid from "#material-ui/core/Grid";
import { makeStyles } from "#material-ui/core/styles";
import Container from "#material-ui/core/Container";
import { EmailTextField } from "./components/UI/Textfield/EmailTextField";
const useStyles = makeStyles(theme => ({
"#global": {
body: {
backgroundColor: theme.palette.common.white
}
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
},
mainBox: {
// margin: '200px',
width: "550px",
textAlign: "left",
boxShadow: "0 2px 3px #ccc",
border: "1px solid #eee",
padding: "40px 70px 50px 70px",
boxSizing: "border-box"
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3)
}
}));
const Apptest = props => {
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<div className={classes.mainBox}>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<EmailTextField />
</Grid>
</form>
</div>
</div>
</Container>
);
};
export default Apptest;
I have a very crude implementation in mind.
There should be a consistent data-model around the Input fields. This data model should be a single source of truth for the that particular Input field. It should be able to tell whether that particular field is touched, has errors, is pristine, what is it's value and all that stuff.
So let's say you have it like this:
errors: [],
onChange: false,
pristine: true,
touched: false,
value,
Let's call it a StateChangeEvent.
Now each Input field will have a handler for events like change and blur. Here that individual component will update the StateChangeEvent. And these methods will eventually call a callback function with StateChangeEvent as an argument.
That way, the parent will know that there was a change in one of the fields and it can respond accordingly.
In the parent component, to make the Submit Button on the form enabled, we can also have a side effect that will update the overall state of the form. Something like this:
useEffect(() => {
const isValid = !fieldOne.onChange &&
fieldOne.errors.length === 0 &&
fieldOne.value.length !== 0 &&
!fieldTwo.onChange &&
fieldTwo.errors.length === 0 &&
fieldTwo.value.length !== 0 &&
...;
setIsFormValid(isValid);
}, [fieldOne, fieldTwo, ...]);
I'm sure this isn't a complete solution. But I'm sure it would get you started.
UPDATE:
Based on the CodeSandbox that you provided, here's what you can do to make this work:
import ...
const useStyles = makeStyles(theme => ({ ... }));
const Apptest = props => {
const classes = useStyles();
const [isInvalid, setIsInvalid] = useState(true);
const handleStateChange = updatedState => {
console.log("updatedState: ", updatedState);
updatedState.errors.length === 0 ? setIsInvalid(false) : setIsInvalid(true);
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<div className={classes.mainBox}>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<EmailTextField onStateChange={handleStateChange} />
</Grid>
<Button
variant="contained"
color="primary"
disabled={isInvalid}
className={classes.button}
>
Submit
</Button>
</form>
</div>
</div>
</Container>
);
};
export default Apptest;
And in the EmailTextField component:
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
export const EmailTextField = props => {
const { onStateChange } = props;
const [state, setState] = useState({
errors: [],
onChange: false,
pristine: true,
touched: false,
value: null
});
const helperText = "Email address will be used as your username.";
const handleBlur = event => {
// Email Validation logic
const matches = event.target.value.match(
`[a-z0-9._%+-]+#[a-z0-9.-]+.[a-z]{2,3}`
);
if (matches) {
const updatedState = {
...state,
touched: true,
value: event.target.value,
errors: []
};
setState(updatedState);
onStateChange(updatedState);
} else {
const updatedState = {
...state,
touched: true,
value: event.target.value,
errors: ["Please enter a valid email"]
};
setState(updatedState);
onStateChange(updatedState);
}
};
return (
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="email address"
error={state.errors.length > 0}
helperText={state.errors.length > 0 ? state.errors[0] : helperText}
name="email"
autoComplete="email"
margin="dense"
onBlur={handleBlur}
/>
</Grid>
);
};
export default EmailTextField;
Here's a Working CodeSandbox Sample for your ref.
I figured it out, sorry for the late reply. I was asleep.Basically an onBlur() takes a callback, now in this case you need to pass the value in the input box to the callback so you can have access to the value of the user input. The other way is to use an onChange() to track the change and set it so that when the onblur is called you can check the value and then you can perform your validations.
So you just have to pass the target value of the event to the callback like so onBlur={(e) => handleOnChangeEmailAddress(e.target.value)} and then you can have access to the value in method. I have refactored the code you shared in the sandbox. Find below a snippet of what I did.
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
export const EmailTextField = props => {
const [value, setValue] = useState("");
const [helperText, setHelperText] = useState(
"Email address will be used as your username."
);
const [isValid, setIsValid] = useState("true");
const handleOnChangeEmailAddress = value => {
// Email Validation logic
if (!value) {
setIsValid(true);
} else {
setIsValid(false);
}
console.log(isValid)
};
return (
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="email address"
error={!isValid}
helperText={helperText}
name="email"
autoComplete="email"
margin="dense"
onBlur={(e) => handleOnChangeEmailAddress(e.target.value)}
/>
</Grid>
);
};
export default EmailTextField;
I hope it helps.. if you have any problems don't hesitate to ask questions..
From your codesandbox example it looks like you were almost there you just needed to pass your onStateChange function as a prop:
<EmailTextField onStateChange={onStateChange} />
Then implement the onStateChange function in your apptest.js file which will get the updated object.
Check out my example below and open the console, you will see console logs for errors and an "isValid" response if the email is valid.
https://codesandbox.io/s/loving-blackwell-nylpy?fontsize=14

Resources