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.
Related
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!!
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.
I have a scenario where there will be two fields of each item. One field is a checkbox and another is dropdown but the point is to get a pair of data from this and I am mapping this based on the item(they have category too). And the dropdown depends on checkbox(when unchecked the dropdown is disabled and value is reset to none or zero)
I tried
<Field name={ `${item.label}` } component={MyCheckBoxComponent}>
<Field name={ `${item.value}` } component={MyDropDownComponent}>
what happens is each of them have unique name and I cant do anything when I need to update the values depending on the checkbox selections.
I have tried putting the two inputs in one Field but I can't get it to work. Help would be much appreciated
You need to use Redux Fields (https://redux-form.com/6.0.4/docs/api/fields.md/) not Redux Field.
You can create a separate component which wraps your check-box component and your drop-down component.
This is how you use it
<Fields
names={[
checkboxFieldName,
dropDownFieldName
]}
component={MyMultiFieldComponent}
anotherCustomProp="Some other information"
/>
And the props that you get in your MyMultiFieldComponent
{
checkboxFieldName: { input: { ... }, meta: { ... } },
dropDownFieldName: { input: { ... }, meta: { ... } },
anotherCustomProp: 'Some other information'
}
The input property has a onChange property (which is a method), you can call it to update the respective field value.
For example in onChange method of check-box
onChangeCheckbox (event) {
const checked = event.target.checked;
if (checked) {
// write your code when checkbox is selected
} else {
const { checkboxFieldName, dropDownFieldName } = props;
checkboxFieldName.input.onChange('Set it to something');
dropDownFieldName.input.onChange('set to zero or none');
}
}
This way you can update multiple field values at the same time.
Hope this helps.
Probably, this is what are you looking for - formValues decorator.
Just wrap your dropdown with this decorator and pass the name into it of your checkbox so you will have access inside of the MyDropDownComponent.
For example:
import { formValues } from 'redux-form';
<form ...>
<Field name="myCheckBox" component={MyCheckBoxComponent}>
<Field name="myDropdown" component={formValues('myCheckBox')(MyDropDownComponent)} />
</form>
So then myCheckBox value will be passed as a prop.
Performance note:
This decorator causes the component to render() every time one of the selected values changes.
Use this sparingly.
See more here - https://redux-form.com/7.3.0/docs/api/formvalues.md/
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)
I'm using propTypes with React because I like how it warn me when I pass something dumb. But sometimes I misspell my prop or I forget to put it in my propTypes and it never get validated.
Is there a (standard) way to make React also validate that no extra props have been passed ?
I'm not sure whether there's a standard way, but you can certainly do a quick and dirty check using Object.keys.
var propsCount = Object.keys(this.props).length,
propTypesCount = Object.keys(this.propTypes).length;
if(propsCount === propTypesCount) {
// correct number of props have been passed
}
The only edge case you will have to watch for is props.children, as this arrives as an implicit property if you nest components/HTML inside your component.
If you want a more fine grained approach, then you'll have to pick out the keys and iterate them yourself, checking.
var passedPropNames = new Set(Object.keys(this.props)),
expectedPropNames = new Set(Object.keys(this.propTypes));
passedPropNames.forEach(function(propName) {
if(!expectedPropNames.has(propName)) {
console.warn('Not expecting a property called', propName);
}
});
expectedPropNames.forEach(function(propName) {
if(!passPropNames.has(propName)) {
console.warn('Expected a property called', propName);
}
});
This will do as you ask.
componentDidMount() {
let matchPropTypes = Object.keys(this.constructor.propTypes).every((a, index) => a === Object.keys(this.props)[index])
if (!matchPropTypes) {console.log('propTypes do not match props', Object.keys(this.constructor.propTypes), Object.keys(this.props))}
}