React JS onClick set state of clicked index - javascript

How can I update isSelected to true for the item that the user clicks on?
Here is my handleSelect function so far:
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val, index) => {
//val.isSelected = index === true;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}
This function is passed down correctly via props and is working, the issue I'm having is with the logic.
The current logic sets all items in the array to true which is incorrect. I only want to update / toggle the state of the item the user clicked.
I have an array of objects within state, here is what one of the objects looks like:
state = {
allGenres: [
{
id: 1,
genreTitle: 'Horror',
isSelected: false,
}
]
}

Here's how we can do it:
On each clicked Genre, we get its id.
After we have the id, then we toggle the selected genre isSelected flag.
Please follow updateGenres method, to check how we did it in an immutable way.
updateGenres(selectedId) {
const { allGenres } = this.state
this.setState({
// Here we make sure we don't mutate the state
allGenres: allGenres.map(genre => ({
...genre,
// Toggle the clicked one, and reset all others to be `false`.
isSelected: genre.id === selectedId
// If you want to keep the values of the rest genres, then the check should be:
// isSelected: (genre.id === selectedId) ? !genre.isSelected : genre.isSelected
}))
})
}
renderGenres() {
const { allGenres } = this.state
return allGenres.map(genre => <Gengre onClick={() => this.updateGenres(genre.id) })
}
render() {
return <div>{this.renderGenres()}</div>
}
The benefit of this approach is that you don't depend on the index of the allGenres and your toggle implementation will be decoupled to the presentation. Imagine if you change the genres sort (from ASC to DESC), then you have to change your toggle logic too.

Easiest way is that maintain allGenres and selectedGenre separately in state
So handleSelect will be like
handleSelect(id) {
this.setState({
selectedGenre: id
});
}

If the i passed to handleSelect is the index of the genre in the genres array then
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val, index) => {
val.isSelected = index === i;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}
if the i refers to the id of the genre then
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val) => {
val.isSelected = val.id === i;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}

I'll assume that you have some components being rendered, and that you want to click in these components... You could use bind and pass to the callback function the index of the array... for example:
class Component extends React.Component {
constructor() {
super();
this.state = {
allGenres: [{
id: 1,
genreTitle: 'Horror',
isSelected: false
}]
}
}
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val, index) => {
val.isSelected = index === i;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}
render() {
const {allGenres} = this.state;
return (
<ul>
:D
{allGenres.map((genre, i) => (
<li onClick={this.handleSelect.bind(this, i)}>
{genre.genreTitle} {genre.isSelected && 'Is Selected'}
</li>
))}
</ul>
);
}
}
I'm using bind in the onClick callback from the li, this way I have access to the genre index in your callback.
There are some other ways of doing this though, since you have the id of the genre, you might want to use that instead of the index, but it's up to you to decided.

Related

Not understanding what's wrong with my Filter function and state in React

