React.JS: Add and Remove field dynamically - javascript

I have in my state:
this.state = { inputs: ['0'] }
In my render :
{this.state.inputs.map((input, i) => <Travel formName={FORM_NAME} number={input}/>)}
<button onClick={ () => this.appendInput() }> CLICK ME TO ADD AN INPUT </button>
<button onClick={ () => this.deleteInput() }> Delete </button>
Now when I click on this.appendInput() I add a field like this:
appendInput() {
var newInput = `${this.state.inputs.length}`;
console.log("`${this.state.inputs.length}`" , `${this.state.inputs.length}`)
this.setState(prevState => ({ inputs: prevState.inputs.concat([newInput]) }));
console.log("this.state.inputs add ", this.state.inputs)
this.forceUpdate();
}
But I don't understand how can I do with the deleteInput, how can I delete the last field in this way?
EDIT:
I have tried like this:
deleteInput(){
var newInput = this.state.inputs.pop();
this.setState( ({ inputs: newInput }));
this.forceUpdate();
}
but then I receive the message:
_this.state.inputs.map

write your delete function like this, you were assigning popped data from the array.
deleteInput(){
var newInput = [...this.state.inputs];
newInput.pop();
this.setState( ({ inputs: newInput }));
this.forceUpdate();
}

pop() will return the removed element, which you don't want to set your state as. Doing this will set your state to the single element that was just removed. This is what causes your error, as you're trying to call map() on an object which is not a list.
Instead, your deleteInput() should do this:
this.setState(state => state.inputs.slice(0,-1))
This will properly remove the last element of your inputs array from your state.
Additionally, as a side note, calling setState() automatically queues a rerender, so calling this.forceUpdate() is unnecessary and inefficient.

You should be able to use .pop() to remove the last item in your inputs array
This thread has some more detailed answers about pop and slice: Remove last item from array

Related

How do I update the textarea using useState?

I have a textarea array with values that can be updated. The text values in the array are updated when text is entered into the textarea. The array can also be updated externally.
The problem is that Textarea doesn't want to update its values with setState() like regular text does.
export function GameActions({}) {
const [array, setArray] = useState<Type>([]);
const changeText = (id: number, text: any) => {
actions[id].text = text;
setActions(actions);
};
return {actions.map((action, index) => (<Textarea
defaultValue={action.text}
onChange={(e) =>
changeText(index, e.currentTarget.value)
}
/>))};
};
Provided actions is an array state property, it should be:
setActions((actions)=>{
return actions.map((act,i)=>{
if(i == id) {
act.text = text;
}
return act;
});
});
When a state property is updated using its previous value, the callback argument should be used.
Also, to update an element of a state array, map should be used, rather than the indexation operator [].
Please read this article, to learn how to update state arrays.

How to apply the same method multiple times to different array elements using a key?

