Formik has two options controlling when validation occurs: validateOnChange and validateOnBlur.
Problem with using validateOnChange - user will get errors as they start typing, say, email - because it is not valid when you just started to type first letters.
In case of validateOnBlur - let's say user typed in invalid email, left the field, returned and fixed to correct email - the error will be shown anyway until they leave the field, so this also does not work for me.
What I would like to achieve - first time validate a field on blur and starting that point - on change.
Every field should get this treatment individually.
I tried to find native ways to do it but couldn't find any, so I came up with this solution that I do not really like because it is not flawless:
TL;DR: I override standard onBlur and onChange coming from Field's props and a) onBlur - mark field as changed once (probably I could use touched here) b) onChange I check if field was touched once and if so - call validation.
0) Disable validation
<Formik
validateOnChange={false}
validateOnBlur={false}
...
1) Added changedFields to my state
public state: IState = {
changedFields: {}
};
2) Use special method that overrides default onBlur and onChange
<Field name="userName" render={(props: FieldProps<MyFormInterface>) => (
<input type="text" label={'Name'} {...formikCustomValidation(props, this)} />
)}/>
3) The method. Passing Field's props and the parent component where state with changedFields is located.
function formikCustomValidation({ field, form }: FieldProps, ownerCmp: Component<any, any>) {
return {
...field,
onBlur: e => {
if (field.value !== form.initialValues[field.name]) {
ownerCmp.setState({
changedFields: {
...ownerCmp.state.changedFields,
[field.name]: true
}
});
}
field.onBlur(e);
setTimeout(form.validateForm);
},
onChange: e => {
field.onChange(e);
if (ownerCmp.state.changedFields[field.name]) {
setTimeout(form.validateForm);
}
}
};
}
The question is - is there a way to do it easier?
The problem with this solution - if I Submit and untouched form and some errors come up - focus and fix value does not mark field as valid - because it is the first 'touch' and field is still in stat waiting for first blur state that supposed to make listen for 'changes'.
Anyway, maintaining this could be a pain, so looking for better solution.
Thanks!
Related
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.
I'm really trying to wrap my head around why this is a design choice and how it's not more of an issue.
I have a form that has a number of inputs on it, and when I start typing in any of the fields the whole form is validated as per my validate function, causing every validation error message to be rendered onto my form.
The form isn't even doing anything magical either, I'm literally just taking in basic text input and validating if the values are empty or not. Yet when I do that, Formik thinks it's a great idea to validate EVERY field, including my pristine ones that are yet to be touched.
Can someone please explain to my how I can change this behaviour so that only the form currently firing onChange events is validated?
I've tried using the form's touched object to check which are yet to be touched and only validate the touched ones, but that produces more problems, especially when trying to submit the form.
validateOnChange and validateOnBlur don't help either, they just turn off the validation
Again, why is this the default behaviour. Who would ever want to validate their entire form at once based on onChange events?
I've even tried to do field-level validation using instead, and even that produces the same behaviour? I'm genuinely at a loss as to how anyone can produce working forms with this. Obviously people do, so if someone can explain how to do this on a field by field basis I'd very much appreciate it.
const formik = useFormik({
initialValues: {
positionTitle: "",
experienceRequired: ""
},
onSubmit: (values) => {
console.log("Form output: ", JSON.stringify(values));
console.log("Errors: ", formik.errors);
},
isInitialValid: false,
validate: validate
validateOnMount: false,
validateOnChange: false,
validateOnBlur: false
});
<FormikProvider value={formik}>
<form onSubmit={formik.handleSubmit} className="flex flex-col mt-4">
<Field
as="input"
validate={validateBasicRequired}
name="positionTitle"
onChange={formik.handleChange}
value={formik.values.positionTitle}
></Field>
{formik.errors.positionTitle &&
formik.errors.positionTitle === "Required" && (
<BasicValidationError message="Required field."></BasicValidationError>
)}
<Select
name="experienceRequired"
onChange={handleSelectOnChange}
options={getMultiSelectOptions(experienceValues)}
></Select>
{formik.errors.experienceRequired &&
formik.errors.experienceRequired === "Required" && (
<BasicValidationError message="Required field."></BasicValidationError>
)}
</form>
</FormikProvider>
This is the validation function I was using before field-level
const validate = (values: PostJobForm) => {
const errors: any = {
compensation: {},
};
if (!values.positionTitle) {
errors.positionTitle = "Required";
}
if (!values.experienceRequired) {
errors.experienceRequired = "Required";
}
return errors;
};
Is there an easy way to change the input border color to green when a field is valid in Formik? I had trouble finding a good example of this - I'm not sure if there's a valid variable for the field?
I already have validations errors showing up in red using{errors.fieldname}, but also want the field border to turn green (or have some type of green checkmark) as soon as the field is valid.
I'm using Yup schema validation if that matters.
You could change the css to apply a green border if !errors.fieldname on form validation. You can play with validateOnBlur and validateOnChange within the Formik component, so that validation is happening either as you type or as you blur out of a field.
Perhaps you can put an onBlur function on your input, and check the errors. If there are no errors on blur, you can set a state variable to capture the fact that the field is valid, and use that to apply a className.
state = {
greenborder: {
fieldName1: null,
fieldName2: null,
}
}
handleBlur = fieldName => {
if (!errors[fieldName]) {
this.setState({
greenborder: {
...this.state.greenborder,
[fieldName]: true
}
})
}
}
// in render:
<input
onBlur={() => handleBlur(fieldName1) }
type="text"
className={this.state.greenboder.fieldName1 ? 'green' : ''}
>
This is just a generic idea. You could also attach the event to the onChange. You can also use the validate prop of the Field component. Hopefully that gets your started.
I'm rendering some material-ui TextFields. By manually typing the value inside it, it'll work properly. It is using handleChange() (change) and handleBlur() (autosave) to handle those events. but the scenario is I have to update the value not by manually typing but from the store.
So, if I pass the value from the store, it is not actually updating the value unless I click inside the field and click away or tab out. the value is showing inside the field but not invoking handleChange() and handleBlur() which are the only way to update the value inside. Also I have to type at least a value.
Approach:
I set an onFocus to the field and on it's handleFocus, I'm trying to either simulating click() and blur() or calling handleClick() and handleBlur(). If I simulate a click event,
Uncaught TypeError: element.click is not a function
If I try to call handleChange() and handleBlur()
readonly refO: React.RefObject<HTMLInputElement>;
constructor(props: InputProps) {
super(props);
this.refO = React.createRef<HTMLInputElement>();
}
...
<TextField
autoFocus
inputRef={this.refO}
id={this.labelId}
required={required}
label={label}
error={error}
value={this.setValue()}
onBlur={handleBlur}
onChange={handleChange}
disabled={disabled}
fullWidth
type={type}
InputLabelProps={InputLabelProps}
InputProps={{ className: cx({ [classes.modified]: isModified }) }}
onFocus={this.handleFocus}
/>
What can I do inside handleFocus() in order to achieve this. or the proper approach to achieve this. I'm very new to TypeScript. is my approach wrong or any other way to overcome this.
handleFocus = (element: HTMLInputElement) => {
element.click();
this.props.handleChange();
this.props.handleBlur();
}
PS: cannot add more code due to some sensitivity issues.
solved the issue.
i knew it has to be done from inside the handleFocus. but what i was doing call other event handlers from it. then i thought why should i. the function has been invoked so just update the values from it.
handleFocus = (value) => {
// update my value
}
it's unconventional but all we require is the result.
ps: i even pinged few people on twitter(this has put our reputation on the stake. it was a race against time). i was that desperate. i apolagize to them.
I have an Input element that I want to display an error on when the form validation fails.
<Input ref="amount" error={false} />
When the user enters an incorrect amount, I want to change "error" to "true". How can this be done?
I have tried:
this.refs.amount.props.error = true;
Which seems bad but I'm not sure how else. If I add a conditional statement in the definition of the Input element, that seems to only evaluate once and then remain the same. Do I need to force an update on the element? If so, how?
Yes it's possible to validate the input when the form is submitted.
All you need is to keep track on input value and use same approach as #SajithDilshan for the input error.
this.state = {
error: false,
value: ''
}
...
render(){
return
...
<Input
ref="amount"
value={this.state.value}
error={this.state.error}
/>
...
}
Then onSubmit should looks like:
onSubmit(e){
const isError = this.state.value === '';
this.setState({error: isError});
// rest of your logic
}
Hope it will help!
Use the onChange() method on the input as below.
<Input ref="amount" onChange={this.onInputChange} error={this.state.error} />
After that implement the onInputChange() method as below in your component.
onInputChange = (e) => {
if (e.target.value === "") { // logic to validate the input
this.setState({error: true});
} else {
this.setState({error: false});
}
}
Note that this will add error property to the state.
Further, you should not modify the props within a component. Props are passes from parent component to the child component as immutable inputs.
This is not exactly the answer, but still:
This type of fiddling with each possible state of form element (valid, invalid, warning, show tooltip, was edited, in focus, left focus, was submitted, submit failed or not, etc) becomes to much trouble when the form grows beyond 1 input field.
I would suggest to use redux-form package that integrates with semantic-ui-react` almost perfectly and provided you have provided it with the validate function does everything else for you. It takes some time to understand the basics of it, but it really pays.