I'm trying to make form validation with react-hook-form. It works fine exept one propblem: it doesn't check input type. I want user to input only URL address, but this thing validate it as a simpte text. Where did I make a mistake?
function EditAvatarPopup({ isOpen, onClose, onUpdateAvatar, submitButtonName }) {
const { register, formState: { errors }, handleSubmit } = useForm();
const [link, setLink] = useState('');
function handleInput(e) {
setLink(e.target.value)
}
function handleSubmitButton() {
console.log(link)
onUpdateAvatar({
avatar: link
});
}
return (
<PopupWithForm
name="change-avatar"
title="Update avatar"
submitName={ submitButtonName }
isOpen={ isOpen }
onClose={ onClose }
onSubmit={ handleSubmit(() => handleSubmitButton() ) }
>
<label htmlFor="userAvatar" className="form__form-field">
<input
id="userAvatar"
type="url"
{ ...register('userAvatar', {
required: "Enter URL link",
value: link
})
}
placeholder="url link"
className="form__input"
onChange={ handleInput }
/>
{ errors.userAvatar && (<span className="form__error">{ errors.userAvatar.message }</span>) }
</label>
</PopupWithForm>
);
}
It looks like type="url" doesn't work, but I can't figure out why
Here is a minimal working example: https://codesandbox.io/s/react-hook-form-js-forked-ml3zx?file=/src/App.js
I recommend to not overwrite onChange props, instead of using const [link, setLink] = useState(''); you can use const link = watch('userAvatar').
You can remove the value property, it's not necessary:
<input
id="userAvatar"
type="url"
{ ...register('userAvatar', {
required: "Enter URL link",
value: link //⬅️ Remove that line
})
}
placeholder="url link"
className="form__input"
onChange={ handleInput } //⬅️ Remove that line
/>
React Hook Form - Watch API
Related
Background Information:
So I'm working on a project that includes sending form data to a database. For now I'm just using a Mock API to get everything up and running before setting up the database in .NET.
Custom Hook and Validator information:
I'm using a custom hook with 2 main functions: handleChange() which is responsible for setting the new values on the main component, CreateUser, and handleSubmit(), which calls on my custom validator (validateNewUser) to check for errors in the form.
My hook and validator are working fine, but I'm having issues with the Axios post request. I am able to place my Axios post request in my custom hook and it works as intended, but I cannot figure out how to send the form to the Mock API only when the form is correctly filled out.
Trials and Errors so far:
I have tried using an if statement in my handlePost function (which I think is the correct way to do this, I could just be missing something, I could be wrong), I tried putting the Axios request inside of a handlePost function but that yielded no progress.
The way I have it set currently works, but basically anytime you click submit it sends the data to the API, most likely because it submits the form every time. The other way I could do it is possibly setting the errors to pop up as the user fills out the form? So that the form is accurate before it even is submitted.
I'm pretty new to React so I apologize if my question/issue is confusing. If more information is needed I will provide what I can.
The code for all parts involved will be below, and any and all input is greatly appreciated.
Main Code:
useCreateUser.jsx:
import { useState } from 'react';
import axios from 'axios';
const useCreateUser = (validateNewUser) => {
const [values, setValues] = useState({
firstName: '',
lastName: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const handleChange = e => {
const {name, value} = e.target
setValues({
...values,
[name]: value
});
};
const handleSubmit = e => {
e.preventDefault();
setErrors(validateNewUser(values));
const user = {
firstName: values.firstName,
lastName: values.lastName,
email: values.email,
password: values.password
};
if (handleSubmit){
axios.post(url, user)
.then((response) => {
console.log(response.data);
})
}
}
return {handleChange, values, handleSubmit, errors}
}
export default useCreateUser;
validateNewUser.jsx:
export default function validateNewUser(values) {
let errors = {}
//Validate First Name
if(!values.firstName.trim()) {
errors.firstName = 'First Name is Required.'
}
//Validate Last Name
if(!values.lastName.trim()) {
errors.lastName = 'Last Name is Required.'
}
//Validate Email
if(!values.email){
errors.email = 'Email is Required.'
} else if(!/\S+#\S+\.\S+/.test(values.email)) {
errors.email = 'Invalid Email Address.'
}
if(!values.password){
errors.password = 'Password is Required.'
} else if(values.password.length < 6) {
errors.password = 'Password must be greater than 6 characters.'
}
return errors;
}
CreateUser.jsx:
import Header from "../Header";
import { Form, Button } from "semantic-ui-react";
import { Box } from "#mui/material";
import useCreateUser from "../../hooks/useCreateUser";
import validateNewUser from "../../validators/validateNewUser";
const CreateUser = () => {
const {handleChange, values, handleSubmit, errors} = useCreateUser(validateNewUser);
return (
<Box m="20px">
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
margin="0 auto"
marginBottom="20px"
>
<Header title="Create a New User" subtitle="Please ensure to select the correct role."/>
</Box>
<Form onSubmit={handleSubmit}>
<Form.Field>
<label>First Name</label>
<input placeholder='First Name' name='firstName' type='text' value={values.firstName} onChange={handleChange} />
{errors.firstName && <p style={{color: "red"}}>{errors.firstName}</p>}
</Form.Field>
<Form.Field>
<label>Last Name</label>
<input placeholder='Last Name' name='lastName' type='text' value={values.lastName} onChange={handleChange} />
{errors.lastName && <p style={{color: "red"}}>{errors.lastName}</p>}
</Form.Field>
<Form.Field>
<label>Email</label>
<input placeholder='Email' name='email' type='email' value={values.email} onChange={handleChange} />
{errors.email && <p style={{color: "red"}}>{errors.email}</p>}
</Form.Field>
<Form.Field>
<label>Password</label>
<input placeholder='Password' name='password' type='password' value={values.password} onChange={handleChange} />
{errors.password && <p style={{color: "red"}}>{errors.password}</p>}
</Form.Field>
<Button type='submit'}>Submit</Button>
</Form>
</Box>
)
}
export default CreateUser;
Few pointers
It is better to give feedback to the user as he enters the data
// validate on change
const handleChange = e => {
const { name, value } = e.target;
const updatedValues = { ...values, [name]: value };
setValues(updatedValues)
const validationErrors = validateNewUser(updatedValues);
setErrors(validationErrors)
};
On Submit you can return the first error, so that the form can scroll or focus on the field with the error.
const handleSubmit = async (e) => {
e.preventDefault();
// this might not be needed
// you can do validations which need api calls
// but event those can be done on input
// setErrors(validateNewUser(values));
// return error code
const firstErrorKey = Object.keys(error)[0]
if (firstErrorKey !== undefined) {
return { hasError: true }
}
const user = {
firstName: values.firstName,
lastName: values.lastName,
email: values.email,
password: values.password
};
const response = await axios.post(url, user)
console.log(response.data);
// return success code depending on server response
// so u can show success
// return { isSuccess: response.data?.code === 0 }
}
}
in form you can set a loader set a loader
const onFormSubmit = async e => {
try {
setState('submitting')
const response = await handleSubmit(e);
if (response.isSuccess) {
// show alert/toast
}
if (response.hasErrors) {
// focus on first error control
const firstErrorKey = Object.keys(error)[0]
if (firstErrorKey !== undefined) {
// focus on control with error
}
}
setState('success')
} catch(error) {
setState('error')
}
}
<Form onSubmit={handleSubmit}>
</Form>
If you can seperate the form input state from api logic it will make the code more maintainable.
If possible use libraries like Formik, they handle the form state and allow you to focus on the business part of the code, like api call, validations
Thinking of component states will also help you design better components
Hope it helps in some way,
Cheers
I have a component in React that has required in its fields. The idea is onClick will trigger a separate function that adds a soccer player to two different stores (similar to a TODO app). However, it appears that the validation does not work -name for example is a required string, but the form seems to add a player even if the string is empty. The same goes for the two number inputs.
I don't want to use onSubmit because this seems to refresh the page every time, which causes me to lose data.
I'm using the react-modal library for my forms. To start with here's a function that opens the modal:
function renderAddButton() {
return (
<Row
horizontal='center'
vertical='center'
onClick={openModal}
>
Add Player
</Row>
);
}
Here's the modal and its hooks:
const [playerName, setPlayerName] = useState('');
const [totalGoals, setTotalGoals] = useState(0);
const [goalPercentage, setGoalPercentage] = useState(0);
function openModal() {
setIsOpen(true);
}
function closeModal() {
setIsOpen(false);
}
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
style={modalStyles}
contentLabel='Player Form Modal'
>
<h3>Create Player</h3>
<form>
<label>Name</label>
<input
type='string'
id='playerNameId'
name='playerName'
defaultValue=''
onChange={(e) => setPlayerName(e.target.value)}
required
/>
<label>Total Goals</label>
<input
type='number'
id='totalGoalsId'
name='totalGoals'
defaultValue='0'
min='0'
onChange={(e) => setTotalGoals(e.target.value)}
required
/>
<label>Goal Percentage</label>
<input
type='number'
id='goalPercentageId'
name='playerGoalPercentage'
defaultValue='0'
min='0'
step ='0.01'
max='1'
onChange={(e) => setGoalPercentage(e.target.value)}
required
/>
<button onClick={(e) => onAddButtonClick(e)}>Submit</button>
</form>
<button onClick={closeModal}>close</button>
</Modal>
And now when this function is triggered, the validations don't seem to work. Empty playerId and totalGoals and goalPercentage seem to go through fine. How do I validate the inputs and stop this function from running if the inputs are empty?
function onAddButtonClick(e) {
e.preventDefault();
setItems((prev) => {
const newItems = [...prev];
const uuid= uuidv4();
newItems.push({
name: playerName,
playerId:uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
playersStore.push({
name: playerName,
playerId:uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
return newItems;
});
}
The required attribute only works with default form actions. You'll need to do your own validation in the handler. You should also explicitly define the button type attribute as well since buttons by default are type="submit".
Create a validation function and pass your state values to it. Return true if input is valid, false otherwise.
const validateInput = ({ goalPercentage, playerName, totalGoals }) => {
if (!playerName.trim()) {
return false;
}
// other validations
return true;
};
Check the input in onAddButtonClick and only update state if input is valid.
function onAddButtonClick(e) {
e.preventDefault();
const validInput = validateInput({ goalPercentage, playerName, totalGoals });
if (!validInput) {
return null;
}
setItems((prev) => {
const newItems = [...prev];
const uuid= uuidv4();
newItems.push({
name: playerName,
playerId: uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
playersStore.push({
name: playerName,
playerId: uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
return newItems;
});
}
Update the button to have an explicit type.
<button
type="button"
onClick={onAddButtonClick}
>
Submit
</button>
you can do something like this
function onAddButtonClick(e) {
e.preventDefault();
if(playerName.trim()===''){ //you can make more validations here
return;
}
//down here use the rest of logic
}
I think I'm doing things right as far as my code is concerned. In this moment, i can't write in the inputs. Someone knows what happen? Below I attach my code:
const Login: SFC<LoginProps> = ({ history }) => {
const alertContext = useContext(AlertContext);
const { alert, showAlert } = alertContext;
const authContext = useContext(AuthContext);
const { message, auth, logIn } = authContext;
useEffect(() => {
if (auth) {
history.push('/techs');
}
if (message) {
showAlert(message.msg, message.category);
}
}, [message, auth, history]);
const [user, saveUser] = useState({
email: '',
password: ''
});
const { email, password } = user;
const onChange = (e: any) => {
saveUser({
...user,
[e.target.name]: e.target.value
})
}
const onSubmit = (e: any) => {
e.preventDefault();
if (email.trim() === '' || password.trim() === '') {
showAlert('All fields are required', 'alert-error');
}
logIn({ email, password });
}
return (
<>
...
<form onSubmit={onSubmit}>
<input type="text" ... value={email} onChange={onChange} />
<input type="password" ... value={password} onChange={onChange} />
<input type="submit" className="fadeIn third" value="Log In" />
</form>
...
</>
);
}
export default Login;
And this is what the component looks like:
It must be because your onChange isn't working the way you expect it to so the value of email is never changing.
I can see two possible issues.
in your onChange, why are you saving e.target.value to e.target.name? Unless the name prop is in your code but you left it out as part of the ellipsis. In which case, it's probably number 2:
Try having email and password as individual pieces of state (strings created with individual calls to useState). This will require two change handlers (or a more creative single handler, and if you indeed have a name prop then your current handler might already work. If the name prop is working). You're never using the user object anyway, and it's possible/likely that this is actually your Actual problem (not just a best practice).
I have a test that unfortunately reveals some serious misunderstanding on my part on how to test this React web application using react-testing-library. The test:
const setup = () => {
const utils = render(<UnderConstruction />)
const input = utils.getByLabelText('Email')
return {
input,
...utils,
}
}
test('It should set the email input', () => {
const { input } = setup();
const element = input as HTMLInputElement;
fireEvent.change(element, { target: { value: 'abc#def' } });
console.log(element.value);
expect(element.value).toBe('abc#def');
})
The simple component (uses marterial-ui) looks like (I have removed large portions of it for brevity):
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
<TextField
id="email-notification"
name="email-notification"
label="Email"
value={email}
onInput={(e: React.FormEvent<EventTarget>) => {
let target = e.target as HTMLInputElement;
setEmail(target.value);
}}
placeholder="Email" />
<Button
type="submit"
variant="contained"
color="secondary"
>
Submit
</Button>
</form>
First the as cast is to keep TypeScript happy. Second is mainly around my use of fireEvent to simulate a user input. Any ideas on how I can test this functionality? Righ now the test aways fails as it is expecting abc#def and receiving ''.
Because you are using TextField component of MUI Component, so you can not get real input with getByLabelText. Instead of that, you should use getByPlaceholderText.
Try this:
const setup = () => {
const utils = render(<UnderConstruction />)
const input = utils.getByPlaceholderText('Email')
return {
input,
...utils,
}
}
test('It should set the email input', () => {
const { input } = setup();
const element = input as HTMLInputElement;
fireEvent.change(element, { target: { value: 'abc#def' } });
console.log(element.value);
expect(element.value).toBe('abc#def');
})
Using UI library like this, if getByLabelText or getByPlaceholderText don't work, I usually get the underlying input element this way:
// add a data-testid="myElement" on the TextField component
// in test:
fireEvent.change(getByTestId('myElement').querySelector('input'), { target: { value: 'abc#def' } });
This is not ideal though, getByLabelText or getByPlaceholderText should be used when possible.
I'm including react-select in my rails project, within a redux-form, it all works well, except that the backend I'm submitting the form to doesn't like the {value, label},
I'd like my form field to just include a list of the values.
here's my form code:
export const renderTagsSelect = props => {
const { input, initialValues } = props;
return (
<Creatable
{...props}
value={input.value || initialValues}
resetValue={initialValues}
onChange={value => input.onChange(value)}
onBlur={() => input.onBlur(input.value)}
/>
);
};
render() {
<form onSubmit={handleSubmit(doSaveItem)}>
<Title>Modifica Item</Title>
<Field
name="title"
label="Titolo"
component={renderInput}
validate={required()}
/>
<br />
<span>Tags</span>
<br />
<TagsFormSelector name="tag_list" tags={item.tag_list} />
<SubmitButton type="submit" disabled={pristine || submitting}>
Salva modifiche
</SubmitButton>
</form>
}
when submitting, the JSON generated by the form is like:
"tag_list": [
{
"label": "cena",
"value": "cena"
},
{
"label": "Paesaggi cittadini",
"value": "Paesaggi cittadini"
}
],
my backend is implemented in rails with acts_as_taggable_on, prefers to receive just something like this:
"tag_list: ["cena", "Paesaggi cittadini"]
for non-optimised this could be, I prefer having the backend drive the API.
Any clues how to achieve this?
You need to just pass a function reference to the onSubmit prop, you can create a new function that formats your data before calling handleSubmit:
<form onSubmit={this.onSubmit}>
And in your onSubmit function:
onSubmit = (tags) => {
const { handleSubmit } = this.props;
const submitValues = tags.map((tag) => tag.value);
handleSubmit(submitValues); // you can also pass doSaveItem here
}
Following kuiro5 suggestion I fixed it in the form with a normaliseData function:
in the form:
<form onSubmit={handleSubmit(doSaveItem)}>
[..]
<TagsFormSelector name="js_tag_list" tags={item.tag_list} />
[..]
in the doSaveItem:
const doSaveItem = (data, dispatch, props) => {
normaliseData(data);
return dispatch(saveItem(props.item.id, data)).catch(response => {
throw new SubmissionError(f.errorsFromProps(response));
});
};
the normaliseData:
const normaliseData = data => {
data["js_tag_list"] &&
(data["tag_list"] = data["js_tag_list"].map(tag => tag.value));
and other data normalisations.
this made it work.