Can I retrieve a attribute value from a parent element in ReactJS? - javascript

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)}

Related

ReactJS - Dropdown events cant be triggered because Input looses focus

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}
</>
);
}

Handle rendering array of editable components

I have a contentEditable component:
EditableComponent.js
const EditableComponent = (props) => {
return <p contentEditable>{props.children}</p>;
};
In the ParentComponent I can add EditableComponents to an array (someArr) with useState, and then I pass someArr and setSomeArray via props to another component (AllEditable) to render it:
ParentComponent.js
import EditableComponent from "./components";
import AllEditable from "./components";
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setContentArr((prevContentArr) => {
return [...prevContentArr, <EditableComponent />];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
Each rendered component (EditableComponent) have a span with the content 'X' that should delete the target onClick:
AllEditable.js
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.filter((_, idx) => idx !== index);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
);
})}
</div>
);
};
The problem:
It doesn't matter which component I'm trying to delete, it removes the last component (even in the Components section in the developer tools) and I'm pretty sure that the logic behind deleting (filter) works well.
I tried deleting the contentEditable attribute, and added some unique random text in each component and then it worked as expected!.
Things I tried
Creating a new array without the removed target
Nesting the components in someArr in objects with key: index, example: {idx: 0, content: <EditableComponent />}
Added a function - onDoubleClick for each EditableComponent to toggle the attribute contentEditable, true or false.
Replaced the element in EditableComponent to <textarea></textarea> instead of <p contentEditable></p>
Your problem is all your EditableComponent components have the same key (because you haven't declared key on them). React cannot identify which EditableComponent you want to delete. You can check this document.
I'd suggest you add a key attribute like below
<EditableComponent key={prevContentArr.length - 1}/>
For safer index reservation, you should use map instead filter
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.map((x, idx) => idx !== index ? x : null);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return content ? (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
) : null;
})}
</div>
);
};
const { useState } = React
const EditableComponent = (props) => {
return <p contentEditable>{props.children}</p>;
};
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.map((x, idx) => idx !== index ? x : null);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return content ? (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
) : null;
})}
</div>
);
};
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setSomeArr((prevContentArr) => {
return [...prevContentArr, <EditableComponent key={prevContentArr.length - 1}/>];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
ReactDOM.render(
<ParentComponent/>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Side note that keys with index values are not the best because your array keeps changing that would make key changes as well. You can use some unique key generator to handle that situation instead.
You shluld change the way you set keys, setting the element key to: "content-index" is confusing for react, because once you remove an item all the indexes will change, and therefore your keys.
So you need to find a way to have static keys for your elements. That way everytime they render, the key will be the same.
The logic is working correctly and in fact it is deleting the correct elements, however since you are using the index to identify elements, you will never see this since it will always appear that only the last one is removed (when the array updates, the indices will update too).
So if you had 0,1,2 and removed 1 now you have an array with indices 0,1
To test this you can place a reference to the index when rendering the content editable div, based on testing you can see that the correct element is in fact being removed:
import "./styles.css";
import React, { useState } from "react";
const EditableComponent = (props) => {
return (
<p contentEditable>
{props.children}
{props.idtfy}
</p>
);
};
const AllEditable = (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.filter((_, idx) => idx !== index);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return (
<div key={`content-${idx}`}>
<span>{idx}</span>
<span
onClick={() => {
deleteContentHandler(idx);
}}
>
X
</span>
<div>{content}</div>
</div>
);
})}
</div>
);
};
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setSomeArr((prevContentArr) => {
return [
...prevContentArr,
<EditableComponent idtfy={prevContentArr.length + 1} />
];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
That said, your key should probably point to some static value, but that will only impact rendering order, not which item is being clicked/closed.

Cannot read properties of undefined (React)

I have an issue with my code, i have a search input and a list of countries
When i type some words i have an error which cause to my app collapse
I've been trying for about two days to find the problem but can't find it.
This is the error message : Uncaught TypeError: Cannot read properties of undefined (reading 'filter')
const Country = ({name, num}) =>{
//console.log(name)
return (
<div>
<p>{name}</p>
</div>
)} // Component
const Input = ({onSearch, search}) =>{
return (
<div>
Find countries: <input onChange={onSearch} value={search} />
</div>
)} // Component
import { useState, useEffect } from "react";
import axios from "axios";
import Input from "./components/Input";
import Country from "./components/Country";
const App = () => {
const [countryList, setCountryList] = useState();
const [search, setSearch] = useState("");
const [filter, setFilter] = useState(false);
useEffect(() => {
axios
.get("https://restcountries.com/v3.1/all")
.then((res) => setCountryList(res.data));
}, []);
const onSearch = (event) => {
if (event.target.value === " ") setFilter(false);
else {
setFilter(true);
setSearch(event.target.value);
}
};
const countriesList = filter
? countryList.filter((country) => {
return country.name.common.includes(search);
})
: null ;
return (
<div>
<Input onSearch={onSearch} search={search} />
{filter ? (
countriesList.length === 0 ? (
<h3>No match</h3>
) : countriesList.length > 10 ? (
<h3>Too many matches, specify another filter...</h3>
) : countriesList.length < 10 && countriesList.length > 1 ? (
countriesList.map((country, i) => (
<Country name={country.name.common} key={i} num={false} />
))
) : (
<Country name={countriesList[0].name.common} num={true} /> &&
console.log("common", countriesList)
)
) : (
<h3>Search for any country</h3>
)}
</div>
);
};
countrylist state must be an array.
Try using array in countyList as its undefined initially
const [countryList, setCountryList] = useState([]);
Also you seems to be you accessing filter (a state value directly while component initilize). Please try replacing with below code. Let me know if issue persists, should be a very simple fix
const [countriesList, setCountriesList] = useState([]);
useEffect(()=>{
if(filter){
setCountriesList(
countryList.filter((country) => {
return country?.name?.common?.includes(search);
}))
}else{
setCountriesList(countryList);
}
},[filter])

Button returns correct values, but is not displayed to the screen once I press it

I'm doing this fullstack course to learn about web dev: https://fullstackopen.com/en/part2/getting_data_from_server
And I have a problem with section 2.13*.
I am able to display a list of the countries after filtering with the button. Pressing the button returns the correct values from the countries arrays as seen with the console.log(country), but it doesn't to the screen.
My guess is that I can't return a div item within another item, but I am pretty sure that works in normal cases, so the fact that I'm returning the item to a different return statement might be the issue?
How can I fix this? I know my code is messy and a refactor might make things simpler, but it is currently beyond me right now since I find it easier to refactor working code.
In the DisplayCountries component, I've tried apply a map to countries that fit the filter input and prints it into a div item. Now when I add a button beside it, it displays correctly, but pressing it does not yield what I expect.
Is the correct approach here to use a useState with the button, so that each button click will rerender the screen? How would I go about doing this if so?
After pressing the button, the detailed information of the country should display such as in 2.12* from the linked website.
import { useState, useEffect } from 'react'
import axios from 'axios'
//feed array of countries
const printLanguages = (languages) => {
// console.log('map', languages.map(language => language.name))
return languages.map(language => <li key={language.name}>{language.name}</li>)
}
const displayCountryView = (country) => {
console.log(country)
return (
<div>
<h1>{country.name}</h1>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<h2>languages</h2>
<ul>
{printLanguages(country.languages)}
</ul>
<img src={country.flag} height="100" width="100"></img>
</div>
)
}
const DisplayCountries = ({ countries, searchValue }) => {
const displayFilter = filteredCountries(countries, searchValue)
// console.log('current search', searchValue)
if (displayFilter.length >= 10) {
return <p>Too many matches, specify another filter</p>
} else if (isFiltered(searchValue)) {
if (displayFilter.length > 1 && displayFilter.length < 10) {
console.log('new level')
return displayFilter.map(country => <div key={country.name}>{country.name}{showButton(country)}</div>)
} else if (displayFilter.length === 1) {
// console.log('suh')
// return displayFilter.map(country => <p key={country.name}>{country.name}</p>)
const country = displayFilter
return displayCountryView(country[0])
// console.log(country)
// console.log('country.name', country[0])
// console.log(country[0].languages)
// console.log(printLanguages(country[0].languages))
// return (
// <div>
// <h1>{country[0].name}</h1>
// <p>capital {country[0].capital}</p>
// <p>population {country[0].population}</p>
// <h2>languages</h2>
// <ul>
// {printLanguages(country[0].languages)}
// </ul>
// <img src={country[0].flag} height="100" width="100"></img>
// </div>
// )
}
} else {
return <p>empty</p>
}
}
const showButton = (country) => {
return <button type="button" onClick={() => displayCountryView(country)}>show</button>
}
const filteredCountries = (countries, searchValue) => {
const showCountries = (!isFiltered(searchValue))
? [{ name: "hi" }]
: countries.filter(country => country.name.toLowerCase().includes(searchValue.toLowerCase()))
// const countryMap = countries.map(country => country.name.toLowerCase())
// console.log(countryMap)
// return countryMap
return showCountries
}
function isFiltered(value) {
if (value === '') {
return false
} else {
return true
}
}
const Filter = ({ search, onChange }) => {
return (
<form >
<div>
find countries <input value={search} onChange={onChange} />
</div>
</form>
)
}
const App = () => {
const [countries, setCountries] = useState([])
const [search, setNewSearch] = useState('')
const [showCountry, setShowCountry] = useState('false')
useEffect(() => {
// console.log('effect')
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
// console.log('promise fulfilled')
setCountries(response.data)
})
}, [])
// const countryNames = countries.map(country => country.name)
// console.log('name', countryNames)
const handleSearchChange = (event) => {
setNewSearch(event.target.value)
}
// const fil = countries.filter(country => country.name==='Afg')
// console.log(countries[0])
// console.log('filtered:',fil)
// console.log(countries[0])
// console.log('render', countries.length, 'persons')
return (
<div>
<Filter search={search} onChange={handleSearchChange} />
<form>
<div>
<DisplayCountries countries={countries} searchValue={search} />
</div>
</form>
</div>
)
}
export default App;

