I have a multistep form, and I keep each step under a single <Formik /> tag. The initial values and validation schemas are kept in objects for each step:
const initialValues = {
step1Values: { ...values1 },
step2Values: { ...values2 },
step3Values: { ...values3 },
}
const validationSchema = Yup.object().shape({
step1Schemas: { ...yupSchema1 },
step2Schemas: { ...yupSchema2 },
step3Schemas: { ...yupSchema3 },
});
// in component render/return:
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
validateOnMount
validateOnBlur
/>
Each step has its own component rendered within the Formik tag, and use useFormikContext to grab values, errors, i.e. with const formikProps = useFormikContext(), then for a given value of a field, I can say:
// in step 1
<input
name="step1Values.someValue"
value={step1Values.someValue}
onChange={formikProps.handleChange}
/>
I want all components under the same form umbrella for persistence purposes (form values will persist even as they change from one step to another). However, there are certain cases where the user can skip step one or two and not fill them out, and jump to step 3 (based on state of the redux-store). Submitting the form is only dependent on step 3 being properly validated according to formik.
I have internal checks in components 1 and 2 to move through those steps, but I want formik to track the errors to show errors if the fields are empty after having been touched. In cases where users can skip step 1 or 2, the validation schema has errors because those fields are considered required. I have a custom validation function customValidate which returns true or false, and which is based on the current state of formikProps.errors.step3. The submit button is disabled until customValidate returns true, so I really don't need formik to validate on submit - in fact, I need it to not validate on submit, so that the onSubmit function fires properly from step 3, regardless of errors in step 1 or 2.
How can I keep validateOnMount and validateOnBlur, but prevent validate on submit? I wish there was a validateOnSubmit={false} option, but I don't see that. There must be a way?
I have a workaround for you: Just remove the validation Schema, validateOnMount, validateOnBlur because we are going to handle them ourselves.
make your Formik as Follows
<Formik
initialValues={initialValues}
/>
you will need this function to format the errors for you:
const getErrorMessages = ({ path, message, inner }) => {
if (inner && inner.length) {
return inner.reduce((acc, { path, message }) => {
acc[path] = message;
return acc;
}, {});
}
return { [path]: message };
};
and then you can validate your form onMount using useEffect like follows
useEffect(() => {
try {
validationSchema.validateSync(values, { abortEarly: false });
setErrors({});
} catch (error) {
setErrors({ ...getErrorMessages(error) });
}
}, []);
and for the onBlur make your own Function
const handleBlur = async (e) => {
setFieldTouched(e.target.name, true);
try {
validationSchema.validateSync(values, { abortEarly: false });
setErrors({})
} catch (error) {
setErrors({ ...getErrorMessages(error) });
}
};
finaly to your inputs you can make them as follows:
<input
name="step1Values.someValue"
value={step1Values.someValue}
onChange={formikProps.handleChange}
onBlur={handleBlur}
/>
final Notes:
you have to use your custom onBlur on all fields or it won't validate onBlur
you can make the same thing with onChange if you want just make a custom function like handleBlur one.
Related
Context description
I have a Vue 3 project and using element plus, I created a form with async validation. The code of validation function (Laravel precognition):
const validatePrecognitive = async (rule: InternalRuleItem, value: Value, callback: (error?: string | Error) => void) => {
return new Promise<void>((resolve, reject) => {
if (!rule.field) {
return;
}
precognitive.post('/events', computedFormData.value, {
validate: [rule.field],
onPrecognitionSuccess(response) {
if (response.status === 204) {
callback();
resolve();
}
},
onValidationError(response) {
callback(new Error(response.data.message));
reject();
},
});
});
};
and I define the rules like this:
const rules: FormRules = reactive({
} as Partial<Record<string, Arrayable<FormItemRule>>>);
// dynamically set the rules for each input, because of same value for all rules
Object.keys(getSubmitShape()).forEach(key => {
rules[key] = [{ asyncValidator: validatePrecognitiveDebounced, trigger: 'blur' }];
});
The getSubmitShape() funtion return Object with all form-item keys, that should be validated. So you can imagine it like:
const rules: FormRules = reactive({
rules['name'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
rules['surname'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
}
This works like a charm, when I write to inputs and switch between them. The errors would appear and dissappear correctly.
Submit
When the form is submitted, I am calling formEl.validate(...) function, which should validate all the inputs. It works, BUT:
for every rule item, one HTTP request is sent to server, because the validation method is called for each rule separately.
What I tried
I tried to implement debounce method, which would save the params from each previous function call and include that in next call, so in a last call, I would have all the inputs, rules and callbacks available. This works, when I try it while writing inputs to elements and switching them fast (triggering validation).
When on submit, it waits the debounce time for each rule. So the requests again go one by one with the delay of debounce time.
Any ideas, how I could make this in one request?
Thanks.
I am using Redux-Form 7.4.2 and I have a form which has field names in the following format: textIntl[0].value, textIntl[1].value etc. I am trying to set up synchronous validation for these fields, but when logging meta.error in the field it is always undefined.
For testing purposes I have hardcoded the error in the validator function which has the exact same name as the form field as the key.
When console logging errors before returning from the validator it is always.
{ textIntl[0].value: "UH OH" }
In Chrome's Redux Dev Tools I can see the syncErrors in the redux store under this form.
form: {
"heading-component-editor": {
syncErrors: {
"textIntl[0].value": "UH OH "
}
}
}
Validator Function:
export const validator = (formData: Immutable.Map<string, any>): Record<string, string> => {
const errors = {
"textIntl[0].value": "UH OH"
};
return errors;
};
Form (Where validator gets passed):
<Form
form={HEADING_EDITOR_FORM_NAME}
onSubmit={onSubmit}
initialValues={component}
validate={validator}
enableReinitialize
>
<TextareaField
name={`textIntl[{0].value`}
...
/>
...
</Form>
I expect meta.error to hold the string error message which is included in the key, value pair within the error object being returned from the validator, but instead it is always undefined.
Has anybody came across this issue before?
in my react-redux form, i want to make recaptcha a required field, and disable my navigation bar component until recaptcha response is verified, i found some similar questions with javaScript but i wasn't able to apply them with React, because i am using react-recaptcha plugin :
<div className="form_wrapper">
<ReCAPTCHA
sitekey="xxxxxxxxxxx"
render="explicit"
onloadCallback={this.callback}
verifyCallback={this.verifyCallback}
/>
</div>
<NavigationBar
fields={requiredFields}
// disableNext={this.props} (here where i make conditions to disable)
/>
here are my callback and verifyCallback methods:
verifyCallback(response) {
return response;
}
callback() {
console.log('Done !!');
}
thank you
i added the code suggested by Hardik Modha, as follows but still having the same issue:
<NavigationBar
fields={requiredFields}
disableNext={this.props ... && !this.validateForm()}
/>
verifyCallback(response) {
this.setState({
reCaptchaResponse: response,
});
}
validateForm() {
if (!this.state.reCaptchaResponse || this.state.reCaptchaResponse.trim().length === 0) {
return false;
}
return true;
}
var Recaptcha = require('react-recaptcha');
// specifying verify callback function
var verifyCallback = function (response) {
this.setState({
reCaptchaResponse: response
});
};
ReactDOM.render(
<Recaptcha
sitekey="xxxxxxxxxxxxxxxxxxxx"
render="explicit"
verifyCallback={verifyCallback}
onloadCallback={callback}
/>,
document.getElementById('example')
);
You can pass a prop verifyCallBack to react-recaptcha, in that callback function you can store the response in state or wherever you want. Now, if response is empty you can simply disable the next button or you can put validation when user clicks on validation.
e.g. If you are storing response in state then you can check whether reCaptcha response is empty or not.
validateForm() {
// other field validations....
if (!this.state.reCaptchaResponse || this.state.reCaptchaResponse.trim().length === 0) {
return {success: false, message: 'Captcha is required.'};
}
}
Edit: For the edited question,
You can also create a state variable say btnDisabled and initialize it with true.
constructor() {
super();
this.state = {btnDisabled: true};
}
and Next button as
<button disabled={this.state.btnDisabled}>Next</button>
Now, in your validateForm method you can check whether the reCaptcha response is empty or not and based on that you can set the btnDisabled variable to true or false.
validateForm() {
// other field validations....
if (!this.state.reCaptchaResponse || this.state.reCaptchaResponse.trim().length === 0) {
return {success: false, message: 'Captcha is required.'};
} else {
this.setState({
btnDisabled: false
});
}
}
Side Note: You should never rely on client side validations. User can easily bypass client side validations. So, You should implement server side validations, too.
I need to disable and re-enable a button during the async call. I am only able to disable it. If I add code to re-enable it is ignored. I acknowledge I may not be asking the right question.
I have a function with a button "Action":
<button className={`doFoo${buttonClasses[type]} ${type}`} onClick={onClick} disabled={isButtonDisabled}>
That is called by a React class "Actions":
<Action type="like" onClick={onLike} isButtonDisabled={isButtonDisabled} />
That is called by another React class "List":
<Actions onLike={this.handleLike} onDislike={this.handleDislike} isButtonDisabled={isButtonDisabled}/>
Also in that class is are the following functions:
...
thumbsUp() {
const {
...
} = this.props;
const {
...
} = this.state;
this.setState({ wasLiked: true, viewProfile: false }, () => {
setTimeout(doThumbsUp, ACTION_CONFIRMATION_ANIMATION_TIMEOUT);
});
function doThumbsUp() {
thumbsUp({
index: activeIndex,
id: profiles[activeIndex].id
});
}
},
handleLike() {
const { showThumbsUpConfirmation } = this.props;
if (showThumbsUpConfirmation) {
this.showThumbsUpConfirmationModal();
} else {
this.thumbsUp();
}
},
...
Here's what the source looks like:
export function thumbsUp({ id, ... }) {
return api.post(`${API.ENDPOINTS.FOO}/${id}/thumbs_up`, {
...
});
}
I can place this.setState(isButtonDisabled: true) at various places in this code and that value makes it to the view and disables the button. However I cannot figure out how to re-enable the button after the work has been done.
If I'm understanding you correctly you want the button to be disabled during async and after async be enabled? If that is the case, wherever you are calling the function that makes the api call, you just need to chain a .then(() => this.setState(isButtonDisabled: false) and that will update the state as soon as response has been received from api call. also if you aren't using es6 just make sure to set this to a variable above the api call to ensure its scoped properly for setState
I have a Form component that I am building in React and this form has some Field components as children. I am using these components to build a log in form that has an email address and password. My form has an onSubmit handler that serializes its children (Fields) and sends off an AJAX request.
Everything is working well in the normal case. But, when browsers like Chrome Autofill the form with my email address and password, the serialization doesn't pick up the values of the inputs.
Field currently looks something like this:
/** #jsx React.DOM */
var Field = React.createClass({
serialize: function() {
return { name: this.props.name, value: this.refs.field.state.value };
},
render: function() {
return (
<div>
<label>
{this.props.label}
<input type={this.props.type} ref="field"
name={this.props.name}
defaultValue={this.props.value} />
</label>
</div>
)
}
});
How do I get the browser Autofill value to show up in my serialized Field data?
State should flow in one direction, from the top layer down to the bottom. So reaching into a child element to get its state is a bad idea. To accomplish the serialization with this structure, you simply need to reach into the DOM to get the value of the input. In the code example above, the serialize method should be:
serialize: function() {
return { name: this.props.name, value: this.refs.field.getDOMNode().value };
},
Using onChange event is the most common scenario to update whatever model property bound to a component, but you can also use onBlur to catch the values when the form is "auto-filled" by the browser.
var Input = React.createClass({
getInitialState: function() {
return {typed: ''};
},
onBlur: function(event) {
this.setState({typed: event.target.value});
},
render: function() {
return <div>
<input type="text" onBlur={this.onBlur.bind(this)}/>
You typed: <code>{this.state.typed}</code>
</div>
}
});
This is my solution using refs :) ..basically I trigger the change events manually for the first second..the reason behind is that every browser is handling autofill in a different way and this changes based on browser version..there is no way to cover this properly as of now
componentDidMount () {
let tries = 4;
this.int = setInterval(() => {
tries--;
if (tries === 0) {
this._createChangeEvent();
clearInterval(this.int);
}
}, 300);
}
_createChangeEvent = () => {
if ("createEvent" in document) {
const evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
this.input.dispatchEvent(evt);
} else {
this.input.fireEvent("onchange");
}
};