I'm working with a gallery of cards in react.
Each card needs to have its own distinct state and toggle method, which is currently specified by a number at the end of the function, e.g. onDropdownToggle1.
The array is applied using this.state.cards.map((product, key) => ( ....
Is there a way to call each function distinctly using the key in the name of the function? Like directly modifying the function name onDropdownToggle + {key}? Or is it better to take the key in as a parameter somehow?
Here is the link to my sandbox which shows exactly what's going on: https://codesandbox.io/s/weathered-worker-0y5vm
Any suggestions would be very much appreciated. Thanks!
Just pass the key as a parameter. Either do it inline like this:
onToggle={(isDropdownOpen) => this.onDropdownToggle(key, isDropdownOpen)}
Or you could curry it like this:
this.onDropdownToggle = key => isDropdownOpen => {
...
}
...
onToggle={this.onDropdownToggle(key)}
Then you can use a single handler to modify each card.
You will have to use a computed property name when settings state
this.setState({
[key]: isDropdownOpen
});
Final result:
// One toggle function that uses the key to update state
this.onDropdownToggle = (key, isDropdownOpen) => {
this.setState({
[key]: isDropdownOpen
});
};
// One dropdown select function that uses the key to update state
this.onDropdownSelect = (key, event) => {
this.setState({
[key]: !this.state[key]
});
};
...
<Dropdown
isPlain
position="right"
onSelect={(e) => this.onDropdownSelect(key,e)}
toggle={
<KebabToggle
onToggle={(isDropdownOpen) => this.onDropdownToggle(key, isDropdownOpen)} />}
isOpen={this.state[key]}
dropdownItems={kebabDropdownItems}
/>
You could also consider using the item.name instead of the key to pass to the functions. But only if they are always unique.

How to Delete individual items from list in react

I have built a simple ToDo App. Rending the tasks from the form input is working fine, but I am unable to delete the tasks when clicked on Delete button.
export class TodoList extends Component {
constructor(props) {
super(props)
this.state = {
task:'',
items:[]
}
}
onChangeHandler=(e)=>{
this.setState({
[e.target.name]: e.target.value
})
}
addItem=(e)=>{
e.preventDefault()
if (this.state.task!==""){
this.setState({
items:[...this.state.items,this.state.task],
task:''
})
}
}
removeItem=(index)=>{
const remainingItems = this.state.items.filter(j => {
return j !== index
})
this.setState({
items: remainingItems
})
};
render() {
return (
<div>
<form>
<input type='text' name="task"onChange={this.onChangeHandler} value={this.state.task} placeholder='Enter Task'/>
<button type='submit' onClick={this.addItem}>Add Task</button>
</form>
<Lists items={this.state.items}
delete={this.removeItem}/>
</div>
)
}
}
export class Lists extends Component {
removeItems=(index)=>{
this.props.delete(index)
}
render() {
return (
<div>
{this.props.items.map((item,index)=>
<li className="Lists" key={index}>{item}
<button type='button' onClick={this.removeItems(index)}>Remove</button>
</li>)}
</div>
)
}
}
Do you even happen to have any items to delete here or the list comes up empty? Delete function itself looks fine but you have couple of problems here.
Don't use index as a key. In case you're reordering or deleting (which you are doing) an array of items, you can run into a lot of issues. Here's a good article: https://medium.com/#vraa/why-using-an-index-as-key-in-react-is-probably-a-bad-idea-7543de68b17c
Probably the error is with this since you're deleting key which, since it's an iterator, is reassigned to another element when array repopulates itself. Change iterator to some other unique identifier for each element.
You're calling removeItems method as soon as it's set. If you have invoked methods (with ()) inside return of render, it will be executed immediately on each refresh. That's why I'm asking do you have anything to delete at all since, if delete function is okay written, this would probably delete all items as soon as they are added.
Best method would be to use dataset. To each element you can add dataset like this:
data-item-id={some-id} and you can fetch it inside your method from the fired event like this const clickedId = event.currentTarget.dataset.someId. Note that dataset in the element must be written like-this, and it's rewritten automatically when fetching it into camelCase (likeThis). Then you can use this index to target the element you want inside the array and delete it.
Note that the iterator issue still applies, and you need a different unique identifier.
Let me know if you need further explanation.
You can delete the current item using splice method.
removeItem = index=> {
let newArray = this.state.items;
newArray.splice(index, 1);
this.setState({
items: newArray
});
};
It would be better to use onClick for removing item like this :
<button type='button' onClick={()=>this.removeItems(index)}>Remove</button>
Hope this helps.
I prefer to pass item that i would like to remove, index can be decieving becouse it changes.
Find index by unique key, i use item.id as unique key.
removeItem = item => {
const items = this.state.items;
// if using lodash i use findIndex
const index = _.findIndex(items, i => i.id === item.id)
// if plain js
const index = items.findIndex(i => i.id === item.id)
items.splice(index, 1);
this.setState({
items
});
};

multiple filters, one onChange function

My structure:
index.js
--> Filters
--> {list}
Filters contains multiple input elements that set the state in index.js via props.
list displays as you guessed it, a list of elements. I now want to filter this list based on the values returned by the filters. Pretty standard I think and found on millions of sites.
The issue is that I want to make the input onChange function reusable. With only one input in the Filters component I had this (shortened):
<input
value={this.state.anySearch}
onChange={this.handleChange}
/>
handleChange = event => {
const value = event.target.value;
this.setState({ anySearch: value });
};
With multiple inputs I tried this, aka reusable for any input:
handleChange = name => event => {
const value = event.target.value;
this.setState({ name: value });
};
onChange={this.handleChange("anySearch")}
But this doesn't work anymore. State now shows only one letter at a time when console logged.
What is best practice to filter according to multiple different criteria à la kayak, and how do I rewrite the handleChange function without pushing each letter into an array?
handleChange = name => event => {
const value = event.target.value;
this.setState({ name: value });
};
your idea of returning a function is correct, you just have an error when setting the state
this.setState({ name: value });
change it to
this.setState({ [name]: value });
Regarding the second question, you can simply iterate over your array and filter out the objects that match that specific criteria, to avoid unnecessary overhead you can implement a caching mechanism that keep track of the searches the user has already done.
something like this:
function filter(criteria) {
if(cache[criteria]) {
return cache[criteria];
}
cache[criteria] = myArray.filter(elem => elem.criteria === criteria);
return cache[criteria];
}

Remove value from state (set new state)

I am trying to remove a value from my state.
I am using .filter as I believe this is the simplest way of doing it. I also want to implement an undo function ( but that's outside the scope of this question).
I have put this code in a sandbox
https://codesandbox.io/s/yrwo2PZ2R
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
movies: x.movies,
};
}
remove = (e) => {
e.preventDefault();
console.log('remove movie.id:', e.target.value)
const index = e.target.value
this.setState({
movies: this.state.movies.filter((_, e) => e.id !== index)
});
}
render() {
return (
<div>
{this.state.movies.map(e =>
<div key={e.id}>
<li>{e.name} {e.id}</li>
<button value={e.id} onClick={this.remove}>remove</button>
</div>,
)}
</div>
);
}
}
Two problems.
First of all, the index you're getting from the event target value is a string, but you're comparing against a number. Change the index declaration as follows:
const index = Number(e.target.value);
Secondly, your filter is a little off. This will work:
this.state.movies.filter(movie => movie.id !== index)
The problem is index has string type, but id in objects has number type. You need type cast, for example:
const index = Number(e.target.value);
Other than that, you have some wrong _ in callback of filter function call. You don't need it. You need:
this.state.movies.filter(e => e.id !== index)
By the way I don't recommend to name values this way. Why e? You have array of movies. Use movie. Why index? You have id to remove. Then use idToRemove name.
You also have problem with adding items.
Firstly, you can add items like this:
this.setState({
movies: [...this.state.movies, { name: item.value.name, id: item.value.id }],
})
Another point: you have to autoincrement id. You can store last value in a variable. this.idCounter for example. And add will look like:
this.setState({
movies: [...this.state.movies, { name: item.value.name, id: this.idCounter++ }],
})
Example: https://codesandbox.io/s/2vMJQ3p5M
You can achieve the same in the following manner
remove = (e) => {
e.preventDefault();
console.log('remove movie.id:', e.target.value)
const index = e.target.value
var movies = [...this.state.movies]
var idx = movies.findIndex((obj) => obj.id === parseInt(index))
movies.splice(idx, 1);
this.setState({
movies
});
}
Also use parseInt to convert index to a string before comparing.
Directly setting the current state from the previous state values can cause problems as setState is asynchronous. You should ideally create a copy of the object and delete the object using splice method
CODESANDBOX

Categories