how to render a component according to the condition?

In two places it is necessary to display the component Items.
But in one place you need an additional component Filters, in another - not need. The Filters component is inside Items.
When I do this , it doesn't work:
const Items = ({ items, users, resourceUrl }) => {
const [goods, setGoods] = useState(items);
const [customers, SetCustomers] = useState(users);
const handleSubmit = e => {
e.preventDefault();
// ...
};
const changeUser = e => {
// ...
}
function DisplayFilter(props) {
const isDisplay = props.isDisplay;
const isUsers = props.users;
if (isDisplay == undefined) {
return (
<ItemsFilters changeUser={changeUser} users={isUsers} />
)
}
return null;
}
return (
<div>
<DisplayFilter isDisplay={resourceUrl} users={users}/>
{goods.map((element) => (
<Comment
date={element.date}
name={element.name}
doctor={element.user}
text={element.text}
/>
))}
<span className="btn-show_more">
<a className="button button_large" onClick={handleSubmit} rel="next">Show more</a>
</span>
</div>
)
};
The data transmitted is not the same.
If you remove the output of the condition and insert in return - <ItemsFilters changeUser={changeUser} users={isUsers} />, that all works. But then the filter is displayed in other places where it should not.
You can do like this
return (
<div>
{props.isDisplay
? (
<ItemsFilters changeUser={changeUser} users={props.users} />
)
: null}
{goods.map((element) => (
<Comment
date={element.date}
name={element.name}
doctor={element.user}
text={element.text}
/>
))}
<span className="btn-show_more">
<a className="button button_large" onClick={handleSubmit} rel="next">Show more</a>
</span>
</div>
)
You could write it like this:
const Items = ({ items, users, resourceUrl }) => {
const [goods, setGoods] = useState(items);
const [customers, SetCustomers] = useState(users);
const handleSubmit = e => {
e.preventDefault();
// ...
};
const changeUser = e => {
// ...
}
return (
<div>
{resourceUrl && <ItemsFilters changeUser={changeUser} users={users} />}
{goods.map((element) => (
<Comment
date={element.date}
name={element.name}
doctor={element.user}
text={element.text}
/>
))}
<span className="btn-show_more">
<a className="button button_large" onClick={handleSubmit} rel="next">Show more</a>
</span>
</div>
)
};
This will only render the Filter element, if your condition (resourceUrl) is present.
Personally, I like having my conditional statements defined explicitly and hold the component that I want to render conditionally in a variable.
The advantage with this is that it makes the code very clear and easy to understand and most importantly, it allows me to perform more complex conditions without getting my code all messy and hard to read as is usually the case with ternary operators.
const Items = ({ items, users, resourceUrl }) => {
const [goods, setGoods] = useState(items);
const [customers, SetCustomers] = useState(users);
const handleSubmit = e => {
e.preventDefault();
// ...
};
const changeUser = e => {
// ...
}
let itemFilters = null;
if(resourceUrl) {
itemFilters = <ItemsFilters changeUser={changeUser} users={users} />
}
return (
<div>
{itemFilters}
{goods.map((element) => (
<Comment
date={element.date}
name={element.name}
doctor={element.user}
text={element.text}
/>
))}
<span className="btn-show_more">
<a className="button button_large" onClick={handleSubmit} rel="next">Show more</a>
</span>
</div>
)
};

Categories