I need a search box in React that opens a dropdown as soon as an entry is made. The dropdown should contain buttons that can trigger an event.
The problem is that the dropdown has to disappear as soon as another input box is used in the application.
I could also implement this, but now I have the problem that the event of the button in question is not triggered because the focus of the input field is lost beforehand as soon as I press the button. As a result, the dropdown disappears and the event is never triggered.
This is roughly my Searchbox Component:
import React, { useState } from 'react';
import Dropdown from './Dropdown';
function Search(props) {
const [focused, setFocused] = useState(false);
const inputHandler = (params) => {
if (params.length > 0)
props.apiCall(params);
}
const buttonHandler = (id) => {
console.log(id);
}
return (
<>
<input
type="text"
placeholder="Suchen.."
onChange={(event) => inputHandler(event.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)} // Problem area
/>
{
focused === true && props.apiData.length > 0 ?
props.apiData.map((mappedData, key) => {
return (
<Dropdown
key={key}
id={mappedData.id}
name={mappedData.name}
/*
even more Data
*/
buttonHandler={buttonHandler}
/>
)
})
: null
}
</>
)
}
export default Search;
This is my Dropdown Component:
import React from 'react';
function Dropdown(props) {
return (
<ul key={props.id}>
<li>{props.name}</li>
<li>even more data</li>
<li>
<input
type="button"
value="Select"
onClick={() => {
props.buttonHandler(props.id)
}}
/>
</li>
</ul>
)
}
export default Dropdown;
To resolve this issue blur event in Search Component can be handled with delay.
function Search(props) {
const [focused, setFocused] = useState(false);
const inputHandler = (params) => {
if (params.length > 0) props.apiCall(params);
};
const buttonHandler = (id) => {
console.log(id);
};
return (
<>
<input
type="text"
placeholder="Suchen.."
onChange={(event) => inputHandler(event.target.value)}
onFocus={() => setFocused(true)}
- onBlur={() => setFocused(false)} // Problem area --> remove this line
+ onBlur={() => setTimeout(()=> setFocused(false),500)} // ---> delay added
/>
{focused === true && props.apiData.length > 0
? props.apiData.map((mappedData, key) => {
return (
<Dropdown
key={key}
id={mappedData.id}
name={mappedData.name}
/*
even more Data
*/
buttonHandler={buttonHandler}
/>
);
})
: null}
</>
);
}
Related
I'm using react-colorful to get colors HEX code.
I would want to show this component when the color's box is clicked, and hide it when clicked outside of it, kinda like the color picker Chrome is using (ex <input type="color" /> )
How to do so ?
https://codesandbox.io/s/react-colorful-demo-forked-wwxq2?file=/src/App.js:308-322
import React, { useState } from "react";
import { HexColorPicker } from "react-colorful";
import "./styles.css";
export default function App() {
const [color, setColor] = useState("#b32aa9");
const handleChange = (e) => {
setColor(e.target.value);
};
return (
<div className="App">
<HexColorPicker color={color} onChange={setColor} />
//Click on this box to show the picker <div className="value" style={{ borderLeftColor: color }}>
Current color is {color}
</div>
<input value={color} onChange={handleChange} />
<div className="buttons">
<button onClick={() => setColor("#c6ad23")}>Choose gold</button>
<button onClick={() => setColor("#556b2f")}>Choose green</button>
<button onClick={() => setColor("#207bd7")}>Choose blue</button>
</div>
</div>
);
}
At first, you should have to declare a state which tracks whether the color box is open or not like
const [open, setopen] = useState(false);
Now add an event handler on the div of the box which toggles the state from false to true and true to false.
const openBox = () => {
setopen(!open);
}
Now in your return statement, add a conditional to open or close colorBox. if clicked on that div which holds the onClick method.
<div className="App">
{open &&<HexColorPicker color={color} onChange={setColor} />
}
<div onClick={() => openBox()}className="value" style={{ borderLeftColor: color }}>
Current color is {color}
</div>
Now if you click on the div which contains the onClick method, it will open the colorBox and on pressing again it will close it.
You can use the following example (from https://codesandbox.io/s/opmco found in Code Recipes)
import React, { useCallback, useRef, useState } from "react";
import { HexColorPicker } from "react-colorful";
import useClickOutside from "./useClickOutside";
export const PopoverPicker = ({ color, onChange }) => {
const popover = useRef();
const [isOpen, toggle] = useState(false);
const close = useCallback(() => toggle(false), []);
useClickOutside(popover, close);
return (
<div className="picker">
<div
className="swatch"
style={{ backgroundColor: color }}
onClick={() => toggle(true)}
/>
{isOpen && (
<div className="popover" ref={popover}>
<HexColorPicker color={color} onChange={onChange} />
</div>
)}
</div>
);
};
useClickOutside.js
import { useEffect } from "react";
// Improved version of https://usehooks.com/useOnClickOutside/
const useClickOutside = (ref, handler) => {
useEffect(() => {
let startedInside = false;
let startedWhenMounted = false;
const listener = (event) => {
// Do nothing if `mousedown` or `touchstart` started inside ref element
if (startedInside || !startedWhenMounted) return;
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) return;
handler(event);
};
const validateEventStart = (event) => {
startedWhenMounted = ref.current;
startedInside = ref.current && ref.current.contains(event.target);
};
document.addEventListener("mousedown", validateEventStart);
document.addEventListener("touchstart", validateEventStart);
document.addEventListener("click", listener);
return () => {
document.removeEventListener("mousedown", validateEventStart);
document.removeEventListener("touchstart", validateEventStart);
document.removeEventListener("click", listener);
};
}, [ref, handler]);
};
export default useClickOutside;
When I click on a specific button I want to capture the {country} prop associated with it.
I tired the following
import React, { useState, useEffect } from 'react'
import axios from 'axios'
// ====================================================================[SEARCH-BAR]=======================================================
// search component
const SearchBar = (props) => {
// console.log(props);
const { searchString, searchOnChangeEventHandler } = props
return (
<>
<form>
<label>Search </label>
<input type='text' placeholder='type to search...' value={searchString} onChange={searchOnChangeEventHandler} />
</form>
</>
)
}
// ================================================================[COUNTRY_CARD]==========================================================
// countryCard component
const CountryCard = (props) => {
console.log(props);
return (
<div>
<p>countryName</p>
<p>capital</p>
<p>population</p>
<p>languages</p>
<ul>
<li>item</li>
<li>item</li>
</ul>
<p>image flag</p>
</div>
)
}
// ===================================================================[DISPLAY]===========================================================
// display component
const Display = (props) => {
const [showCountryCard, setShowCountryCard] = useState(false)
const [thisCountry, setThisCountry] = useState({})
// console.log(props);
const { countries, searchString } = props
// console.log(countries);
// eslint-disable-next-line eqeqeq
// searchString empty
if (searchString == false) {
return (
<>
<div>
<span>Type in SearchBar for a country...</span>
</div>
</>
)
}
// to count number of matches
const filteredResultsCount = countries.filter(country => country.name.toLowerCase().includes(searchString.toLowerCase())).length
// function to filterCountries
const filteredResults = (searchString, countries) => countries.filter(country => {
return country.name.toLowerCase().includes(searchString.toLowerCase())
})
// RENDER CONDITIONS
// searchString return <= 10 matches && >1 match
// event handler for show-btn
const showCardEventHandler = (event) => {
console.log(event.target.parentElement);
setShowCountryCard(!showCountryCard)
}
if (filteredResultsCount <= 10 && filteredResultsCount > 1) {
return (
<>
<ul>
{
filteredResults(searchString, countries).map(country =>
<li
key={country.numericCode}
country={country}
>
<span>{country.name}</span>
<button
value={showCountryCard}
onClick={showCardEventHandler}
>show</button>
</li>
)
}
</ul>
{
showCountryCard ? <p>show country card</p> : null
}
</>
)
}
// searchString returns >10 matches
if (filteredResultsCount > 10) {
return (
<span>{filteredResultsCount} matches!, please refine your search...</span>
)
}
// searchString returns ===1 match
if (filteredResultsCount === 1) {
return (
<>
{
filteredResults(searchString, countries).map(country => <CountryCard key={country.numericCode} country={country} />)
}
</>
)
}
// invalid searchString
if (filteredResultsCount === 0) {
return (
<span><strong>{filteredResultsCount} matches!</strong> please refine your search...</span>
)
}
}
// ===================================================================[APP]==============================================================
// app component
const App = () => {
// to store countries
const [countries, setCountries] = useState([])
// to fetch data from
const url = 'https://restcountries.eu/rest/v2/all'
useEffect(() => {
// console.log('effect');
axios
.get(url)
.then(response => {
// console.log('promise fulfilled');
const countries = response.data
// array of objects
setCountries(countries)
})
}, [])
// console.log('countries', countries.length);
// console.log(countries);
// to store search string
const [searchString, setSearchString] = useState('')
// event handler search input
const searchOnChangeEventHandler = (event) => setSearchString(event.target.value)
return (
<>
<h1>Countries Data</h1>
<SearchBar searchString={searchString} searchOnChangeEventHandler={searchOnChangeEventHandler} />
<br />
<Display countries={countries} searchString={searchString} />
</>
)
}
export default App
Please take a look at <Display/> component and in particular I'm trying to work on this part
const showCardEventHandler = (event) => {
console.log(event.target.parentElement);
setShowCountryCard(!showCountryCard)
}
if (filteredResultsCount <= 10 && filteredResultsCount > 1) {
return (
<>
<ul>
{
filteredResults(searchString, countries).map(country =>
<li
key={country.numericCode}
country={country}
>
<span>{country.name}</span>
<button
value={showCountryCard}
onClick={showCardEventHandler}
>show</button>
</li>
)
}
</ul>
{
showCountryCard ? <p>show country card</p> : null
}
</>
)
}
I want to be able to render a list of countries if they are more than 10 and allow a user to click on a specific country, which then will be used to render the <CountryCard/> component.
If there is only 1 matching value from search then I will directly display the country card component. The second functionality works.
After the following refactor the first functionality works, but Ima little confused as to why so I'm adding on to the post. This is the component being rendered and now I'm passing country prop onClick, like so
if (filteredResultsCount <= 10 && filteredResultsCount > 1) {
return (
<>
<ul>
{filteredResults(searchString, countries).map((country) => (
<li key={country.numericCode} country={country}>
<span>{country.name}</span>
<button
value={showCountryCard}
onClick={() => toggleCardEventHandler(country)}>
{showCountryCard ? 'hide' : 'show'}
</button>
</li>
))}
</ul>
{showCountryCard ? <CountryCard country={country} /> : null}
</>
);
}
The event handler is as follows
const toggleCardEventHandler = (country) => {
// console.log(country);
setShowCountryCard(!showCountryCard);
setCountry(country)
};
This works properly.
My question is, when I change the eventHandler onClick={toggleCardEventHandler(country)} it breaks, but shouldnt it be accessible through closure?
Also, if I change the code to this
onClick={() => {
toggleCardEventHandler()
setCountry(country)
}}
The code works the way I want but which is a better way to pass the value to the toggleCardEventHandler() and set the country there or to do it like this?
As I understand it you want to pass the country.name to your showCardEventHandler.
Update showCardEventHandler so it takes the event and the country name:
const showCardEventHandler = (event, countryName) => {
console.log(countryName);
setShowCountryCard(!showCountryCard)
}
Now pass the countryname to the function:
<li
key={country.numericCode}
country={country}
>
<span>{country.name}</span>
<button
value={showCountryCard}
onClick={e => showCardEventHandler(e, country.name)}
>show</button>
</li>
Since you are not using the event in showCardEventHandler you can remove it from the signature
const showCardEventHandler = (countryName) => {}
and call it with onClick={() => showCardEventHandler(country.name)}
I'm building a React tab navigation component with emotion. I'm having trouble finding a solution that would allow me to:
Initially hide all content except for the buttons and not style the buttons.
When you click on a button activate the style and show the content associated with that button.
And finally when you click outside or the input is empty reset to initial state.
Here is the code:
Code
import React, { useState } from "react";
import ReactDOM from "react-dom";
import styled from "#emotion/styled";
import "./styles.css";
const StyledShowButton = styled("button", {
shouldForwardProp: (prop) => ["active"].indexOf(prop) === -1
})`
color: ${({ active }) => (active ? "red" : "black")};
`;
function App() {
const [active, setActive] = useState(0);
const [showInput, setShowInput] = useState(false);
const handleInputChange = (e) => {
if (e.target.value < 1) {
console.log("Reset Everyting");
}
};
const handleTabClick = (e) => {
const index = parseInt(e.target.id, 0);
if (index !== active) {
setActive(index);
}
if (!showInput) {
setShowInput(!showInput);
}
};
return (
<div className="App">
<StyledShowButton
type="button"
id={0}
active={active === 0}
onClick={handleTabClick}
>
First
</StyledShowButton>
<StyledShowButton
type="button"
id={1}
active={active === 1}
onClick={handleTabClick}
>
Second
</StyledShowButton>
{/* CONTENT */}
{active === 0 ? (
<input placeholder="First input" onChange={handleInputChange} />
) : (
<input placeholder="Second input" onChange={handleInputChange} />
)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Just ask if I didn't make my self clear enough,
Thanks beforehand!
Erik
You can hide inpts in this way at first by assigning a null value to the active state.
You can also initialize values from 1 so that id and state state are not confused.
I made the arrangements.
You can review the code below.
You can also view it from this link. Code:
function App() {
const [active, setActive] = useState(null);
const [showInput, setShowInput] = useState(false);
const handleInputChange = (e) => {
if (e.target.value < 1) {
setActive(null);
}
};
const handleTabClick = (e) => {
const index = parseInt(e.target.id, 0);
if (index !== active) {
setActive(index);
}
if (!showInput) {
setShowInput(!showInput);
}
};
return (
<div className="App">
<StyledShowButton
type="button"
id={1}
active={active === 1}
onClick={handleTabClick}
>
First
</StyledShowButton>
<StyledShowButton
type="button"
id={2}
active={active === 2}
onClick={handleTabClick}
>
Second
</StyledShowButton>
{/* CONTENT */}
{active &&
(active === 1 ? (
<>
<input placeholder="First input" onChange={handleInputChange} />
</>
) : (
<input placeholder="Second input" onChange={handleInputChange} />
))}
</div>
);
}
I have run into a weird bug with my search input component where it loses focus on every keypress and can't figure out why.
const App = () => {
let [characters, setCharacters] = useState([]);
let [filteredCharacters, setFilteredCharacters] = useState([]);
// more state
let [search, setSearch] = useState("");
useEffect(() => {
setIsLoading(true);
axios
.get(`https://swapi.co/api/people/?page=${pageNumber}`)
.then(res => {
console.log(res.data.results);
setCharacters(res.data.results);
setFilteredCharacters(res.data.results);
})
.catch(err => console.error(err))
.then(() => {
setIsLoading(false);
});
}, [pageNumber]);
function updatePage(e) {
// update page
}
function handleSearch(e) {
setSearch(e.target.value);
const filtered = characters.filter(character => {
if (character.name.toLocaleLowerCase().indexOf(e.target.value) !== -1) {
return character;
}
});
setFilteredCharacters(filtered);
}
function SearchBar() {
return (
<React.Fragment>
<StyledInput
type="text"
id="search"
value={search}
placeholder="Search by name..."
onChange={e => handleSearch(e)}
/>
</React.Fragment>
);
}
return (
<React.Fragment>
<GlobalStyles />
<div className="App">
<Heading>React Wars!</Heading>
<SearchBar />
<Pagination handleClick={updatePage} />
{!isLoading && Object.entries(filteredCharacters).length ? (
<CharacterList characters={filteredCharacters} />
) : (
<LoadingBar />
)}
</div>
</React.Fragment>
);
};
I'm also getting a Line 65:50: Expected to return a value at the end of arrow function array-callback-return for the characters.filter() line so I don't know if that has to do with it.
If it helps, the deployed app with the bug can be seen here --> https://mundane-vacation.surge.sh
You have a problem with focus because your component SearchBar is declared inside App component and it is recreated on each rendering. You need to move SearchBar out of App or just move StyledInput on the place of SearchBar. If you choose the first way, I recommend you remove React.Fragment from SearchBar, because it has only one rendered child:
function SearchBar(props) {
return (
<StyledInput
type="text"
id="search"
value={props.value}
placeholder="Search by name..."
onChange={props.onChange}
/>
);
}
And in App:
<SearchBar onChange={handleSearch} value={search} />
To fix your warning, you need always return a boolean value inside the filter function. Now you return nothing (i.e. undefined) in case if the character does not pass the condition. So change your filter function like this:
const filtered = characters.filter(character => {
return character.name.toLocaleLowerCase().indexOf(e.target.value) !== -1
});
I have an application that adds GitHub users to a list. When I put input in the form, a user is returned and added to the list. I want the user to be added to the list only if I click on the user when it shows up after the resource request. Specifically, what I want is to have a click event in the child component trigger the root component’s triggering of the hook, to add the new element to the list.
Root component,
const App = () => {
const [cards, setCards] = useState([])
const addNewCard = cardInfo => {
console.log("addNewCard called ...")
setCards([cardInfo, ...cards])
}
return (
<div className="App">
<Form onSubmit={addNewCard}/>
<CardsList cards={cards} />
</div>
)
}
export default App;
Form component,
const Form = props => {
const [username, setUsername] = useState('');
const chooseUser = (event) => {
setUsername(event.target.value)
}
const handleSubmit = event => {
event.persist();
console.log("FETCHING ...")
fetch(`http://localhost:3666/api/users/${username}`, {
})
.then(checkStatus)
.then(data => data.json())
.then(resp => {
console.log("RESULT: ", resp)
props.onSubmit(resp)
setUsername('')
})
.catch(err => console.log(err))
}
const checkStatus = response => {
console.log(response.status)
const status = response.status
if (status >= 200 && status <= 399) return response
else console.log("No results ...")
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Gitbub username"
value={username}
required
onChange={chooseUser}
onKeyUp={debounce(handleSubmit, 1000)}
/>
<button type="submit">Add card</button>
</form>
)
}
export default Form;
List component,
const CardsList = props => {
return (
<div>
{props.cards.map(card => (
<Card key={card.html_url} {... card}
/>
))}
</div>
)
}
export default CardsList
and the Card Component,
const Card = props => {
const [selected, selectCard] = useState(false)
return (
<div style={{margin: '1em'}}>
<img alt="avatar" src={props.avatar_url} style={{width: '70px'}} />
<div>
<div style={{fontWeight: 'bold'}}><a href={props.html_url}>{props.name}</a></div>
<div>{props.blog}</div>
</div>
</div>
)
}
export default Card
Right now, my Form component has all the control. How can I give control over the addNewCard method in App to the Card child component?
Thanks a million in advance.
One solution might be to create a removeCard method in App which is fired if the click event you want controlling addNewCard doesn't happen.
// App.js
...
const removeCard = username => {
console.log("Tried to remove card ....", username)
setCards([...cards.filter(card => card.name != username)])
}
Then you pass both removeCard and addNewCard to CardList.
// App.js
...
<CardsList remove={removeCard} cards={cards} add={addNewCard}/>
Go ahead and pass those methods on to Card in CardsList. You will also want some prop on card assigned to a boolean, like, "selected".
// CardsList.js
return (
<div>
{props.cards.map(card => (
<Card key={card.html_url} {... card}
remove={handleClick}
add={props.add}
selected={false}
/>
))}
</div>
Set up your hook and click event in the child Card component,
// Card.js
...
const [selected, selectCard] = useState(false)
...
and configure your events to trigger the hook and use the state.
// Card.js
...
return (
<div style={{margin: '1em', opacity: selected ? '1' : '0.5'}}
onMouseLeave={() => selected ? null : props.remove(props.name)}
onClick={() => selectCard(true)}
>
...
This doesn't really shift control of addNewCard from Form to Card, but it ultimately forces the UI to follow the state of the Card component.