Material UI 5 Stepper + react hook form 7 - reactjs

I want multi step form. I have MUI5 and react-hook-form 7.
I have 2 form one is signup and second is address.
I just want to update this example: https://codesandbox.io/s/unruffled-river-9fkxo?file=/src/MultiStepForm.js:2597-2608.
I tried something like this https://codesandbox.io/s/mui5-react-hook-form-7-multi-step-form-9idkw?file=/src/App.js
but value is reset on step change and also validation is not working.
I also want to get those values in last step and submit first.
and Can i create defaultValues object step wise?
const defaultValues = {
"step1": {
firstname: "",
lastname: "",
}
"step2": {
address: "",
city: ""
}
}
because I want to submit first form data. and rest of data on last step.
So is this approach is okay? or should I have to do another way?
Can you please help me.

You can save to values to localStorage and if user go back you can easily set defaultValues.
For example:
useEffect(() => {
if (location.pathname === '/step1') {
setActiveStep(3);
} else if (location.pathname === '/step2') {
setActiveStep(0);
} else if (location.pathname === '/step3') {
setActiveStep(1);
} else if (location.pathname === '/step4') {
setActiveStep(2);
} else {
}
}, [location.pathname]);

There were a lot of errors in your code. I have changed a few things in it, and you can find the working code here (Works at least)
Here are the errors I found:
In InputController.js, you were using useForm instead of useFormContext
const { control } = useForm(); this is wrong
const { control } = useFormContext(); this is correct
In App.js, firstname and lastname variables (in defaultValues) were written differently from how they were written in the validation schema.
in defaultValues, you wrote firstname, but in validation schema you wrote firstName (notice the difference in camelCase). The same issue was present in the lastname variable. This is why validation was not working.
I moved the submit button into the App.js so that It can be rendered conditionally. You only want it to show on the last step :)
Also note that you need to add defaultValues as a prop on the controller component for the TextField because it's a controlled component. Check how I solved this in InputController.js. Lastly, react hook form stores the value for you (this is taken care of by spreading the {...field} on the TextField.
Hope this helps.

Related

Conditionally enabling a form button with react-hook-form and material ui

