I have a controlled component with a form that consists of two radio buttons and a text input. I have a function that is called for onChange event of the text input and have written a test that fires a change event. I expect that the spy function should be called once but the test always fails.
Test
test('Update function is called when text entered into searchbox', () => {
const spy = jest.fn();
const {getByTestId} = render(<SearchDropdown handleChange={spy}/>);
expect(getByTestId('searchText').value).toBe("");
fireEvent.change(getByTestId('searchText'), { target: { value: "23" } });
expect(getByTestId('searchText').value).toBe("23");
expect(spy).toHaveBeenCalledTimes(1);
});
Component - does not take in any props
import React, { Component } from 'react'
import { faSearch } from '#fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import SearchResults from './SearchResults';
export default class SearchDropdown extends Component {
constructor(props){
super(props)
this.state = {
type: 'Spec',
number: '',
}
}
handleChange = (event) => {
let name = event.target.name;
let target = event.target.value;
this.setState({
[name]: target}
);
}
handleSearch = (event) => {
event.preventDefault();
if (this.state.number !== "") {
console.log("Make request to API");
}
}
render() {
return (
<div data-testid="searchMenu">
<div className="list-group dropdown-menu dropdown-menu-right mr-5 text-center">
<h5><FontAwesomeIcon icon={faSearch}/> Quick Search</h5>
<form className="m-2">
<div className="form-group" onChange={this.handleChange}>
<input type="radio" value="Spec" name="type" defaultChecked /> Specification
<input type="radio" value="Claim" name="type" className="ml-2"/> Claim
</div>
<div className="form-group">
<label>Search Number</label>
<input type="text" data-testid="searchText" onChange={this.handleChange} value={this.state.number} name="number" className="form-control"/>
</div>
<SearchResults />
</form>
<div className="panel panel-default">
</div>
<button className="bg-transparent border-0 link">Advanced Search</button>
</div>
</div>
)
}
}
Error Message
expect(jest.fn()).toHaveBeenCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
fireEvent.change(getByTestId('searchText'), { target: { value: "23" } });
expect(getByTestId('searchText').value).toBe("23");
expect(spy).toHaveBeenCalledTimes(1);
^
});
test('Clicking search brings up results modal', () => {
at Object.toHaveBeenCalledTimes (src/Tests/SearchDropdown.test.js:18:17)
Am I missing something obvious?
I'm fairly new to React and React Testing Library, but there are a couple of things I noted:
In your test you're passing a spy as the value for prop handleChange to your component, which it doesn't have; just as you state: "does not take in any props".
Your component defines handleChange function which you use an event handler, it doens't get it from a prop.
Your handleChange function should be passed only as prop to input elements, not a div.
You should have one handler per each input instead of calling the same handler for all of them.
That said, in order for your test to provide any value, you should change your SearchDropdown component so that it receives the handlers from a parent or you test that firing click/change events on the inputs actually updates the view as expected.
I don't think there's any value in testing if an internal event handler function has been called.
You're using React Testing Library in your test, but not in the way it's intended to.
From React Testing Library Intro:
The utilities this library provides facilitate querying the DOM in the
same way the user would. Finding for elements by their label text
(just like a user would), finding links and buttons from their text
(like a user would). It also exposes a recommended way to find
elements by a data-testid as an "escape hatch" for elements where the
text content and label do not make sense or is not practical.
Related
I have a React app using hooks and trying to figure out how to keep a submit button disabled if the search field is empty.
Assuming a regular form field with submit button, how can I set a state hook that keeps the search button disabled until the user inputs text. I assume there should be an onChange function that probably updates the state on input change, but not exactly sure of the implementation.
const [disabled, isDisabled] = useState(true);
<input type="text" id="q" name="q" placeholder="Search.." name="search">
<button type="submit"><i class="fa fa-search"></i></button>
If you want to disable a button when an input string is empty, then the only state you need is the value of the input string.
const [inputVal, setInputVal] = useState('')
// ...
<input value={inputVal} onChange={e => setInputVal(e.target.value)} />
// ...
<button disabled={!inputVal}> /* ... */ </button>
Here we connect the input component to the state value. This is called a controlled component, because its value is controlled from by an external source (the state value) as opposed to an uncontrolled component, which means the input element holds it's own internal state (the default way inputs work if you don't set their value prop directly.
When the input component receives input (such as someone typing a character) the onChange prop is called. All we do then is take the new value of the input element (e.target.value) and use it to set the state.
If you can derive state from other state, then you shouldn't be storing it in state. Having a state variable called disabled only makes things more complex. The general idea is to use as little state as possible, and compute as much as you can from that state.
Please check this complete example where I used class component and use a disable property under state object. When you write something on textbox, the disable property will be set as false.
import React from "react";
export default class Login extends React.Component {
constructor(props) {
super(props);
this.state = {disable: true};
}
handleChange = (event) => {
this.setState({disable: event.target.value === ''})
};
render() {
return (
<div>
<div>
Name: <input onChange={this.handleChange}/> <br/>
<button disabled={this.state.disable} >Login</button>
</div>
</div>
);
}
}
here is the same example of functional component
import React, {useState} from "react";
export default function Login() {
const [disable, setDisable] = useState(true);
function handleChange(event) {
setDisable(event.target.value === '');
}
return (
<div>
<div>
Name: <input onChange={handleChange}/> <br/>
<button disabled={disable}>Login</button>
</div>
</div>
);
}
I am learning React and below one is sample code I am trying out. I am able to render this component and able to type in characters in input field without any handleChange() method ? Is this fine ? because what I know is, in order to make input fields available for typing, we need to add handleChange method something like below
handleChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
import React from "react";
class StudentForm extends React.Component {
constructor() {
super();
}
render() {
return (
<div>
<form>
<h1>Student Form</h1>
<input type="text" name="firstname"></input>
</form>
</div>
);
}
}
export default StudentForm;
handleChange is for setting the state value.
without onChange handler you can type in but your value is not getting stored anywhere.
For example, if you try to access your state this.state.firstname you will always get undefined.
You should have controlled component. Which is a simple and cleaner way access and store value in state.
To make your component controlled, you should have value and onChange props on input,
<input type="text" name="firstname" value={this.state.firstname} onChange={this.handleChange.bind(this)}></input>
Yes, consider the following
<input type="text" name="firstname" />
This is an uncrontrolled input which means React doesn't now about it's value nor how to change it. To make an input controlled you need to explicitly specify the value and onChange properties to bind this input to React's state
const Input = () =>{
const [value, setValue] = useState('')
return <input value={value} onChange={e => setValue(e.target.value)} />
}
Now the input is fully controlled by React, which provides the value it must print and a way to change it
After making below changes, I made this input element as controlled element and now I am not able to type in anything without using onChange handler.
import React from "react";
class StudentForm extends React.Component {
constructor() {
super();
this.state = {
firstname: ""
};
}
render() {
return (
<div>
<form>
<h1>Student Form</h1>
<input
type="text"
name="firstname"
value={this.state.firstname}
></input>
</form>
</div>
);
}
}
export default StudentForm;
I'm trying to get input tags' HTML inner values when submitting a form.
private handleSubmit = (event: any) => {
event.preventDefault();
console.log(event.currentTarget);
};
When the submit the form, it calls the function handleSubmit and it console logs the following.
Under the form tag, the first div has username value and the second div has password value. I would like to acess the two values. I think I should use DOM to do that, but can't be sure if I'm going for the right direction cuz I found some postings saying using DOM is not recommended.
Can anyone explain how I can acheive this?
Ideally you should update your state as the user enters information, and then access the data from the state. This would also allow you to run any validation on the data prior to it going into the state if you'd like.
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
username: null,
password: null
}
this.submitForm = this.submitForm.bind(this);
this.updateState = this.updateState.bind(this);
}
updateState (e) {
this.setState({[e.target.name]: e.target.value})
}
submitForm (e) {
e.preventDefault();
console.log(this.state);
}
render() {
return (
<div className="App">
<form onSubmit={this.submitForm}>
<input type="text" name="username" placeholder="username" onChange={this.updateState} /><br />
<input type="password" name="password" placeholder="password" onChange={this.updateState} /><br />
<button type="submit">Submit</button>
</form>
</div>
);
}
}
export default App;
The above code does the following:
Stores default values for username and password. While this isn't required, it makes the code more readable
binds this to functions that need to access state
Uses an updateState() function that is called onChange of the inputs
updateState uses the name attribute of the input as the key for the state
You could customize the updateState() function to do some validation, before saving to state if you'd like.
Excessive Rendering
ReactJS is pretty smart to no re-render the REAL DOM if your render() method doesn't actually rely on the state values that were updated; however, if you'd like to prevent ReactJS from even creating the Virtual DOM and comparing, you could utilize the shouldComponentUpdate() lifecycle hook.
In the particular example above, since render doesn't rely on ANYTHING in state, you could simply add the following method:
shouldComponentUpdate(prevState, nextState) {
return false;
}
That will prevent the render method from EVER re-rendering, which is most likely not going to work in a normal component, thus you could do something like this instead, only re-rendering on values you care about.
shouldComponentUpdate(prevState, nextState) {
if (nextState.email !== prevState.email) {
return true
}
return false;
}
Demo
https://repl.it/#AnonymousSB/SO53689072
If you want to use the DOM, once you have the form element (event.currentTarget in your case), you can use the .elements property to access a list of child inputs and buttons.
Alternatively, you can use React refs to keep track of the underlying HTML element when it's rendered.
render() {
return ... <input ref={(e) => this._name = e; } ....> ... ;
}
handleSubmit(e) {
var name = this._name ? this._name.value : '';
....
}
This can achieve what you want to
class Login extends Component{
state={
username:"",
password:""
}
onChange = (event)=>{
event.preventDefault()
this.setState({
[event.target.name]: event.target.value})
}
onSubmit = (event)=>{
event.preventDefault()
// submit whatever is in state from here
console.log(this.state)
}
render(){
return(<div>
<form onSubmit={handleSubmit}>
<input type="text" name="username" onChange={this.onChange} /><br />
<input type="password" name="password" onChange={this.onChange} /><br />
<button type="submit">Submit</button>
</form>
</div>)
}
}
Using the component Field of redux-form, I am trying to display the length of the string written in the input. But it seems like the handleChange function is never fired by the onChange.
Here is the component :
import React from 'react';
import {Field} from 'redux-form';
var max_chars = 160;
var LimitedCharsField = React.createClass({
getInitialState: function() {
return {
chars_left: max_chars
};
},
handleChange(event) {
var input = event.target.value;
this.setState({
chars_left: max_chars - input.length
});
},
render: function() {
return (
<div>
<Field name="short_description" id="short_description" component={this.props.renderSmallTextField} type="text"
label="Qualification du site" onChange={this.handleChange.bind(this)} maxLength={max_chars}/>
<br/>
<small id="short_description_countdown" >{this.state.chars_left} caractères restants</small>
</div>
);
}
});
export default LimitedCharsField;
And I am calling in a parent component which is the main form:
<LimitedCharsField renderSmallTextField={renderSmallTextField}/>
The component does appear in the form but the handleChange dosen't fire at all.
What should be done in order to get the function handleChange fired?
Many thanks.
Edit: Below the renderSmallTextField
const renderSmallTextField = ({input, label, meta:{touched, error}, ...custom}) => (
<TextField
hintText={label}
errorText={touched && error}
{...input}
{...custom}
/>
);
I would imagine this is because the onChange prop isn't passed around correctly. Couldn't say for sure without seeing the code for the renderSmallTextField component though. Try setting component="input" as a simple test. If the handleChange method starts firing you have found your problem.
I'm trying to implement a very simple use case, a UI feature, where:
There is a label with some content in it
If clicked, a text input replaces it with the content of label available
User can edit the content
When enter is pressed, the input hides and label is back with updated content
I could get finally all correct (in fact with a MongoBD backend, redux, etc.), and the only thing I couldn't ever do (paying a complete day in googling and reading S.O.F similar posts) was this:
When my text input appears, I can't transfer focus to it! First I tired this way:
<div className={((this.state.toggleWordEdit) ? '' : 'hidden')}>
<input id={this.props.word._id} className="form-control"
ref="updateTheWord"
defaultValue={this.state.word}
onChange={this.handleChange}
onKeyPress={this.handleSubmit}
autoFocus={this.state.toggleWordEdit}/></div>
<div className={((this.state.toggleWordEdit) ? 'hidden' : '')}>
<h3 onClick={this.updateWord}>
{this.state.word}</h3>
</div>
but autoFocus sure didn't work (I "guess" because the form is rendered, but in hidden state, making autoFocus useless).
Next I tried in my this.updateWor, many of suggestions I found on google and S.O.F.:
this.refs.updateTheWord.focus();
which together with similar suggestions all didn't work. Also I tried to fool React just to see if at all I can do something! I used real DOM:
const x = document.getElementById(this.props.word._id);
x.focus();
and it didn't work either. One thing I even could not understand to put into word is a suggestion like this:
having ref as a method (I "guess")
I didn't even try it because I have multiples of these components and I need ref to further get value of, per component, and I couldn't imagine if my ref is not named, how I could get the value of!
So could you please give an idea, helping me to understand that in case I'm not using a Form (because I need a single input box replacing a label) how I could set its focus when it's CSS (Bootstrap) class is losing 'hidden' please?
The way you have used refs is not the most preferred way or else its not the best practice anymore . try some thing like this
class MyClass extends React.Component {
constructor(props) {
super(props);
this.focus = this.focus.bind(this);
}
focus() {
this.textInput.current.focus();
}
render() {
return (
<div>
<input
type="text"
ref={(input) => { this.textInput = input; }} />
<input
type="button"
value="Set Focus"
onClick={this.focus}
/>
</div>
);
}
}
Update
From React 16.3 upwards you can use the React.createRef() API
class MyClass extends React.Component {
constructor(props) {
super(props);
// create a ref to store the textInput DOM element
this.textInput = React.createRef();
this.focus = this.focus.bind(this);
}
focus() {
// Explicitly focus the text input using the raw DOM API
// Note: we're accessing "current" to get the DOM node
this.textInput.current.focus();
}
render() {
// tell React that we want to associate the <input> ref
// with the `textInput` that we created in the constructor
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Set Focus"
onClick={this.focus}
/>
</div>
);
}
}
From React 18.xx upwards you can use the useRef Hook
import React, { useRef } from "react";
export const Form = () => {
const inputRef = useRef(null);
const focus = () => {
inputRef.current.focus();
};
return (
<div>
<input type="text" ref={inputRef} />
<input type="button" value="Set Focus" onClick={focus} />
</div>
);
};
Just add autofocus attribute to the input. (of course in JSX it is autoFocus)
<input autoFocus ...
useFocus hook
// General Focus Hook
const useFocus = (initialFocus = false, id = "") => {
const [focus, setFocus] = useState(initialFocus)
const setFocusWithTrueDefault = (param) => setFocus(isBoolean(param)? param : true)
return ([
setFocusWithTrueDefault, {
autoFocus: focus,
key: `${id}${focus}`,
onFocus: () => setFocus(true),
onBlur: () => setFocus(false),
},
])
}
const FocusDemo = () => {
const [labelStr, setLabelStr] = useState("Your initial Value")
const [setFocus, focusProps] = useFocus(true)
return (
<> {/* React.Fragment */}
<input
onChange={(e)=> setLabelStr(e.target.value)}
value={labelStr}
{...focusProps}
/>
<h3 onClick={setFocus}>{labelStr}</h3>
</>
)
}
For a more complete demo click here.
In addition to the previous answers, I've added setTimeout to make it work
handleClick() {
if (this.searchInput) {
setTimeout(() => {
this.searchInput.focus();
}, 100);
}
}
where searchInput is the jsx ref of the input
<input
type="text"
name="searchText"
ref={(input) => { this.searchInput = input; }}
placeholder="Search" />
and the handleClick() is an onClick handler to any element
#BenCarp's answer in typescript
Pass the inputRef to an input and just call setFocus to set the focus to it.
export const useInputFocus = (): [MutableRefObject<HTMLInputElement | undefined>, () => void] => {
const inputRef = useRef<HTMLInputElement>();
const setFocus = (): void => {
const currentEl = inputRef.current;
if (currentEl) {
currentEl.focus();
}
};
return [inputRef, setFocus];
};
Use componentDidUpdate method to every time update the component
componentDidUpdate(prevProps, prevState) {
this.input.focus();
}
You can use "useRef" hook and make a reference to your input control, then use your reference.current.focus()