Yup & React Hook Form: How to validate onChange rather than onSubmit - javascript

I have an onChange function onNameChange that contains a valid variable that should match the yup validation of the name field. The problem is that the valid variable only seems to be correct after submitting the form, not on changing the name field; I want this to be valid before having to submit.
How can I get the value to be correct on changing the name field rather than submitting? Note that I found a similar post but that uses Formik, which is not what I want to use: Formik + Yup: How to immediately validate form before submit?
The Yup settings:
const schema = Yup.object().shape({
name: Yup.string()
.required("Required")
.min(3, "Enter at least 3 characters")
});
const {
register,
handleSubmit,
setError,
formState: { errors },
trigger
} = useForm({
resolver: yupResolver(schema)
// mode: "onTouched",
// reValidateMode: "onChange"
});
The name changing function:
const onNameChange = async ({ target: { value } }) => {
const valid = await trigger("name");
console.log("valid", valid, "value", value);
if (!valid) {
// #todo: bug here? valid only correct after submitting
return;
}
getPokemon(value);
setShowPokemon(false);
};
The demo form:
<form onSubmit={handleSubmit(onSubmit /*, onError*/)}>
<input
{...register("name", { required: true })}
name="name"
placeholder="Enter a pokemon"
onChange={onNameChange}
/>
<button type="submit" onClick={onSubmit}>
Show Pokemon
</button>
{errors.name && <p>{errors.name.message}</p>}
</form>
I've made a live demo on codesandbox that should be helpful:
https://codesandbox.io/s/react-playground-forked-odwi2?file=/Pokemon.js
Thanks

The problem is that you aren't updating the RHF state after changing your name <input />, because you are overriding the onChange prop, which is returned from {...register('name')}.
So basically you have to options here:
use setValue to update the RHF state value for name inside your onNameChange callback
use <Controller /> component
You can read about it in this discussion on GitHub.
This how it would be implemented for the second option using <Controller />:
<form onSubmit={handleSubmit(onSubmit /*, onError*/)}>
<Controller
name="name"
control={control}
defaultValue=""
render={({ field: { value, onChange, ...field } }) => (
<input
{...field}
onChange={({ target: { value } }) => {
onChange(value);
onNameChange(value);
}}
placeholder="Enter a pokemon"
/>
)}
/>
<button type="submit" onClick={onSubmit}>
Show Pokemon
</button>
{errors.name && <p>{errors.name.message}</p>}
</form>

Related

Show validation feedback to the user

I am making a simple react form with a react-hook-form and yup as the validator. If the user's input is successfully validated, I want to show some feedback to the user, like a green outline. The problem is that I only want to show the green outline when my input has been validated, not every time the component is rendered. How can I do this? Component:
const schema = yup.object().shape({
email: yup
.string()
.required("This field is required"),
password: yup.string().required("This field is required"),
});
export const Form = () => {
const {
register,
handleSubmit,
getValues,
formState: { errors },
} = useForm({
mode: "onBlur",
resolver: yupResolver(schema),
});
return (
<form noValidate onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input
id="email"
{...register("email")}
/>
{errors.email ? errors?.email.message : null}
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register("password")}
/>
{errors.password ? errors?.password.message : null}
<button
type="submit"
>
Submit
</button>
</form>
);
};
You can implement what you want with the touched property. Using the touched property for this kind of scenario is very common. Here's what you can do:
From the useForm hook, you can also extract the touchedFields
const {
register,
handleSubmit,
getValues,
formState: { errors, touchedFields }
} = useForm({
mode: "onBlur",
resolver: yupResolver(schema)
});
The touchedFields properties stores the touched fields. A field is touched the first time the user leaves the focus from that field. So, then you can conditionally show a message about a field if it is touched and it has no errors like this:
{touchedFields.email && !errors.email ? <div>Email ok</div> : null}
You can try this sandbox. I hope you can get the idea from it.

Setting the default value inside the input field in the antd library after calling the function in useEffect

