I am building form using react-form-hook. I am stuck on the validation part.
I don't want to use client side validation to show errors. I want to validate the touched fields server side and show the returned errors on the client side.
The form got submitted on the onBlur and i am able to show the errors. But i couldn't track changed fields and only submit the form. Any help would be appreciated.
Here is the code snippet that I have come up with.
const {
register,
handleSubmit,
watch,
setError,
setValue,
formState: { isLoading, errors, touchedFields },
} = useForm({
defaultValues: { ...form },
});
async function handleFocusOut(formData) {
console.log(formData, touchedFields);
try {
const { data } = await axios.post(`/api/ads/publish`, formData);
console.log(data);
} catch (error) {
const {data, status} = error.response;
if(status === 422) {
const errs = {...data.errors};
Object.keys(errs).filter(field => !touchedFields[field]).filter(field => delete errs[field]);
Object.keys(errs).map(
(field) => setError(field, {message: errs[field][0], type: 'custom'})
);
console.log(errors);
}
toast(error.message);
console.log(error);
}
return;
}
return (
<form onBlur={handleSubmit(handleFocusOut)}>
{fields.map((field) => (
<Fragment key={field.key}>
{field.type === "text" && (
<div className="mb-4">
<TextInput
register={register}
id={field.key}
label={field.label}
name={field.name}
settings={field.settings}
/>
{errors[field.name] && <p className="text-xs text-red-500">{errors[field.name].message}</p>}
</div>
)}
{field.type === "textarea" && (
<div className="mb-4">
<TextareaInput
register={register}
id={field.key}
label={field.label}
name={field.name}
settings={field.settings}
/>
{errors[field.name] && <p className="text-xs text-red-500">{errors[field.name].message}</p>}
</div>
)}
</Fragment>
))}
</form>
)
Related
I am writing a test for checking if removeItem is called successfully on logout. The test passes but I get an error that "the LoginForm inside a test was not wrapped in act(...)".
Here is the LoginForm component code:
const LoginForm = () => {
const [loggedIn, setLoggedIn] = useState(false);
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<LoginData>();
const onSubmit: SubmitHandler<LoginData> = async (data) => {
try {
const response = await loginUser(data);
if (response.status === 200) {
localStorage.setItem("accessToken", response.data.accessToken);
setLoggedIn(true);
toast.success("login successful");
reset();
}
} catch (error: any) {
toast.error(error.response.data.error);
reset();
}
};
const logoutUser = () => {
try {
localStorage.removeItem("accessToken");
setLoggedIn(false);
toast.success("logout successful");
} catch (error) {
toast.error("Could not logout");
}
};
return (
<main>
<ToastContainer />
<form data-testid="login-form" onSubmit={handleSubmit(onSubmit)}>
<h1>User Login:</h1>
<div className="form__input">
<input
type="text"
id="email"
placeholder="Email Address"
data-testid="email"
{...register("email", { required: "Email address is required" })}
/>
{errors.email && (
<span className="errorMsg" role="alert">
{errors.email.message}
</span>
)}
</div>
<div className="form__input">
<input
type="password"
id="password"
placeholder="Password"
data-testid="password"
{...register("password", { required: "Password is required" })}
/>
{errors.password && (
<span className="errorMsg" role="alert">
{errors.password.message}
</span>
)}
</div>
{loggedIn === false ? (
<button className="submitBtn" type="submit">
Login
</button>
) : (
<button
data-testid="logoutBtn"
className="submitBtn"
onClick={logoutUser}
>
Logout
</button>
)}
</form>
</main>
);
};
I believe its a problem with async functions and have tried wrapping the expect statement in await waitFor(())=>{} but the problem persists.
Here is the test code:
it("should remove accessToken from localStorage on logout", async () => {
render(<Login />);
const email = await screen.findByRole("textbox");
const password = await screen.findByPlaceholderText("Password");
const loginBtn = await screen.findByRole("button", {
name: /login/i,
});
fireEvent.change(email, { target: { value: "email" } });
fireEvent.change(password, { target: { value: "password" } });
fireEvent.click(loginBtn);
mockedAxios.post.mockImplementation(() =>
Promise.resolve({
status: 200,
data: { accessToken: "eYagkaogk...", refreshToken: "eyAagga..." },
})
);
const removeItem = jest.spyOn(Storage.prototype, "removeItem");
const logoutBtn = await screen.findByRole("button", {
name: /logout/i,
});
fireEvent.click(logoutBtn);
await waitFor(() => {
expect(removeItem).toHaveBeenCalledWith("accessToken");
});
});
The reason this happens if that when you click the login button, it launches a promise that later comes back and sets a new react state. Since there is nothing in the test that waits until that happens, it means the test is potentially flaky since it may or may not be complete before your final assertion, which means any assertions aren't guaranteed to be happening on the same predictable component state.
Add an extra wait for this to finish.
// ...
fireEvent.click(loginBtn);
mockedAxios.post.mockImplementation(() =>
Promise.resolve({
status: 200,
data: { accessToken: "eYagkaogk...", refreshToken: "eyAagga..." },
})
);
await waitFor(() => expect(mockedAxios.post).toHaveBeenCalledTimes(1))
// ...
It may be necessary to be more precise than toHaveBeenCalledTimes and to also assert on the URL that was used. This is a bit brittle without this as this could capture other calls to axios and the problem would remain. You only care about capturing the login request.
I'm very new to this, so I'm assuming it's a stupid little problem that I'm unware of.
I have form that renders input lines based off the amount of setpoints it gets from the backend. I have put in several console.log, and they all function fine until I get to the input.jsx. My console.log() in input never gets called, and the code just seems to.. forget about the input.
main code:
import React from "react";
import Joi from "joi-browser";
import Modal from "react-bootstrap/Modal";
import Button from "react-bootstrap/button";
import Form from "./common/form";
import axios from "axios";
class SetpointsModal extends Form {
state = {
data: {},
errors: {},
};
schema = {};
doSubmit = async () => {
const { data } = await axios.put(
`/api/${this.props.id}_setpoints`,
this.state.data
);
console.log(data);
};
async componentDidMount() {
const { data } = await axios.get(`/api/${this.props.id}_setpoints`);
Object.keys(data).forEach((key) => {
this.schema[key] = Joi.number().label(data[key].label);
});
this.setState({ data });
}
render() {
const { title, show, handleClose } = this.props;
return (
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>{title} Setpoints</Modal.Title>
</Modal.Header>
<form onSubmit={this.handleSubmit}>
<Modal.Body>
{Object.keys(this.state.data).forEach((field) => {
this.renderInput(field);
})}
</Modal.Body>
<Modal.Footer>
{this.renderBtn("Update")}
<Button variant="secondary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</form>
</Modal>
);
}
}
export default SetpointsModal;
Handle submit + render Input:
handleSubmit = (e) => {
e.preventDefault();
const errors = this.validate();
console.log(errors);
this.setState({ errors: errors || {} });
if (errors) return;
this.doSubmit();
};
renderInput(name) {
const { errors } = this.state;
const { label, data } = this.state.data[name];
console.log(name);
return (
<Input
name={name}
label={label}
value={data}
error={errors[name]}
onChange={this.handleChange}
/>
);
Input:
import React from "react";
const Input = ({ name, label, error, ...rest }) => {
return (
<div className="form-group">
<label htmlFor={name}>{label}</label>
<input {...rest} id={name} name={name} className="form-control" />
{error && <div className="alert alert-danger">{error}</div>}
</div>
);
};
export default Input;
forEach here won't be doing what you want it to do; it will 'do' something on each iteration but won't 'return' anything.
Using map might be more in line with what you were hoping for (docs here); this will return an array of components which will render out.
I'd suggest the following change in your render (you will need to pass a key down to each Input from your function):
{Object.keys(this.state.data).map((field, key) => (
this.renderInput(field, key);
))}
I have the following Form:
const MyForm = () => {
return (
<>
<Formik
validateOnChange={true}
initialValues={{ plan: "", email: "", name: "" }}
validate={values => {
console.log(values)
const errors = {}
if (values.plan !== "123" && values.plan !== "456") {
errors.plan = "Not valid"
} else if (values.plan === "") {
errors.plan = "Please enter something"
}
if (!values.email) {
errors.email = "Please provide an e-mail address."
} else if (
!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = "Please provide a valid e-mail address."
}
return errors
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false)
}, 400)
}}
>
{({ isSubmitting, errors }) => (
<Form>
<FieldWrapper>
<InputField type="text" name="plan" label="plan" />
<StyledErrorMessage name="plan" component="div" />
</FieldWrapper>
<Button
disabled={errors.plan}
>
Continue
</Button>
</Form>
)}
</Formik>
</>
)
}
I have a Continue Button and I want it to be disabled if there are any errors. I am doing <Button disabled={errors.plan}> and this works.
However: it does not disable to Button when the user just doesn't touch the field at all - since then, the validation isn't called and consequently, there won't be any errors in the error object. So initially, the button is not disabled.
How can I circumvent this?
I'm not too familiar with Formik, but could you add a state for completed status of the form, that is initially set to false, and when completed setState(true). Then your conditional for <Button> can check both errors.plan && completedState.
I am setting a state variable with useState whether or not a recaptcha has been clicked, but when I use the setState function my captcha variable doesn't pass ot my on submit, don't understand why, If I remove the setState the captcha variable passes just fine to my onSubmit.
If I remove setSubmitButton(false) everything works fine, not quite sure why.
When I run the setSubmittButton(false) captcha is endefined in my submit function when I dont have it there I get the correct captcha value in my submit function.
import React, { useState } from "react"
import { useForm } from "react-hook-form"
import ReCAPTCHA from "react-google-recaptcha"
const ContactForm = () => {
const { register, handleSubmit, watch, errors } = useForm()
const isBrowser = typeof window !== `undefined`
let location
if (isBrowser) {
location = window.location.hostname
}
let fetchUrl
if (location === "localhost") {
fetchUrl = `http://localhost:8888/.netlify/functions/contact`
} else if (location === "fsdf.gtsb.io") {
fetchUrl = `https://fdsfd/.netlify/functions/contact`
} else {
fetchUrl = "/.netlify/functions/contact"
}
console.log(fetchUrl)
const onSubmit = async data => {
setLoading(true)
console.log(captcha, "captcha value final")
const response = await fetch(fetchUrl, {
method: "POST",
body: JSON.stringify({ data, captcha: captcha }),
})
.then(response => response.json())
.then(data => {
console.log(data)
if (data.captcha === false) {
setCaptchaFailed(true)
}
})
.catch(error => {
console.error("Error:", error)
})
}
const [submitButton, setSubmitButton] = useState(true)
const [loading, setLoading] = useState(false)
const [captchaFailed, setCaptchaFailed] = useState(false)
let captcha
function onChange(value) {
setSubmitButton(false) // IF I REMOVE THIS LINE EVERYTHING WORKS FINE ******
console.log("Captcha value:", value)
captcha = value
}
function error(value) {
alert(value)
}
return (
<>
{/* "handleSubmit" will validate your inputs before invoking "onSubmit" */}
{!loading ? (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label for="name">Name</label>
<input name="name" ref={register({ required: true })} />
{errors.name && <span>This field is required</span>}
</div>
<div>
<label for="email">Email</label>
<input
type="email"
name="email"
ref={register({ required: true })}
/>
{errors.email && <span>This field is required</span>}
</div>
<div>
<label for="message">Message</label>
<textarea name="message" ref={register({ required: true })} />
{errors.message && <span>This field is required</span>}
</div>
<ReCAPTCHA
sitekey="fdsfsa"
onChange={onChange}
onErrored={error}
/>
<input
type="submit"
className={submitButton ? "disabled" : ""}
disabled={submitButton ? "disabled" : ""}
/>
</form>
) : (
<>
{captchaFailed ? (
<>
<p>Captcha Verification Failed</p>
</>
) : (
<h1>Loading</h1>
)}
</>
)}
</>
)
}
export default ContactForm
You should store the captcha value in a state variable (or ref) instead of a plain JS variable. The plain JS variable will reset to undefined when the component re-renders (after the setSubmitButton changes the state).
const [ captchaValue, setCaptchaValue ] = useState(null);
function onChange(value) {
setSubmitButton(false);
console.log("Captcha value:", value);
setCaptchaValue(value);
}
const onSubmit = async data => {
setLoading(true)
console.log(captchaValue, "captcha value final");
...
}
Tip: Also, make sure to replace all the occurrences of the for html attributes to htmlFor. You can use an HTML-to-JSX transformer for automating such tasks.
im fairly new to redux.
What im trying to validate is, two key fields cannot hold the same value and all fields are required. For the required part, i am using Field-Level Validations and that seems to work fine. For deciding whether an element already exists, i am using Sync Validation.
When the sync validation works, i checked it with console log. It catching the error and adding it to the errors object. But my form is not showing that.. is it not binded? What am i missing here?
I have added 'onFocus' and 'onBlur' mouse events to the text-fields, to make then readonly onBlur. They seem to be working fine. But the moment i added that, my {touched && error && <span>{error}</span>} error stops getting displayed. What am i doing wrong here?
my form
const required = value => (value ? "" : "required")
class CreateObject extends React.Component {
enableTextField = (e) => {
document.getElementById(e.target.id).removeAttribute("readonly");
}
disableTextField = (e) => {
document.getElementById(e.target.id).setAttribute("readonly", true);
}
renderField = ({ input, label, type, id, meta: { touched, error } }) => (
<React.Fragment>
{touched && error && <span>{error}</span>}
<FormControl {...input} type={type} placeholder={label} id={id}
className={`align-inline object-field-length ${error ? 'error' : ''}`}
onFocus={this.enableTextField.bind(this)}
onBlur={this.disableTextField.bind(this)}
/>
</React.Fragment>
);
renderObjects = ({ fields, meta: { touched, error, submitFailed, errors } }) => {
return (
<ul>
<li>
<center>
<Button bsStyle="success" onClick={() => fields.push({})}>Add New Object</Button>
</center>
</li>
{fields.map((object, index) => (
<li key={index}>
<br />
<center>
<Field
name={`${object}.key`}
type='text'
component={this.renderField}
validate={required}
label="Key"
id={`${object}.key`}
/>
<div className="divider" />
<Field
name={`${object}.method`}
type='text'
component={this.renderField}
label="Method"
validate={required}
id={`${object}.key` + `${object}.method`}
/>
<div className="divider" />
<Field
name={`${object}.value`}
type='text'
component={this.renderField}
label="Value"
validate={required}
id={`${object}.key` + `${object}.value`}
/>
<div className="divider" />
<span
className="align-inline"
onClick={() => fields.remove(index)}
className="allIcons mdi mdi-delete-forever"
/>
</center>
</li>
)
)}
</ul>
);
}
submit() {
//this
}
render() {
const { handleSubmit, pristine, reset, submitting, invalid } = this.props;
console.log(this.props);
return (
<form onSubmit={handleSubmit(this.submit.bind(this))}>
<FieldArray name='objects' component={this.renderObjects} />
<center>
<Button className="align-inline" type="submit" disabled={pristine || submitting || invalid}>Submit</Button>
<div className="divider" />
<Button className="align-inline" disabled={pristine || submitting} onClick={reset}> Clear All Values </Button>
</center>
</form>
);
}
}
export default reduxForm({
form: 'ObjectRepo',
validate
})(CreateObject);
validate.js
const validate = values => {
const error = {}
if (!values.objects || !values.objects.length) {
error.objects = { _error: 'At least one object must be entered' }
} else {
const objectArrayErrors = []
values.objects.forEach((object, objectIndex) => {
const objectErrors = { _error: 'Object Key should be unique' }
if (values.objects.filter(item => item.key == object.key).length == 2) {
objectArrayErrors[objectIndex] = objectErrors
}
})
if (objectArrayErrors.length) {
error.objects = objectArrayErrors
}
}
console.log(error)
return error
}
export default validate
Thanks a lot in advance!
You might want to look at the code below which works for me. This is your container (or smart component if you wish)
export const validateProps = {
name: [required],
value: [required, intOrFloat, maxPercent],
someId: [required],
}
export const transformer = new TypesModel({
name: String,
value: Number,
someId: Number,
})
export default createFormContainer(
formName,
'your_form',
transformer,
validateProps,
mapStateToProps,
mapDispatchToProps,
null,
false,
null,
onSuccessSubmit)(YourFormContainer)
And the code for types model is
class TypesModel {
constructor(schema) {
this.schema = schema
}
transform(data) {
const keys = Object.keys(this.schema)
const result = {}
for (let index = 0, len = keys.length; index < len; index += 1) {
const keyName = keys[index]
try {
result[keyName] = this.schema[keyName](data[keyName])
} catch (e) {
throw new Error(`Type conversion for field "${keyName}" failed`)
}
}
return result
}
}
export default TypesModel
You want validators to look like:
export const required = value => isEmpty(value) &&
'Required field'
export const intOrFloat = value => (!isInt(`${value}`) && !isFloat(`${value}`)) &&
'Must be an integer of float'