I'm creating a React form with Material UI. My goal is to force the user to answer all the questions before they can click the download button. In other words I'm trying to leave the download button in the disabled state until I can determine that values are set on each field. I've tried to get this working with and without react-hook-form.
What I've tried
Without react-hook-form...
I have my example in coding sandbox here:
https://codesandbox.io/s/enable-download-button-xwois4?file=/src/App.js
In this attempt I abandoned react-hook-form and added some logic that executes onChange. It looks through each of the formValues and ensures none of them are empty or set to "no answer". Like this:
const handleInputChange = (e) => {
// do setFormValues stuff first
// check that every question has been answered and enable / disable the download button
let disableDownload = false;
Object.values(formValues).forEach((val) => {
if (
typeof val === "undefined" ||
val === null ||
val === "no answer" ||
val === ""
) {
disableDownload = true;
}
});
setBtnDisabled(disableDownload);
The problem with this approach, as you'll see from playing with the UI in codesandbox, is that it requires an extra toggle of the form field value in order to detect that every field has been set. After the extra "toggle" the button does indeed re-enable. Maybe I could change this to onBlur but overall I feel like this approach isn't the right way to do it.
Using react-hook-form
With this approach...the approach I prefer to get working but really struggled with, I ran into several problems.
First the setup:
I removed the logic for setBtnDisabled() in the handleInputChange function.
I tried following the example on the react-hook-form website for material ui but in that example they're explicitly defining the defaultValues in useForm where-as mine come from useEffect. I want my initial values to come from my questionsObject so I don't think I want to get rid of useEffect.
I couldn't figure out what to do with {...field} as in the linked material ui example. I tried dropping it on RadioGroup
<Controller
name="RadioGroup"
control={control}
rules={{ required: true }}
render={({ field }) => (
<RadioGroup
questiontype={question.type}
name={questionId}
value={formValues[questionId]}
onChange={handleInputChange}
row
{...field}
>
but I get an MUI error of : MUI: A component is changing the uncontrolled value state of RadioGroup to be controlled.
Also, I don't see that useForm's state is changing at all. For example, I was hoping the list of touchedfields would increase as I clicked radio buttons but it isn't. I read that I should pass formState into useEffect() like this:
useEffect(() => {
const outputObj = {};
Object.keys(questionsObject).map(
(question) => (outputObj[question] = questionsObject[question].value)
);
setFormValues(outputObj);
}, [formState]);
but that makes me question what I need to do with formValues. Wondering if formState is supposed to replace useState for this.

React-Phone-Number-Input + React-Hook-Form: How to get current country code in controller validation?

I'm using react-phone-number-input library. The phone number input is not required but if the number is present I wish it could be validated before the form is sent.
If the cellphone field is pristine / left untouched when form is submitted then isValid accepts undefined (valid state).
If country code is changed right away (without actually inputting the number) isValid accepts the selected country's calling code (e.g. +46 for Sweden). This is still a perfectly valid case.
When accessed in the isValid function the phoneCountryCode always holds the previous selected value. So there's always a disparity between the validated phone number and the current country code. I'm not sure if the problem is library-specific, maybe it's my mistake. How do I get rid of the mentioned disparity?
I made a reproduction on CodeSandbox.
import PhoneInput, {
parsePhoneNumber,
getCountryCallingCode
} from "react-phone-number-input";
const [phoneCountryCode, phoneCountryCodeSetter] = useState('DE');
<Controller
name="cellphone"
rules={{
validate: {
isValid: value => {
if(value) {
const callingCode = getCountryCallingCode(phoneCountryCode);
if(! new RegExp(`^\\+${callingCode}$`).test(value)) {
// The parsePhoneNumber utility returns null
// if provided with only the calling code
return !!parsePhoneNumber(value);
}
}
return true;
}
}
}}
control={control}
render={({ field }) => (
<PhoneInput
{...field}
onCountryChange={(v) => phoneCountryCodeSetter(v)}
limitMaxLength={true}
international={true}
defaultCountry="DE"
/>
)}
/>
It's a react specific, nothing wrongs in the library. React never update state immediately, state updates in react are asynchronous; when an update is requested there's no guarantee that the updates will be made immediately. Then updater functions enqueue changes to the component state, but react may delay the changes.
for example:
const [age, setAge] = useSate(21);
const handleClick = () => {
setAge(24)
console.log("Age:", age);
}
You'll see 21 logged in the console.
So this is how react works. In your case, as you change country this "onCountryChange" event triggers updating function to update the state and to validate the phone number but the state is not updated yet that's why it is picking the previous countryCode value(Do console log here).
To understand this more clearly put this code inside your component.
useEffect(() => {
console.log("useEffect: phoneCountryCode", phoneCountryCode);
}, [phoneCountryCode]);
console.log(phoneCountryCode, value); // inside isValid()
useEffect callback will be called when phoneCountryCode value is updated and console.log inside isValid() will be called before the state gets updated.
Hopes this makes sense. Happy Coding!!

react-hook-form reset errors messages only

I have some dynamic fields, which gets removed/added on the basis of some hook state. I have fields which gets removed from the list but the errors for them are still visible. I have tried to clearErrors, unregister to remove it but nothing works.
is it possible? reset does work but it resets the whole form too.
I am using v6 of react-hook-form and i cannot upgrade it to 7. That's out of the picture for now.
yup validator is being used with it for validations.
I stuck into the same problem seems like bug, if you try unregister the control it doesn't do it. Here how I have done.
When you remove the control do unregister and reset specific control.
const handleRemoveRow = (control) => {
//all code logic and stuff
//................
unregister(control);
reset({ [control]: undefined });
};
After that on useEffect hook assume you have one main state of form, reassign the values back.
useEffect(() => {
const keyValue = getValues();
keyValues.map(({controlName,Value}) => {
setValue(controlName, Value);
});
}, [getValues()]);
This is a more of pseudo-code but I hope you got the concept.

Validate one field based on another Field in redux Form

I am using redux-form, and my Component has several FieldArray. One of the FieldArray component generates table like shown in the screenshot. Here each row is a Field component including the checkbox. What I want to achieve is, if checkbox component on that row is checked, then only price field should be required.
I tried to solve this by using validate.js as described in docs, but since, this component has structure as:
<FirstComponent>
<FieldArray
component={SecondComponent}
name="options"
fruits={fruitsList}
/>
</FirstComponent>
In my SecondComponent I am iterating over fruitsList and if length is greater than 1, then I render ThirdComponent. This component is responsible for generating the Fruit lists as show in the screenshot. Having some degree of nesting, when I validate with values, it has a lot of performance lag, my screen freezes until it renders the ThirdComponent. Each component has bit of Fields so can not merge them easily. Any easier way to solve this in elegant way would be helpful. The logic for validating is as follow:
props.fruitsList.map((fruit, index) => {
const isChecked = values.getIn(['fruit', index, 'checked'])
if (isChecked) {
const price = values.getIn(['fruit', index, 'price'])
if (price === undefined || price.length === 0) {
errors.fruits[index] = { price: l('FORM->REQUIRED_FIELD') }
}
}
return errors
})
The synchronous validation function is given all the values in the form. Therefore, assuming your checkbox is a form value, you have all the info you need.
function validate(values) {
const errors = {}
errors.fruits = values.fruits.map(fruit => {
const fruitErrors = {}
if (fruit.checked) { // only do fruit validation if it's checked
if (!fruit.price) {
fruitErrors.price = 'Required' // Bad user!!
}
}
return fruitErrors
})
return errors
}
...
MyGroceryForm = reduxForm({
form: 'groceries',
validate
})(MyGroceryForm)

redux-form: How do I dynamically exclude one sync validator?

My redux-form decorated form component conditionally has an email field, depending on my redux state (depending on whether or not the user is a guest.)
I only want to sync-validate that field when it is present (conditionally rendered.) Currently the form's validator includes an email field validator, and this validator runs even when the field has been excluded from the form, during render.
When I "instantiate" the form's validator, and when I pass the validator as an argument to the redux-form decorator, state isn't known. So I'm currently unable to decide whether to include the email field validator, in either of these places.
What's the best way to dynamically include / exclude a single field's redux-form validator, at "runtime" / "validation time", based on state?
MyForm.js
import validate from './MyFormValidator';
// [form class whose render optionally shows the email component]
export default reduxForm(
{
form: 'myForm',
fields,
validate
}
)(MyForm)
MyFormValidator.js
import {createValidator, required, email} from '../../utils/validation';
export default createValidator({
email: [email],
country: [required],
// ...
});
utils/validation.js
export function email(value) {
const emailRegex = /.../i;
if (!emailRegex.exec(value)) {
return 'Please provide a valid email address.';
}
}
export function required(value, message) {
if (isEmpty(value)) {
return message || 'Required.';
}
}
export function createValidator(rules) {
return (data = {}) => {
const errors = {};
Object.keys(rules).forEach((key) => {
const rule = join([].concat(rules[key]));
const error = rule(data[key], data);
if (error) {
errors[key] = error;
}
});
return errors;
};
}
My sync validation is modeled after this implementation, linked to from the 4.2.0 redux-form docs (I'm using 5.3.1): https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/utils/validation.js
Thanks to #lewiscowper who suggested this solution on some JavaScript Slack.
Since the input is a text field, the value will come through as an empty string if the input was rendered.
So if value is null/undefined, we can skip validation for the field, since we know it wasn't rendered.
To avoid false-positive usages of the validator, I renamed it emailUnlessNull.
export function emailUnlessNull(value) {
// Don't validate the text field if it's value is undefined (if it wasn't included in the form)
if (value == null) {
return;
}
const emailRegex = /.../i;
if (!emailRegex.exec(value)) {
return 'Please provide a valid email address.';
}
}
In the end I think a solution that totally excludes the validator --- and doesn't complicate the validator's logic and it's usage --- would be much better, but for now this works.
Happy to accept a better answer.
EDIT:
You also have to make sure that, in the optional-field-render scenario, you also trigger it's value to be an empty string. Before it's been touched, its value comes through the validator as null.
MyForm.js
componentWillReceiveProps(nextProps) {
const { values, fields, isGuestOrder } = this.props;
if (isGuestOrder && values.email == null) {
fields.email.onChange('');
}
}
If you want to consider React-Redux-Form, this is already built-in:
import { Field } from 'react-redux-form';
import { required, email } from '../../utils/validation';
// inside render():
<Field model="user.email"
errors={{ email }}
/>
Two things will happen:
The error validators (in error) will run on every change. You can set validateOn="blur" to modify this behavior.
Validation will only occur if the <Field> is rendered. If the <Field> is unmounted, validation will be reset for that field.

Resources