I have the following code:
Parent component:
class App extends Component {
state = {
colors: []
};
async componentDidMount() {
const fetched = await fetch("./response.json");
const fetchedJson = await fetched.json();
const res = fetchedJson.colors;
this.setState({
colors: res
});
}
filterItems = (name) => {
const lowerCaseName = name.toLowerCase();
const newColors = [...this.state.colors];
const res = newColors.filter((color) => {
const lowerCaseColorName = color.name.toLowerCase();
return lowerCaseColorName.includes(lowerCaseName);
});
this.setState({
colors: res
});
};
render() {
const { colors } = this.state;
return (
<div>
<InputText filterItems={this.filterItems} />
<AllData colors={colors} />
</div>
);
}
}
And this is my Child component:
class Filter extends Component {
state = {
inputVal: ""
};
onChange = (e) => {
this.setState({
inputVal: e.target.value
});
this.props.filterItems(e.target.value);
};
render() {
return (
<form onSubmit={this.onSubmit}>
<input
type="text"
onChange={this.onChange}
value={this.state.inputVal}
/>
</form>
);
}
}
export default Filter;
There's also another child component called AllData but its job is just displaying out data and put styling on it, so I'm not including it here.
Currently the data displayed are just:
Fire Dragon
Water Horse
Earth Bird
Wood Dog
Wind Cat
Here are my questions:
The filter function works fine when I type in a word into the search box in filter. However, when I backtrack and remove a previous character down to the whole input string, the res array doesn't return its whole original arrays but instead retains the result of the filter only when I type more.
Ex:
When I type in the string "cat", res becomes: [{name: "Wind Cat", id: 5}
However I remove that string by backtracking on the keyboard, res is still at [{name: "Wind Cat", id: 5}. Why is it not going back to returning all of the items, and how do I fix this?
I currently have this code:
onChange = (e) => {
this.setState({
inputVal: e.target.value
});
this.props.filterItems(e.target.value);
};
However, if I change it to:
this.props.filterItems(this.state.inputVal);
and console.log(name) out in the parent component at filterItems, every time I type in a string, it seems like the console.logged name only display the character before.
Ex:
If I type in the string c -> name would be "" (empty)
If I type in the string ca -> name would be c
Why is this happening?
class App extends Component {
state = {
colors: [],
filtered: [],
};
async componentDidMount() {
const fetched = await fetch("./response.json");
const fetchedJson = await fetched.json();
const res = fetchedJson.colors;
this.setState({
colors: res
});
}
filterItems = (name) => {
const lowerCaseName = name.toLowerCase();
const newColors = [...this.state.colors];
const filtered = newColors.filter(color => {
const parts = color.split(' ').map(part => part.toLowerCase());
return parts.reduce((carry, part) => {
return carry ? carry : part.startsWith(lowerCaseName);
}, false);
});
this.setState({
filtered,
});
};
render() {
const { colors, filtered } = this.state;
return (
<div>
<InputText filterItems={this.filterItems} />
<AllData colors={filtered ? filtered : colors} />
</div>
);
}
}
this happens because you filter the array and thus lose the objects. what you can do is have 2 arrays, one with all the data and one with the filtered data. apply the filter to the array with all the data and set the filtered array to the result

How to update state with usestate in an array of objects?

I'm having some trouble with the React useState hook. I have a todolist with a checkbox button and I want to update the 'done' property to 'true' that has the same id as the id of the 'clicked' checkbox button. If I console.log my 'toggleDone' function it returns the right id. But I have no idea how I can update the right property.
The current state:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: false
},
]
})
const toggleDone = (id) => {
console.log(id);
}
return (
<div className="App">
<Todos todos={state.todos} toggleDone={toggleDone}/>
</div>
);
}
The updated state I want:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: true // if I checked this checkbox.
},
]
})
You can safely use javascript's array map functionality since that will not modify existing state, which react does not like, and it returns a new array. The process is to loop over the state's array and find the correct id. Update the done boolean. Then set state with the updated list.
const toggleDone = (id) => {
console.log(id);
// loop over the todos list and find the provided id.
let updatedList = state.todos.map(item =>
{
if (item.id == id){
return {...item, done: !item.done}; //gets everything that was already in item, and updates "done"
}
return item; // else return unmodified item
});
setState({todos: updatedList}); // set state to new object with updated list
}
Edit: updated the code to toggle item.done instead of setting it to true.
You need to use the spread operator like so:
const toggleDone = (id) => {
let newState = [...state];
newState[index].done = true;
setState(newState])
}
D. Smith's answer is great, but could be refactored to be made more declarative like so..
const toggleDone = (id) => {
console.log(id);
setState(state => {
// loop over the todos list and find the provided id.
return state.todos.map(item => {
//gets everything that was already in item, and updates "done"
//else returns unmodified item
return item.id === id ? {...item, done: !item.done} : item
})
}); // set state to new object with updated list
}
const toggleDone = (id) => {
console.log(id);
// copy old state
const newState = {...state, todos: [...state.todos]};
// change value
const matchingIndex = newState.todos.findIndex((item) => item.id == id);
if (matchingIndex !== -1) {
newState.todos[matchingIndex] = {
...newState.todos[matchingIndex],
done: !newState.todos[matchingIndex].done
}
}
// set new state
setState(newState);
}
Something similar to D. Smith's answer but a little more concise:
const toggleDone = (id) => {
setState(prevState => {
// Loop over your list
return prevState.map((item) => {
// Check for the item with the specified id and update it
return item.id === id ? {...item, done: !item.done} : item
})
})
}
All the great answers but I would do it like this
setState(prevState => {
...prevState,
todos: [...prevState.todos, newObj]
})
This will safely update the state safely. Also the data integrity will be kept. This will also solve the data consistency at the time of update.
if you want to do any condition do like this
setState(prevState => {
if(condition){
return {
...prevState,
todos: [...prevState.todos, newObj]
}
}else{
return prevState
}
})
I would create just the todos array using useState instead of another state, the key is creating a copy of the todos array, updating that, and setting it as the new array.
Here is a working example: https://codesandbox.io/s/competent-bogdan-kn22e?file=/src/App.js
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
title: "take out trash",
done: false
},
{
id: 2,
title: "wife to dinner",
done: false
},
{
id: 3,
title: "make react app",
done: false
}
]);
const toggleDone = (e, item) => {
const indexToUpdate = todos.findIndex((todo) => todo.id === item.id);
const updatedTodos = [...todos]; // creates a copy of the array
updatedTodos[indexToUpdate].done = !item.done;
setTodos(updatedTodos);
};