I have the loadProfile function that I want to call in useEffect. If the loadProfile function is called within useEffect, the name Mario should be displayed inside the input name field. How can I set the default value in the antd library inside input? I try to use defaultValue but the input field remains empty.
Example here: https://stackblitz.com/edit/react-311hn1
const App = () => {
const [values, setValues] = useState({
role: '',
name: '',
email: '',
password: '',
buttonText: 'Submit'
});
useEffect(() => {
loadProfile();
}, []);
const loadProfile = () => {
setValues({...values, role, name="Mario", email});
}
const {role, name, email, buttonText} = values;
const updateForm = () => (
<Form
{...layout}
name="basic"
initialValues={{
remember: true,
name: name
}}
>
<Form.Item
label= 'Name'
name='name'
rules={[
{
required: true,
message: 'Please input your name!',
},
]}
>
<Input
defaultValue= {name}
/>
</Form.Item>
<Form.Item
label="Email"
name="email"
value={email}
rules={[
{
type: 'email',
required: true,
message: 'Please input your email!',
},
]}
>
<Input
/>
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
{buttonText}
</Button>
</Form.Item>
</Form>
);
return (
<>
<Row>
<Col span={24} style={{textAlign: 'center'}}>
<h1>Private</h1>
<p>Update Form</p>
</Col>
</Row>
{updateForm()}
</>
);
};
You have to make three changes to your code. This is a working component, I also extracted your component and put the appropriate state in there. I also made it functional.
https://stackblitz.com/edit/react-tlm1qg
First change
setValues({...values, role, name="Mario", email});
to
setValues({...values, name: "Mario"});
This will properly set the state.
Second change:
Next, you should notice that if you set defaultValue="test123" it still won't work, something is up. Remove name from Form.Item and boom it works. test123 shows up. But if you put values.name in there, it still doesn't work!
Third Change:
That's because defaultValue only sets that value right when the component is created, not on mount. So you have to use value={values.name} and it will set that value once on mount per your useEffect
In the demo component I also added a change handler for you so the user can type in there, if you wanted that.
If you look at the FAQ for Ant Design it says:
Components inside Form.Item with name property will turn into
controlled mode, which makes defaultValue not work anymore. Please try
initialValues of Form to set default value.
Ant Design is taking over control - so you don't have to set value={name} and onChange.
You want to set the values AFTER the component is created. So to do that you need
const [form] = Form.useForm();
React.useEffect(() => {
form.setFieldsValue({
username: 'Mario',
});
}, []);
and in you Form make sure you add:
<Form
{...layout}
name="basic"
initialValues={{
remember: true,
}}
form={form} // Add this!
>
I updated my online example.
Big picture - when you want to set the value after creation, use this hook and form.setFieldsValue

Don't Validate untouched fields in Formik and Yup on submitting if field is not empty

