In a simple scenario like so
function onSubmit() { e.preventDefault(); /* Some Submit Logic */ }
<form data-testid="form" onSubmit={(e) => onSubmit(e)}>
<button type="submit" data-testid="submit-button">Submit</button>
</form>
How do I make sure that the form gets submitted when the submit button is clicked?
const { queryByTestId } = render(<LoginForm/>);
const LoginForm = queryByTestId("form")
const SubmitButton = queryByTestId("submit-button")
fireEvent.click(SubmitButton)
???
How do I test if onSubmit() has been called or maybe form has been submitted?
Basically, here is what I "solved" it:
// LoginForm.js
export function LoginForm({ handleSubmit }) {
const [name, setName] = useState('');
function handleChange(e) {
setName(e.target.value)
}
return (
<form data-testid="form" onSubmit={() => handleSubmit({ name })}>
<input required data-testid="input" type="text" value={name} onChange={(e) => handleChange(e)}/>
<button type="submit" data-testid="submit-button">Submit</button>
</form>
)
}
export default function LoginPage() {
function handleSubmit(e) {
// submit stuff
}
return <LoginForm handleSubmit={(e) => handleSubmit(e)}/>
}
Now the test's file:
// LoginForm.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import LoginPage, { LoginForm } from "./LoginPage";
it("Form can be submited & input field is modifiable", () => {
const mockSubmit = jest.fn();
const { debug, queryByTestId } = render(<LoginForm handleSubmit={mockSubmit}/>);
fireEvent.change(queryByTestId("input"), { target: { value: 'Joe Doe' } }); // invoke handleChange
fireEvent.submit(queryByTestId("form"));
expect(mockSubmit).toHaveBeenCalled(); // Test if handleSubmit has been called
expect(mockSubmit.mock.calls).toEqual([[{name: 'Joe Doe'}]]); // Test if handleChange works
});
getByRole('form', { name: /formname/i })
RTL is meant to move away from using id's. An ideal solution is to name your form which does two things. It allow you to uniquely name it making it useful to screen readers and also gets the browser to assign it a role of form. Without the name, getByRole('form') will do nothing.
MDN: <form> element
Implicit ARIA role - form if the form has an accessible name,
otherwise no corresponding role
I'd suggest to use nock to intercept request sending from form and return mocked response after.
For example:
nock('https://foo.bar').post('/sign-up', formValues).reply(201);
But I would like to know a better solutions tho.
Related
I'm using React Hook Form v7 and I'm trying to make my data form persistent on page reload. I read the official RHF documentation which suggests to use little state machine and I tried to implement it but without success. Is there a better way to do it? However...
The first problem I encountered using it, is that my data is a complex object so the updateAction it should be not that easy.
The second problem is that I don't know when and how to trigger the updateAction to save the data. Should I trigger it on input blur? On input change?
Here's my test code:
If persisting in the localStorage works you, here is how I achieved it.
Define a custom hook to for persisting the data
export const usePersistForm = ({
value,
localStorageKey,
}) => {
useEffect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(value));
}, [value, localStorageKey]);
return;
};
Just use it in the form component
const FORM_DATA_KEY = "app_form_local_data";
export const AppForm = ({
initialValues,
handleFormSubmit,
}) => {
// useCallback may not be needed, you can use a function
// This was to improve performance since i was using modals
const getSavedData = useCallback(() => {
let data = localStorage.getItem(FORM_DATA_KEY);
if (data) {
// Parse it to a javaScript object
try {
data = JSON.parse(data);
} catch (err) {
console.log(err);
}
return data;
}
return initialValues;
}, [initialValues]);
const {
handleSubmit,
register,
getValues,
formState: { errors },
} = useForm({ defaultValues: getSavedData() });
const onSubmit: SubmitHandler = (data) => {
// Clear from localStorage on submit
// if this doesn’t work for you, you can use setTimeout
// Better still you can clear on successful submission
localStorage.removeItem(FORM_DATA_KEY);
handleFormSubmit(data);
};
// getValues periodically retrieves the form data
usePersistForm({ value: getValues(), localStorageKey: FORM_DATA_KEY });
return (
<form onSubmit={handleSubmit(onSubmit)}>
...
</form>
)
}
I already faced this issue and implemented it by creating a custom Hook called useLocalStorage. But since you are using the React hook form, it makes the code a bit complicated and not much clean!
I suggest you simply use the light package react-hook-form-persist.
The only work you need to do is to add useFormPersist hook after useForm hook. Done!
import { useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist";
const yourComponent = () => {
const {
register,
control,
watch,
setValue,
handleSubmit,
reset
} = useForm({
defaultValues: initialValues
});
useFormPersist("form-name", { watch, setValue });
return (
<TextField
title='title'
type="text"
label='label'
{...register("input-field-name")}
/>
...
);
}
The state itself won't persist any data on page reload.
You need to add your state data to Local Storage.
Then load it back into the state on componentDidMount (useEffect with empty dependency array).
const Form = () => {
const [formData, setFormData] = useState({})
useEffect(() => {
if(localStorage) {
const formDataFromLocalStorage = localStorage.getItem('formData');
if(formDataFromLocalStorage) {
const formDataCopy = JSON.parse(formDataFromLocalStorage)
setFormData({...formDataCopy})
}
}
}, []);
useEffect(() => {
localStorage && localStorage.setItem("formData", JSON.stringify(formData))
}, [formData]);
const handleInputsChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
return (
<div>
<input
type="text"
name="firstName"
placeholder='first name'
onChange={e => handleInputsChange(e)}
value={formData?.firstName}
/>
<input
type="text"
name="lastName"
placeholder='last name'
onChange={e => handleInputsChange(e)}
value={formData?.lastName}
/>
</div>
)
}
How do you implement set focus in an input using React-Hook-Form, this is what their FAQ's "How to share ref usage" code here https://www.react-hook-form.com/faqs/#Howtosharerefusage
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm();
const firstNameRef = useRef();
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input name="firstName" ref={(e) => {
register(e)
firstNameRef.current = e // you can still assign to ref
}} />
<input name="lastName" ref={(e) => {
// register's first argument is ref, and second is validation rules
register(e, { required: true })
}} />
<button>Submit</button>
</form>
);
}
I tried set focusing the ref inside useEffect but it doesn't work:
useEffect(()=>{
firstNameRef.current.focus();
},[])
Neither does inside the input:
<input name="firstName" ref={(e) => {
register(e)
firstNameRef.current = e;
e.focus();
}} />
You can set the focus using the setFocus helper returned by the useForm hook (no need to use a custom ref):
const allMethods = useForm();
const { setFocus } = allMethods;
...
setFocus('inputName');
https://react-hook-form.com/api/useform/setFocus
Are you using Typescript?
If so, replace...
const firstNameRef = useRef();
With...
const firstNameRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (firstNameRef.current) {
register(firstNameRef.current)
firstNameRef.current.focus()
}
}, []);
<input name="firstName" ref={firstNameRef} />
Got this from : https://github.com/react-hook-form/react-hook-form/issues/230
If you using Version 7, you can check this link in the docs
https://www.react-hook-form.com/faqs/#Howtosharerefusage
I can't comment (I don't have enough reputation), but without setting a timeout, setFocus didn't work. After adding a simple timeout from Subham's answer, it worked like a charm!
PS: Even adding a timeout of 0: setTimeout(() => setFocus(fieldName), 0) works. Can anyone explain?
I think you can simply use ref={(el) => el.focus()} to set the focus. The catch here is to make sure no other element within your page is also setting focus right after that el.focus() call.
const {
register,
handleSubmit,
setFocus, // here
formState: { errors },
} = useForm<FormFields>({
resolver: yupResolver(LoginSchema),
mode: "onTouched",
});
useEffect(() => {
setFocus("email");
}, [setFocus]);
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 try to implement the Search function into my management system using React-Hooks and GraphQL-Apollo Client. While the interface is shown successfully, when I press the 'Search' button it came out an error which named:
Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm pretty sure the useQuery is being called inside a function, so I do not understand what will cause this. The other function such as display all users and add new users are working fine.
I have tried a couple of ways to implement the search function and search around online while still couldn't get it solve. This is my first time encounters React-Hooks too.
Here is my current code in the searchUser component
import React from 'react'
import {useQuery} from 'react-apollo-hooks';
import {searchUserQ} from '../queries/queries';
const SearchUserForm = () => {
let name = '';
let content;
return (
<div id="edit-user">
<div className="field">
<label htmlFor="name">Search UserName</label>
<input type="text" id="name" onChange={ (event) => {
name = event.target.value }}/>
<button onClick={(event) => {
event.preventDefault();
const { data, loading, error } = useQuery(searchUserQ, {
variables: { name: name },
suspend: false
});
if (loading) {
content = <p>Loading User...</p>
}
if (error){
console.log(`Error Occur: ${ error }`);
content = <p>Error Occur!</p>
}
content = data.users.map(user => (
<p key={user.id}>Username: {user.name} ID: {user.id}</p>
));
}}>
Submit
</button>
<p>{ content }</p>
</div>
</div>
)
}
export default SearchUserForm;
Can anyone help with this?
One more question is that my data seems to return undefined everytime I execute the query. Is there something wrong with my query?
Here are the query:
const searchUserQ = gql`
query User($name: String!){
user(name: $name){
id
name
email
}
}
`;
Thanks and appreciate on the help!
According to the Rules of Hooks:
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
If you need to manually call a query manually ins response to an event, use the Apollo client directly. You can use useApolloClient to get an instance of the client inside your component:
const SearchUserForm = () => {
const client = useApolloClient();
...
return (
...
<button onClick={async (event) => {
try {
const { data } = client.query({
query: searchUserQ,
variables: { name: event.target.value },
});
// Do something with data, like set state
catch (e) {
// Handle errors
}
}} />
You can also still use useQuery, like this:
const SearchUserForm = () => {
const [name, setName] = useState('')
const { data, loading, error } = useQuery(searchUserQ, {
variables: { name },
});
...
return (
...
<button onClick={async (event) => {
setName(event.target.value)
...
}} />
You can use the useLazyQuery method and expose your data object to your entire component.
import {useLazyQuery} from '#apollo/react-hooks';
// - etc. -
const SearchUserForm = () => {
// note: keep the query hook on the top level in the component
const [runQuery, { data, loading, error }] = useLazyQuery(searchUserQ);
// you can now just use the data as you would regularly do:
if (data) {
console.log(data);
}
return (
<div id="edit-user">
<div className="field">
<label htmlFor="name">Search UserName</label>
<input
type="text"
id="name"
onChange={(event) => {name = event.target.value }} />
<button onClick={
(event) => {
event.preventDefault();
// query is executed here
runQuery({
variables: { name }, // note: name = property shorthand
suspend: false
})
}}>
// - etc. -
);
}
As opposed to doing the useQuery, the useLazyQuery method will only be executed on the click.
At the point where you are able to pass the 'name' value as a parameter.
If for example you would use the useQuery instead, and have a parameter that is required in your query (i.e. String!), the useQuery method will provide you with an error. Because on component render it will try to run that query directly without the required parameter because at that period of time it's not available yet.
I found problem to my second answer, should just wrap an if-statement before executing it, here is the complete code:
import React, {useState} from 'react'
import {useQuery} from 'react-apollo-hooks';
import {searchUserQ} from '../queries/queries';
const SearchUserForm = () => {
const [name, setName] = useState('');
const { data, error, loading } = useQuery(searchUserQ, {
variables: { name }
});
let content;
let sName;
if (data.user !== undefined && data.user !== null) {
if (loading) { content = 'Loading User...' }
if (error) { content = `Error Occur: ${error}` }
const user = data.user;
content = `Username: ${ user.name } ID: ${ user.id }`
}
return (
<div id="edit-user">
<div className="field">
<label htmlFor="name">Search UserName</label>
<input type="text" id="name" onChange={(event) => {
sName = event.target.value;
}}/>
<button onClick={(event) => {
setName(sName);
}} value={name}>
Search
</button>
</div>
<div>
<p>{ content }</p>
</div>
</div>
)
};
export default SearchUserForm;