Removing item from an array of items does not work

I am trying to remove an item from a list of items, but it does not seem to work. I have a page where I can add entries dynamically and items can be removed individually too. Adding seems to just work fine.
Sandbox: https://codesandbox.io/s/angry-heyrovsky-r7b4k
Code
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "",
values: []
};
}
onChange = event => {
this.setState({ value: event.currentTarget.value });
};
onAdd = () => {
this.setState({
value: "",
values: [...this.state.values, this.state.value]
});
};
onRemove = index => {
console.log(index);
let { values } = this.state;
let filteredIDs = values.splice(index, 1);
this.setState({
values: filteredIDs
});
};
render() {
let { values, value } = this.state;
return (
<>
<input
required
placeholder="xyz#example.com"
value={value}
onChange={this.onChange}
/>
<button onClick={this.onAdd}>Add</button>
<div>
<ul className="email-list-holder wd-minus-150">
{values.map((value, index) => (
<li key={index}>
{value}
<button
onClick={() => this.onRemove(index)}
style={{ cursor: "pointer" }}
>
Remove
</button>
</li>
))}
</ul>
</div>
</>
);
}
}
let filteredIDs = values.splice(index, 1); returns the removed item after it removes it from values
you'll want
onRemove = index => {
let { values } = this.state;
values.splice(index, 1);
this.setState({
values
});
tested and works on your codesandbox :p
Here is the working demo for you
https://codesandbox.io/s/purple-snow-kkudc
You have to change the below line.
let filteredIDs = values.splice(index, 1);
Use it instead of above one.
let filteredIDs = values.filter((x, i)=> i!==index);
Hope this will work for you.
I think you are using wrong javascript method when remove the item.
Splice method changes the contents of an array by removing or replacing existing elements and/or adding new elements
Slice method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included) where begin and end represent the index of items in that array. The original array will not be modified.
Replace
let filteredIDs = values.splice(index, 1);
With
let filteredIDs = values.slice(index, 1);
You are setting the removed part of the array instead of the correct one.
onRemove = index => {
console.log(index);
let { values } = this.state;
values.splice(index, 1);
this.setState({
values
});
};
This should work.
You set the removed items as the new values. This will fix it.
onRemove = index => {
console.log(index);
let { values } = this.state;
let filteredIDs = values.splice(index, 1);
this.setState({
values: values
});
};
splice returns the deleted elements and you are setting the removed elements. You can directly do:
values.splice(index, 1);
this.setState({
values,
})
You can also use uniqueId in order to give each new element a uniqueId this would help in filtering logic.
Here's how I may have structured the state and methods:
this.state = {
values: {
todo1: {
value: 'a'
},
todo2: {
value: 'b'
},
}
}
// Addition
this.setState({
values: {
...this.state.values,
uniqueId: {
value: 'New Value from input'
}
}
});
// Deletion
const stateValues = this.state.values;
delete stateValues[uniqueId];
this.setState({
values: stateValues,
});

How to add an arrow to menu elements that have children?