I have this validation schema for a form made using withFormik() used in my React application, Here validateJql() is my custom validation function for yup
validationSchema: Yup.object().shape({
rework: Yup.string().required("Rework query is required").validateJql(),
originalEstimate: Yup.string().required("Original Estimate query is required").validateJql()
})
and my form Component is like this:
const addSomeForm = (props) => {
const {
values,
touched,
errors,
isSubmitting,
handleChange,
handleSubmit,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<div>
<label htmlFor="name" className="col-form-label"><b>Rework Query:</b></label>
<textarea id="query.rework" rows="5" type="text" className="form-control" placeholder="Enter JQL with aggregate Function" value={values.query.rework} onChange={handleChange} required />
{errors.query && errors.query.rework && touched.query && <span className="alert label"> <strong>{errors.query.rework}</strong></span>}
</div>
</div>
<div className="form-group">
<div>
<label htmlFor="name" className="col-form-label"><b>Original Estimate:</b></label>
<textarea id="query.originalEstimate" rows="5" type="text" className="form-control" placeholder="Enter JQL with aggregate Function" value={values.query.originalEstimate} onChange={handleChange} required />
{errors.query && errors.query.originalEstimate && touched.query && <span className="alert label"> <strong>{errors.query.originalEstimate}</strong></span>}
</div>
</div>
</form>
)
Now, what I want to do is not to run validation on form submit if the field rework and originalEstimate is not touched and also not empty. How can I achieve this with withFormik HOC or Yup? I have partially been through Yup docs and Formik docs but could not find something to fit with my problem.
This is the case after submitting the form once and editing after that for minor tweaks in some of those multiple fields. if there are multiple fields and only some are edited, I don't want to run validation for all the fields existed.
Thank you in advance.
This is the default desired behavior as stated in formik docs but i think you can do the following:
Instead of using validationSchema, use validate function.
Validate function will work the same way your validationSchema works. You just need to use Yup programmatically from a function with mixed.validate
So you can have the full control of all the props in your form. You could also use the getFieldMeta to get the touched and value of the field and use that in your validation. Or get those props from touched object in form with getIn
Something like:
// Some util functions
function mapYupErrorsToFormikErrors(err: { inner: any[] }) {
return err.inner
.filter((i: { path: any }) => !!i.path)
.reduce(
(curr: any, next: { path: any; errors: any[] }) => ({
...curr,
[next.path]: next.errors[0],
}),
{},
)
}
function validateSchema(values: object, schema: Schema<object>) {
return schema
.validate(values, {
abortEarly: false,
strict: false,
})
.then(() => {
return {}
})
.catch(mapYupErrorsToFormikErrors)
}
// Your validation function, as you are using `withFormik` you will have the props present
function validateFoo(values, props) {
const { touched, value } = props.getFieldMeta('fooFieldName') // (or props.form.getFieldmeta, not sure)
const errors = validateSchema(values, yourYupSchema)
if (!touched && !value && errors.fooFieldName) {
delete errors.fooFieldName
}
return errors
}
Well, touched might not work for your use case because formik probably would set it to true on submission, but there you have all the props and you can use something different, like the empty value or some other state prop you manually set. You got all the control there.
I had a similar issue, I ended up creating another field where I set the value when showing the edit screen. Then i compare inside a test function like this :
originalField: yup.string().default(''),
field: yup.string().default('').required('Field is required.').test('is-test',
'This is my test.',
async (value, $field) => {
if($field.parent.originalField !== '' && value === $field.parent.originalField) return true
return await complexAsyncValidation(value)
}
Not perfect, but definitely working

How to make validation to work for redux-form field generated from an array

I am trying to use redux-form to generate a quiz form. My data source for an individual redux-form field component comes from an array - questions in my case. Everything works as expected except validation. Any thoughts how this can be fixed?
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { Input, Button } from 'reactstrap';
const validate = values => {
const errors = {};
if (!values.question) { // this is just an example of what I am trying to do, validation does not work
errors.question = 'Required';
} else if (values.question.length < 15) {
errors.question = 'Must be 15 characters or more';
}
return errors;
};
const renderField = ({ input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<Input {...input} type={type} />
{touched && (error && <span>{error}</span>)}
</div>
</div>
);
const renderQuestions = questions => {
return questions.map(question => {
return (
<Field key={question.id} name={question.prompt} type="textarea" component={renderField} label={question.prompt} />
);
});
};
const QuizStepForm = props => {
const { handleSubmit, pristine, reset, submitting, questions } = props;
return (
<form onSubmit={handleSubmit}>
<Field name="username" type="textarea" component={renderField} label="username" />
{renderQuestions(questions)}
<div>
<br />
<Button color="primary" style={{ margin: '10px' }} type="submit" disabled={submitting}>
Submit
</Button>
<Button type="button" disabled={pristine || submitting} onClick={reset}>
Clear Values
</Button>
</div>
</form>
);
};
export default reduxForm({
form: 'quizStepForm',
validate
})(QuizStepForm);
Your validation function assumes there is one field named "question." But your code creates a set of fields whose name is set by {question.prompt}. If you stick with this implementation, your validation code will need to know about all the question.prompt array values and check values[question.prompt] for each one, then set errors[question.prompt] for any failures. That would probably work, though it seems like a suboptimal design.
This might be a good use case for a FieldArray. In FieldArrays, the validation function is called for you on each field; your validation code doesn't have to know the names of all the fields.

react-bootstrap: clear element value

I'm trying to clear my input fields after an onClick event.
I'm using react-bootstrap library and while there is a getValue() method, there is not setValue(value) method.
I've stumbled upon this discussion .
I did not fully understand what they are suggesting in order to simply clean a form after submission.
After all, If I would use a simple HTML <input> instead of react-bootstrap I could grab the node via element ref and set it's value to be empty string or something.
What is considered a react way to clean my react-bootstrap <Input /> element?
Store the state in your React component, set the element value via props, get the element value via event callbacks. Here is an example:
Here is an example taken directly from their documentation. I just added a clearInput() method to show you you can clear the input by just updating the state of your component. This will trigger a re-render which will cause the input value to update.
const ExampleInput = React.createClass({
getInitialState() {
return {
value: ''
};
},
validationState() {
let length = this.state.value.length;
if (length > 10) return 'success';
else if (length > 5) return 'warning';
else if (length > 0) return 'error';
},
handleChange() {
// This could also be done using ReactLink:
// http://facebook.github.io/react/docs/two-way-binding-helpers.html
this.setState({
value: this.refs.input.getValue()
});
},
// Example of how you can clear the input by just updating your state
clearInput() {
this.setState({ value: "" });
},
render() {
return (
<Input
type="text"
value={this.state.value}
placeholder="Enter text"
label="Working example with validation"
help="Validation is based on string length."
bsStyle={this.validationState()}
hasFeedback
ref="input"
groupClassName="group-class"
labelClassName="label-class"
onChange={this.handleChange} />
);
}
});
For what I'm doing at the moment, I didn't really think it was necessary to control the Input component's value through setState/Flux (aka I didn't want to deal with all the boilerplate)...so here's a gist of what I did. Hopefully the React gods forgive me.
import React from 'react';
import { Button, Input } from 'react-bootstrap';
export class BootstrapForm extends React.Component {
constructor() {
super();
this.clearForm = this.clearForm.bind(this);
this.handleSave = this.handleSave.bind(this);
}
clearForm() {
const fields = ['firstName', 'lastName', 'email'];
fields.map(field => {
this.refs[field].refs['input'].value = '';
});
}
handleSubmit() {
// Handle submitting the form
}
render() {
return (
<div>
<form>
<div>
<Input
ref='firstName'
type='text'
label='First Name'
placeholder='Enter First Name'
/>
<Input
ref='lastName'
type='text'
label='Last Name'
placeholder='Enter Last Name'
/>
<Input
ref='email'
type='email'
label='E-Mail'
placeholder='Enter Email Address'
/>
<div>
<Button bsStyle={'success'} onClick={this.handleSubmit}>Submit</Button>
<Button onClick={this.clearForm}>Clear Form</Button>
</div>
</div>
</form>
</div>
);
}
}
If you use FormControl as an input, and you want to use ref to change/get value from it, you use inputRef instead of ref
<FormControl inputRef={input => this.inputText = input} .../>
and use this to get/change its value:
this.inputText.value
This worked for me in case someone else is looking for an answer :D
You can access the values of react-bootstrap by using
console.log(e.target.form.elements.fooBar.value)
You can clear them by using
e.target.form.elements.fooBar.value = ""
import React from 'react';
import {Button, Form} from 'react-bootstrap';
export default function Example(props) {
const handleSubmit = (e) => {
// Handle submitting the form
}
const resetSearch = (e) => {
e.preventDefault();
e.target.form.elements.fooBar.value = ""
}
render() {
return (
<Form onSubmit={handleSubmit} onReset={resetSearch} >
<Form.Control type="input" name="fooBar" />
<Button type="submit"> Submit </Button>
<Button onClick={resetSearch} type="submit" > Reset </Button>
</Form>
);
}
}
You can also just use ReactDOM:
<FormControl
componentClass="select"
ref="sStrike">
<option value="-">Select…</option>
<option value="1">1</option>
<option value="2">2</option>
</FormControl>
Now a different FormControl fires an onChange={handleChange} and in that handler you can just id the ref and set to the default value:
ReactDOM.findDOMNode(this.refs.sStrike).value = '-';
and that will set the select box to the 'default' value.
Just add a button in-side form with the attribute type="reset"
<Button variant="primary" type="reset">Reset</Button>

Categories