I am trying to write a test to check a value after a change in a <select> dropdown. This is my component:
import React from 'react';
const BasicDropdown = ({ key, value, options }) => {
return (
<div className={`basic-dropdown-${key}`}>
<select
className={`basic-dropdown-${key}-select`}
name={`basic-dropdown-${key}-select`}
{...{value}}
>
{options.map(option =>
<option
className={`basic-dropdown-${key}-select-${option}-option`}
key={option}
value={option}
>
{option.charAt(0).toUpperCase() + option.slice(1)}
</option>
)}
</select>
</div>
);
};
export default BasicDropdown;
So far it's very simple. The reason for having a component is that I will expand this later depending on the props and other things. So I decided to write a test for this component to start with:
import React from 'react'
import TestUtils from 'react-dom/test-utils';
import BasicDropdown from './BasicDropdown';
const options = ['option-A', 'option-B', 'option-C'];
describe("BasicDropdown", () => {
let renderedCmpt;
beforeAll(() => {
renderedCmpt = TestUtils.renderIntoDocument(
<BasicDropdown key="test" value="option-A" options={options} />
)
});
it("Should have correct value after change", () => {
const select = TestUtils.findRenderedDOMComponentWithClass(renderedCmpt, 'basic-dropdown-test-select');
expect(select.value).toEqual("option-A");
TestUtils.Simulate.change(select, {target: {value: 'option-C'}});
const selectChanged = TestUtils.findRenderedDOMComponentWithClass(renderedCmpt, 'basic-dropdown-test-select');
expect(selectChanged.value).toEqual("option-C");
});
});
My problem is that when running this test using jest (jest --coverage=false --config ~/projects/Beehive/config/jest.config.js ~/projects/Beehive/tests/BasicDropdown-test.js) I get the following error:
Expected: "option-C"
Received: "option-A"
at line 21, which means that the value of the select is never changed during the Simulate.
What am I doing wrong?
You need to add onChange on select element to reflect the change on the test, else it will always be option-A.
Related
Is there a way to tell when the dropdown is open and also closed? onfocus and onblur doesn't seem to be working.
<div className="select-container">
<select>
{options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
</div
You should use useState to keep track of the dropdown status. It would look something like this:
import "./styles.css";
import { useState } from "react";
export default function App() {
const [isDropDownOpen, setDropDownOpen] = useState(false);
let options = [
{
label: "money"
}
];
const handleSelect = () => {
setDropDownOpen(!isDropDownOpen);
};
const handleBlur = () => {
setDropDownOpen(!isDropDownOpen);
};
console.log(isDropDownOpen);
return (
<div>
<select onBlur={handleBlur} onClick={handleSelect}>
{options.map((option) => (
<option value={option.value}>{option.label}</option>
))}
</select>
</div>
);
}
I have tied it into the handleSelect function, which will probably do more than just keep track of whether or not the dropdown is open but either way, it works as a reference point for now.
EDIT: Also, in case you click outside the dropdown, I used onBlur, which is controlled by handleBlur to change the boolean value because obviously, the dropdown will close.
Check the console.log on the this code sandbox to see how it works: https://codesandbox.io/s/amazing-easley-0mf7o3?file=/src/App.js
I'm using react instant search to show list of users in select options. I managed to show all the users in select option but I want to only show users with admin role (role === 'admin').
Ofcourse I can do this in client side by filtering the hits on role but is there
any way to achieve this with react instant search?
This is how I showed all user.
AlgoliaProvider.js
import React from "react";
import algoliasearch from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch-dom";
const searchClient = algoliasearch(
process.env.REACT_APP_ALGOLIA_APP_ID,
process.env.REACT_APP_ALGOLIA_SEARCH_KEY
);
const AlgoliaProvider = ({ indexName, children, ...rest }) => {
return (
<InstantSearch indexName={indexName} searchClient={searchClient} {...rest}>
{children}
</InstantSearch>
);
};
export default AlgoliaProvider;
AutoCompleteSelect.js
import React from 'react';
import { Select } from 'antd';
import { connectAutoComplete } from 'react-instantsearch-dom';
const { Option } = Select;
const AutoCompleteSelect = ({
hits,
refine,
...rest
}) => {
const handleSuggestion = value => {
refine(value);
};
return (
<Select showSearch onSearch={handleSuggestion} {...rest}>
{hits.map(user => (
<Option
key={user.uid}
value={user.uid}
>
<span>{user.name}</span>
</Option>
))}
</Select>
);
};
export default connectAutoComplete(AutoCompleteSelect);
App.js
<AlgoliaProvider indexName="User">
<AutoCompleteSelect placeholder="user search" />
</AlgoliaProvider>
You need the faceting feature to achieve this.
Read more about it here: https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/
I have configured one such facet via the UI (algolia frontend) and then I use it like this in instantSearch:
<InstantSearch searchClient={.... >
<Configure
hitsPerPage={<Number>}
filters={`role:${user.role}`} />
...
...
</InstantSearch>
See the filters part in the code, 'role' needs to be added as a facet in the configuration (via ui or code).
using Searchstate
<InstantSearch
....
searchState={{
role: 'admin',
}}
}}
More on: https://www.algolia.com/doc/api-reference/widgets/ui-state/react/
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [theRoom, setRoom] = useState(null);
const updateVideoDevice = (e) => {
console.log("room", theRoom);
};
const createRoom = () => {
console.log("we change the room", theRoom);
setRoom({
localparticipants: {}
});
console.log("we change the room after", theRoom);
};
useEffect(() => {
const select = document.getElementById("video-devices");
select.addEventListener("change", updateVideoDevice);
}, []);
return (
<div className="App">
<select id="video-devices">
<option value="1">1</option>
<option value="2">2</option>
</select>
<button onClick={createRoom}>change obj</button>
</div>
);
}
I have this codebase. When I press the change obj button for the first time it doesn't set theRoom to the object
{
localparticipants: {}
}
But when I press the button for the second time it does, and after that, I try to change the select element's options I got null for the console log in the updateVideoDevice function.
How do I solve these two issues with React?
const [theRoom, setRoom] = useState(null);
setRoom is asynchronous function, so you will not see the change immediately.
Also, you don't need to add event listener to select tag. You can simply use onChange method. So remove all with useEffect and then change the code as below.
<select id="video-devices" onChange={updateVideoDevice}>
you need to chnage how you call the onclick method so it's look like this:
<button onClick={()=>createRoom()}>change obj</button>
Here's a working solution:
setRoom is asynchronous so your 2 console.log(theRoom) can't show the right value while you're still in the function.
to handle the select change value, you can use the onChange React prop that allows you to catch every value change of a hook
I put you 2 console.login a useEffect to allow you to see the changes of the theRoom and selectValue values
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [theRoom, setRoom] = useState(null);
const [selectValue, setSelectValue] = useState(null);
const createRoom = () => {
setRoom({
localparticipants: {}
});
};
// everytime selectValue or theRoom values changed, this trigger this
useEffect(() => {
console.log(selectValue);
console.log(theRoom);
}, [selectValue, theRoom]);
return (
<div className="App">
<select
id="video-devices"
onChange={(e) => setSelectValue(e.target.value)}
value={selectValue}
>
<option value="1">1</option>
<option value="2">2</option>
</select>
<button onClick={createRoom}>change obj</button>
</div>
);
}
So here's the user function I'm trying to create:
1.) User double clicks on text
2.) Text turns into input field where user can edit text
3.) User hits enter, and upon submission, text is updated to be edited text.
Basically, it's just an edit function where the user can change certain blocks of text.
So here's my problem - I can turn the text into an input field upon a double click, but how do I get the edited text submitted and rendered?
My parent component, App.js, stores the function to update the App state (updateHandler). The updated information needs to be passed from the Tasks.jsx component, which is where the text input is being handled. I should also point out that some props are being sent to Tasks via TaskList. Code as follows:
App.js
import React, {useState} from 'react';
import Header from './Header'
import Card from './Card'
import cardData from './cardData'
import Dates from './Dates'
import Tasks from './Tasks'
import Footer from './Footer'
import TaskList from './TaskList'
const jobItems= [
{
id:8,
chore: 'wash dishes'
},
{
id:9,
chore: 'do laundry'
},
{
id:10,
chore: 'clean bathroom'
}
]
function App() {
const [listOfTasks, setTasks] = useState(jobItems)
const updateHandler = (task) => {
setTasks(listOfTasks.map(item => {
if(item.id === task.id) {
return {
...item,
chore: task.chore
}
} else {
return task
}
}))
}
const cardComponents = cardData.map(card => {
return <Card key = {card.id} name = {card.name}/>
})
return (
<div>
<Header/>
<Dates/>
<div className = 'card-container'>
{cardComponents}
</div>
<TaskList jobItems = {listOfTasks} setTasks = {setTasks} updateHandler = {updateHandler}/>
<div>
<Footer/>
</div>
</div>
)
}
export default App;
Tasks.jsx
import React, {useState} from 'react'
function Tasks (props) {
const [isEditing, setIsEditing] = useState(false)
return(
<div className = 'tasks-container'>
{
isEditing ?
<form>
<input type = 'text' defaultValue = {props.item.chore}/>
</form>
: <h1 onDoubleClick ={()=> setIsEditing(true)}>{props.item.chore}</h1>
}
</div>
)
}
export default Tasks
TaskList.jsx
import React from 'react'
import Tasks from './Tasks'
function TaskList (props) {
const settingTasks = props.setTasks //might need 'this'
return (
<div>
{
props.jobItems.map(item => {
return <Tasks key = {item.id} item = {item} setTasks = {settingTasks} jobItems ={props.jobItems} updateHandler = {props.updateHandler}/>
})
}
</div>
)
}
export default TaskList
You forgot onChange handler on input element to set item's chore value.
Tasks.jsx must be like below
import React, {useState} from 'react'
function Tasks (props) {
const [isEditing, setIsEditing] = useState(false)
const handleInputChange = (e)=>{
// console.log( e.target.value );
// your awesome stuffs goes here
}
return(
<div className = 'tasks-container'>
{
isEditing ?
<form>
<input type = 'text' onChange={handleInputChange} defaultValue = {props.item.chore}/>
</form>
: <h1 onDoubleClick ={()=> setIsEditing(true)}>{props.item.chore}</h1>
}
</div>
)
}
export default Tasks
So, first of all, I would encourage you not to switch between input fields and divs but rather to use a contenteditable div. Then you just use the onInput attribute to call a setState function, like this:
function Tasks ({item}) {
return(
<div className = 'tasks-container'>
<div contenteditable="true" onInput={e => editTask(item.id, e.currentTarget.textContent)} >
{item.chore}
</div>
</div>
)
}
Then, in the parent component, you can define editTask to be a function that find an item by its id and replaces it with the new content (in a copy of the original tasks array, not the original array itself.
Additionally, you should avoid renaming the variable between components. (listOfTasks -> jobItems). This adds needless overhead, and you'll inevitably get confused at some point which variable is connected to which. Instead say, <MyComponent jobItems={jobItems} > or if you want to allow for greater abstraction <MyComponent items={jobItems} > and then you can reuse the component for listable items other than jobs.
See sandbox for working example:
https://codesandbox.io/s/practical-lewin-sxoys?file=/src/App.js
Your Task component needs a keyPress handler to set isEditing to false when enter is pressed:
const handleKeyPress = (e) => {
if (e.key === "Enter") {
setIsEditing(false);
}
};
Your updateHandler should also be passed to the input's onChange attribute, and instead of defaultValue, use value. It also needs to be reconfigured to take in the onChange event, and you can map tasks with an index to find them in state:
const updateHandler = (e, index) => {
const value = e.target.value;
setTasks(state => [
...state.slice(0, index),
{ ...state[index], chore: value },
...state.slice(index + 1)
]);
};
Finally, TaskList seems like an unnecessary middleman since all the functionality is between App and Task; you can just render the tasks directly into a div with a className of your choosing.
react-edit-text is a package I created which does exactly what you described.
It provides a lightweight editable text component in React.
A live demo is also available.
I'm working on React JS project. I have 4 dropdown button (select option). And all the dropdown will be coming from DB dynamically. So wanted to know what is the right method to implement.
Initially I had only 1 dropdown box, so implemented it with ajax call and append the values with <option> tag under <select> tag. Now I have 3 more dropdown, so do I need to call multiple ajax calls for all 4 box ? or is there any other ways to implement it ?
Please do suggest here. Because I don't want to implement in wrong way and revert back again.
If you create a small component for your dropdowns, like so:
import React, { Component } from 'react';
class SelectOption extends Component {
render() {
return (
<option value={this.props.dataItem.key}>{this.props.dataItem.val}</option>
)
}
}
class SimpleDropdown extends Component {
render() {
let options = [];
if (this.props.selectableData) {
const selectableData = this.props.selectableData;
options = selectableData.map((dataItem) =>
<SelectOption key={'option_' + dataItem.key} dataItem={dataItem} />
);
}
return (
<div>
<select onChange={this.props.handleInputChange} name={this.props.name} >
{options}
</select>
</div>
)
}
}
export default SimpleDropdown;
You can use it in your parent component, something like this...
import React, { Component } from 'react';
import SimpleDropdown from './common/SimpleDropdown';
class Parentextends Component {
componentDidMount() {
// here you handle your ajax call or calls, depending on what you choose to go with
}
handleInputChange = (e) => {
const target = e.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
render() {
const ajaxResultFirst = ajaxResult.First;
const ajaxResultSecond = ajaxResult.Second;
const ajaxResultThird = ajaxResult.Third;
const ajaxResultFourth = ajaxResult.Fourth;
return (
<section className="form">
<SimpleDropdown
name="FirstDropdown"
selectableData={ajaxResultFirst}
handleInputChange={this.handleInputChange}
/>
<SimpleDropdown
name="SecondDropdown"
selectableData={ajaxResultSecond}
handleInputChange={this.handleInputChange}
/>
<SimpleDropdown
name="ThirdDropdown"
selectableData={ajaxResultThird}
handleInputChange={this.handleInputChange}
/>
<SimpleDropdown
name="FourthDropdown"
selectableData={ajaxResultFourth}
handleInputChange={this.handleInputChange}
/>
</section>
);
}
};
export default Parent;
Something like this should work. But I still recommend using a different plugin than jquery for making ajax requests, my first choice is axios https://github.com/mzabriskie/axios.