I am trying to add a FontAwesome arrow next to each item in my menu that has children (i.e. I want to indicate that you can click the element to display more data within that category). The menu is populated with json data from an API, and because it is so many nested objects, I decided to use recursion to make it work. But now I am having trouble adding an arrow only to the elements that have more data within it, instead of every single element in the menu.
Does anyone have an idea of how I could change it so the arrow only shows up next to the elements that need it? See below for image
class Menu extends React.Component {
state = {
devices: [],
objectKey: null,
tempKey: []
};
This is where I'm currently adding the arrow...
createMenuLevels = level => {
const { objectKey } = this.state;
const levelKeys = Object.entries(level).map(([key, value]) => {
return (
<ul key={key}>
<div onClick={() => this.handleDisplayNextLevel(key)}>{key} <FontAwesome name="angle-right"/> </div>
{objectKey[key] && this.createMenuLevels(value)}
</ul>
);
});
return <div>{levelKeys}</div>;
};
handleDisplayNextLevel = key => {
this.setState(prevState => ({
objectKey: {
...prevState.objectKey,
[key]: !this.state.objectKey[key]
}
}));
};
initializeTK = level => {
Object.entries(level).map(([key, value]) => {
const newTemp = this.state.tempKey;
newTemp.push(key);
this.setState({ tempKey: newTemp });
this.initializeTK(value);
});
};
initializeOK = () => {
const { tempKey } = this.state;
let tempObject = {};
tempKey.forEach(tempKey => {
tempObject[tempKey] = true;
});
this.setState({ objectKey: tempObject });
};
componentDidMount() {
axios.get("https://www.ifixit.com/api/2.0/categories").then(response => {
this.setState({ devices: response.data });
});
const { devices } = this.state;
this.initializeTK(devices);
this.initializeOK();
this.setState({ devices });
}
render() {
const { devices } = this.state;
return <div>{this.createMenuLevels(devices)}</div>;
}
}
This is what it looks like as of right now, but I would like it so items like Necktie and Umbrella don't have arrows, since there is no more data within those items to be shown
You could check in the map loop from createMenuLevels if the value is empty or not and construct the div based on that information.
createMenuLevels = level => {
const { objectKey } = this.state;
const levelKeys = Object.entries(level).map(([key, value]) => {
//check here if childs are included:
var arrow = value ? "<FontAwesome name='angle-right'/>" : "";
return (
<ul key={key}>
<div onClick={() => this.handleDisplayNextLevel(key)}>{key} {arrow} </div>
{objectKey[key] && this.createMenuLevels(value)}
</ul>
);
});
return <div>{levelKeys}</div>;
};
Instead of just checking if the value is set you could check if it is an array with: Array.isArray(value)

Filtering an array by category name

I would like to filter an array of images based on their category property.
I am able to map and push the category property of all images into a new array and set the state to the new array.
However, I am stuck figuring out how to check for duplicates in the new array, and NOT push a new value if it's already exists.
interface Asset {
id: string
name: string
category: string
}
import * as React from "react"
interface MediaLibraryProps {
mediaLibrary: Asset[]
}
class MediaLibrary extends React.Component<MediaLibraryProps> {
state = {
categories: [],
}
categoryFilter = () => {
const categoryArray: any = []
this.props.mediaLibrary.filter(file => {
if (file.category !== categoryArray) {
categoryArray.push(file.category)
} else {
return
}
})
this.setState({
categories: categoryArray
})
console.log(this.state.categories)
}
render() {
const select = this.state.categories.map(category =>
<option key={category}>{category}</option>
)
return (
<div>
<select>
{ select }
</select>
<button onClick={this.categoryFilter}>LOG</button>
</div>
)
}
}
export default MediaLibrary
I'm expecting only unique names to be pushed to the categories array.
Actual results - everything is being pushed.
See Remove duplicate values from JS array
Example:
uniqueArray = a.filter(function(item, pos) {
return a.indexOf(item) == pos;
})
Quick answer for your question:
const categoryArray: any = []
this.props.mediaLibrary.filter(file => {
if (categoryArray.indexOf(file.category) < 0) {
categoryArray.push(file.category)
} else {
return
}
})
this.setState({
categories: categoryArray
})
console.log(this.state.categories)
Better approach:
The filter here is unnecessary. better approach is to use map.
const categoryArray: any = []
this.props.mediaLibrary.map(file => {
if (categoryArray.indexOf(file.category) < 0) {
categoryArray.push(file.category)
}
})
this.setState({
categories: categoryArray
})
console.log(this.state.categories)
It can be achieved with filter.As per question,
let filteredData = this.props.mediaLibrary.filter(file => file.category !== categoryArray);

Categories