I am trying to develop a form using Formik, Material UI, and React-phone-number-input lib (for phone number formatting). I faced a problem. When a phone number is being entered, one is already formatted as intended, but that number is not inserted into Formik state. So, the phone number value is not visible for Formik, and as a result, Formik can not take away an error marker "Required", when some value is entered. Having Guessed, I use react-phone-number-input lib and Formik in the not right way together. How to use them right?
github:https://github.com/AlexKor-5/FormChallenge/tree/0d37064ef54c8e87a6effb950575a5a3817d9220
I have the base file src/App.js. Where I use PhoneNumberInput component. This is actually my phone number input.
export const App = () => {
return (
<>
<Formik
initialValues={{
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
}}
validationSchema={Yup.object({
firstName: Yup.string()
.max(15, 'Have to be 15 characters or less')
.required('Required'),
lastName: Yup.string()
.max(20, 'Have to be 20 or less characters')
.required('Required'),
email: Yup.string().required('Required.'),
phoneNumber: Yup.string().required('Required'),
})}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2))
setSubmitting(false)
}, 400)
}}
>
{context => (
<Form>
<MainContainer>
<Title text={'Step 2'} iconRender={<AccountCircleRoundedIcon />} />
<TextInput text={'Email'} name={'email'} />
<PhoneNumberInput
name={'phoneNumber'}
label={'Phone Number'}
context={context}
/>
<MyButton>Next</MyButton>
</MainContainer>
</Form>
)}
</Formik>
</>
)
}
And in src/components/PhoneNumberInput/PhoneNumberInput.js I define PhoneNumberInput component.
I use Input component from react-phone-number-input to have the opportunity to use a custom input.
const MyField = React.forwardRef(function custom(props, ref) {
const { name, label } = props
return (
<Field
{...props}
component={TextField}
label={label}
name={name}
variant="outlined"
inputRef={ref}
fullWidth
/>
)
})
export const PhoneNumberInput = ({ text, ...props }) => {
const [value, setValue] = useState()
const [focus, setFocus] = useState(false)
console.log(props.context)
return (
<>
<Input
{...props}
country="UA"
international={focus}
value={value}
withCountryCallingCode
onChange={setValue}
inputComponent={MyField}
onFocus={() => setFocus(true)}
control={props.control}
/>
</>
)
}
What is wrong? How to tackle that?
Actually it's pretty easy to implement with the help of Formik hooks
This is my working solution:
import PhoneInput from "react-phone-number-input";
import { useField } from "formik";
import FieldWrapper from "../FieldWrapper";
const PhoneInputField = ({ label, ...props }) => {
const [field, meta, helpers] = useField(props.name);
return (
<FieldWrapper label={label} meta={meta} {...props}>
<PhoneInput
{...props}
{...field}
value={field.value}
defaultCountry="NO"
onChange={(value) => {
helpers.setValue(value);
}}
/>
</FieldWrapper>
);
};
export default PhoneInputField;
Then use this component in your Formik Form as so
// your boilerplate code ect...
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
{({ handleSubmit, isSubmitting }) => (
<FieldPhoneInput
label="Phone Number"
name="phone"
type="tel"
placeholder="Write your phone number here"
/>
)}
</Formik>
Related
I have a Formik form that is using a progressive stepper and have multiple fields across different components, thus requiring the need to store the values in React Context. However none of the field values are being passed, so when I click submit, all values are empty strings and the validation fails. You can see on each Formik Field i am setting the value as {state.[field]}, which comes from the Context, so I believe something is going wrong here. Can anyone see what I'm doing wrong?
Thanks a lot
Here is my parent component
const AddSongPage = () => {
const { state, dispatch } = useUploadFormContext();
const initialValues = {
name: "",
};
const { mutate: handleCreateTrack } = useCreateSyncTrackMutation(
gqlClient,
{}
);
const handleSubmit = (values: any) => {
handleCreateTrack(
{
...values,
},
{
onSuccess() {
console.log("Track added succesfully");
},
}
);
};
const validate = Yup.object({
name: Yup.string().required("Song name is required"),
description: Yup.string().optional(),
});
return (
<Layout headerBg="brand.blue">
<Formik
onSubmit={(values) => handleSubmit(values)}
initialValues={initialValues}
validationSchema={validate}
>
<Form>
<Box> {state.activeStep === 1 && <Step1 />}</Box>
<Box> {state.activeStep === 2 && <Step2 />}</Box>
<Box> {state.activeStep === 3 && <Step3 />}</Box>
<Button type={"submit"}>Submit</Button>
</Form>
</Formik>
</Layout>
);
};
Here is step 1
const Step1 = () => {
const { state, dispatch } = useUploadFormContext();
const onInputChange = (e: FormEvent<HTMLInputElement>) => {
const inputName = e.currentTarget.name;
dispatch({
type: "SET_UPLOAD_FORM",
payload: {
[inputName]: e.currentTarget.value,
},
});
};
return (
<Stack spacing={4}>
<Field name={"name"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.name}
/>
</FormControl>
)}
</Field>
<Field name={"description"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.description}
/>
</FormControl>
)}
</Field>
</Stack>
);
};
export default Step1;
On the code below there is a text input that shows an error message when:
type invalid email (or leave blank) and hit enter
type invalid email (or leave blank) and blur (or clicking outside)
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import * as Yup from 'yup';
const TextInput = ({ value, handleChange }) => (
<input
type="text"
value={value}
onChange={(e) => handleChange(e.target.value)}
/>
);
export default () => {
return (
<Formik
initialValues={{
email: '',
}}
validationSchema={Yup.object().shape({
email: Yup.string()
.required('Email is required.')
.email('Email is invalid.'),
})}
onSubmit={(values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
}}
enableReinitialize
>
{({ setFieldValue }) => (
<Form>
<div>
<Field
type="text"
name="email"
onChange={({ target: { value } }) => {
console.log(value);
setFieldValue('email', value);
}}
/>
<ErrorMessage name="email">
{(error) => <div style={{ color: '#f00' }}>{error}</div>}
</ErrorMessage>
</div>
<input type="submit" value="Submit" />
</Form>
)}
</Formik>
);
};
Now I want to keep the same behavior when doing the following change:
from
<Field
type="text"
name="email"
onChange={({target:{value}}) => {
console.log(value);
setFieldValue("email", value);
}}
/>
to:
<Field
component={TextInput}
name="email"
handleChange={(value) => {
console.log(value);
setFieldValue("email", value);
}}
/>
I mean, basically I'm changing from: type="text" to component={TextInput} which is basically a text input as you can see above.
After doing that change the error happens when:
[SUCCESS] type invalid email (or leave blank) and hit enter
but not when:
[FAIL] type invalid email (or leave blank) and blur (or clicking outside)
Do you know how can I get the error displayed on the second situation?
I'm looking for a standard way to do it and not tricky workarounds.
Here you have a StackBlitz you can play with and if you want you can post your forked one.
Thanks!
The way I handle this is to pass name and id between the component and wrapper, otherwise, Formik has no relationship and therefore no knowledge of what to error.
Here's a fork
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import * as Yup from 'yup';
const TextInput = ({ value, handleChange, name, id }) => (
<Field
id={id} // <- adding id
name={name} // <- adding name
type="text"
value={value}
onChange={handleChange} // You don't need to capture the event, you've already done the work in the wrapper
/>
);
export default () => {
return (
<Formik
initialValues={{
email: '',
}}
validationSchema={Yup.object().shape({
email: Yup.string()
.required('Email is required.')
.email('Email is invalid.'),
})}
onSubmit={(values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
}}
enableReinitialize
>
{({ setFieldValue }) => (
<Form>
<div>
<Field
component={TextInput}
id="email" // <- id
name="email" // <- name
handleChange={e => {
const value = e.target.value;
console.log(value);
setFieldValue('email', value);
}}
/>
<ErrorMessage name="email">
{(error) => <div style={{ color: '#f00' }}>{error}</div>}
</ErrorMessage>
</div>
<input type="submit" value="Submit" />
</Form>
)}
</Formik>
);
};
I have the following code which you can find here:
https://stackblitz.com/edit/react-d2fadr?file=src%2FApp.js
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
<ErrorMessage name={fieldName} />
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
I want to know if is it any way to render the ErrorMessage element automatically?
The error message should be shown somewhere around the input text.
If you know how, you can fork the StackBlitz above with your suggestion.
Thanks!
Don't really know why ErroMessage is not rendering before you submit your form once but you can replace the line <ErrorMessage name={fieldName} /> by {formik.errors[fieldName]} to make it works
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
{formik.errors[fieldName]}
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
the issue is validation schema. When I changed 6
return /^[0-9a-f]{6}$/.test(value);
to 3
return /^[0-9a-f]{3}$/.test(value);
and submitted with the initial value, ErrorMessage component is rendered
To reach your goal, I changed your code as below:
Since Formik's default component is Input, I deleted your TextInput component as there was nothing special in your component and handleChange function.
<Field name="hexColor" placeholder="Hex Color" onChange={(e) => handleChange(e.target.value)}/>
<ErrorMessage name="hexColor" />
As in mentioned in this answer, I changed your submit button condition to determine whether the button is disabled or not:
<Button type="submit" disabled={Object.keys(errors).length}>
Submit
</Button>
You can view my entire solution here.
Edit
If you want to keep your component, you should pass props as you might be missing something important, e.g. onChange,onBlur etc.
const TextInput = ({ field, ...props }) => {
return (
<input
{...field} {...props}
// ... your custom things
/>
);
};
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
onChange={(e) => handleChange(e.target.value)}
/>
Solution 2
I currently have a form I am building using formik. I am able to update each of the fields(name, email, phone) as expected. The issue I am running into is when I provide one of the formik intial values with a state value and update that state. The act of updating the state causes the form to completely reset the input fields Is there a way to update the test state without resetting the entire form?? My code is as follows:
import { useState } from "react";
import { Formik, FieldArray } from "formik";
import "./styles.css";
export default function App() {
const [test, setTest] = useState(null);
return (
<Formik
enableReinitialize
initialValues={{
test: test,
name: "",
email: "",
phone: ""
}}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
handleSubmit
}) => (
<>
<button onClick={() => setTest("something")}>Update State</button>
<br />
<input
placeholder="name"
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
<br />
<input
placeholder="email"
type="text"
name="email"
value={values.email}
onChange={handleChange}
/>
<br />
<input
placeholder="phone"
type="text"
name="phone"
value={values.phone}
onChange={handleChange}
/>
<>
<pre style={{ textAlign: "left" }}>
<strong>Values</strong>
<br />
{JSON.stringify(values, null, 2)}
</pre>
<pre style={{ textAlign: "left" }}>
<strong>Errors</strong>
<br />
{JSON.stringify(errors, null, 2)}
</pre>
</>
</>
)}
</Formik>
);
}
In order to replicate what I am talking about in the codesandbox first update the name, email, and phone number. Then hit the udate state button and everything will be reset
codesandbox https://codesandbox.io/s/wispy-sun-mzeuy?file=/src/App.js:0-1567
This is because you are initializing the Formik component with enableReinitialize set to true:
enableReinitialize?: boolean
Default is false. Control whether Formik should reset the form if initialValues changes (using deep equality).
When the state variable test is updated, the formik-values are reinitialized to the values set in initialValues.
If you want this to work, I've forked your sandbox and modified it to use the formik hook (without enableReinitialize) in combination with a useEffect to manually update the value from state:
const [test, setTest] = useState(null);
const { values, errors, handleChange, setFieldValue } = useFormik({
initialValues: {
test: test,
name: "",
email: "",
phone: ""
}
});
useEffect(() => {
setFieldValue("test", test);
}, [test]);
My solution for this problem was to use the Formik values as state and set it directly with setFieldValue, so enableInitialize would not be triggered.
export default function App() {
//const [test, setTest] = useState(null); // This removed
return (
<Formik
enableReinitialize
initialValues={{
test: "",
name: "",
email: "",
phone: ""
}}
>
...
<button onClick={() => setFieldValue("test", "something")}>Update State</button>
Mostly the reason you would want to use states outside of Formik is to use it outside of Formik (ok clear), but this way you still can trigger a function from outside of Formik which you pass the values and the setFieldValue- Function to do whatever when triggered e.g. at onChange of a Formik field:
const handlerDefinedOutside = (values: any, setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void): void => { /* do stuff */ };
<TextField
onChange={(e) => {
handleChange(e);
handlerDefinedOutside(values, setFieldValue);
}}
/>
If my login in successful, an authentication token is returned, which is stored in the local storage. Upon successful login, I want to go the a private route.
I found this code Javascript snippet but I am unable to make it work for Typescript. I don't have any isAuthenthicated property yet. How could I modify this accordingly?
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{pathname: '/login', state: { from: props.location }
}}/>
)
)}/>
)
Here is my login screen:
const LoginMutation = gql`
mutation LoginMutation($email: String!, $password: String!) {
loginEmail(email: $email, password: $password)
}
`;
const schema = Yup.object({
email: Yup
.string()
.email('Invalid Email')
.required('Please Enter your Email'),
password: Yup
.string()
.required('Please Enter your password')
});
function LoginPage (){
const [state, setState] = useState({
email: '',
password: '',
loggedIn: false,
});
function submitForm(LoginMutation: any) {
const { email, password } = state;
console.log(email, password)
if(email && password){
LoginMutation({
variables: {
email: email,
password: password,
},
}).then(({ data }: any) => {
localStorage.setItem('token', data.loginEmail);
})
.catch(console.log)
}
}
return (
<Mutation mutation={LoginMutation}>
{(LoginMutation: any) => (
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Formik
initialValues={{ email: '', password: '' }}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
validationSchema={schema}
>
{props => {
const {
values: { email, password },
errors,
touched,
handleChange,
isValid,
setFieldTouched
} = props;
const change = (name: string, e: any) => {
e.persist();
handleChange(e);
setFieldTouched(name, true, false);
setState( prevState => ({ ...prevState, [name]: e.target.value }));
};
return (
<form style={{ width: '100%' }} onSubmit={e => {e.preventDefault();submitForm(LoginMutation)}}>
<TextField
variant="outlined"
margin="normal"
id="email"
fullWidth
name="email"
helperText={touched.email ? errors.email : ""}
error={touched.email && Boolean(errors.email)}
label="Email"
value={email}
onChange={change.bind(null, "email")}
/>
<TextField
variant="outlined"
margin="normal"
fullWidth
id="password"
name="password"
helperText={touched.password ? errors.password : ""}
error={touched.password && Boolean(errors.password)}
label="Password"
type="password"
value={password}
onChange={change.bind(null, "password")}
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<br />
<Button className='button-center'
type="submit"
disabled={!isValid || !email || !password}
>
Submit</Button>
</form>
)
}}
</Formik>
</div>
)
}
</Mutation>
);
}
export default LoginPage;
There is a similar question but it doesn't answer my case since I'm storing the token in local storage.
just replace fakeAuth.isAuthenticated by your saved token which you might save it also as a global state right? so, in general, it just a boolean prop to check if the user is successfully login or not depend on that situation the user will redirect either to the login page or the protected page
This will go in your index.tsx file:
const token = localStorage.getItem('token');
const PrivateRoute = ({component, isAuthenticated, ...rest}: any) => {
const routeComponent = (props: any) => (
isAuthenticated
? React.createElement(component, props)
: <Redirect to={{pathname: '/login'}}/>
);
return <Route {...rest} render={routeComponent}/>;
};
And use this in the browser router/switch:
<PrivateRoute
path='/panel'
isAuthenticated={token}
component={PrivateContainer}
/>