Add/remove form inputs dynamically - javascript

I have a form with one initial empty input field that I want to clone using a Add button and to remove with a Remove one.
As it's not recommended to use index for the keys with dynamic forms, I tried to use uniqid module. But each time the state is updating, keys are renewed and I don't have unique data to identify each input of the form. I can add some items, but can't remove.
input fields have no unique values, no id, how can I do ?
const Form = () => {
const update = e => {};
const items = [{ content: "", color: "" }];
return (
<Fragment>
{items.map((item, idx) => (
<input
htmlFor={`item_${idx}`}
value={item.content}
onChange={update("item", idx)}
/>
))}
<button onClick={e => dispatch(add(idx))}>Add</button>
<button onClick={e => dispatch(remove(idx))}>Remove</button>
</Fragment>
);

You may simply extend your existing items to have unique id property - at its very simplest, you may assign the value of maximum used id increased by 1 to that property - I guess, it'll do the trick for most of practical use cases:
const [inputs, setInputs] = useState([{id:0,value:''}]),
onRowAdd = () => {
const maxId = Math.max(...inputs.map(({id}) => id))
setInputs([...inputs, {id:maxId+1, value:''}])
}
With that, you'll have unique id to anchor to as you delete rows:
onRowRemove = idToDelete => setInputs(inputs.filter(({id}) => id != idToDelete))
Following is the demo of this concept:
const { useState } = React,
{ render } = ReactDOM
const Form = () => {
const [inputs, setInputs] = useState([{id:0,value:''}]),
onInput = (id,value) => {
const inputsCopy = [...inputs],
itemToModify = inputsCopy.find(item => item.id == id)
itemToModify.value = value
setInputs(inputsCopy)
},
onRowAdd = () => {
const maxId = Math.max(...inputs.map(({id}) => id))
setInputs([...inputs, {id:maxId+1, value:''}])
},
onRowRemove = idToDelete => setInputs(inputs.filter(({id}) => id != idToDelete)),
onFormSubmit = e => (e.preventDefault(), console.log(inputs))
return (
<form onSubmit={onFormSubmit} >
{
inputs.map(({id,value}) => (
<div key={id}>
<input
onKeyUp={({target:{value}}) => onInput(id,value)}
/>
<input type="button" onClick={onRowAdd} value="Add" />
<input type="button" onClick={() => onRowRemove(id)} value="Remove" />
</div>
))
}
<input type="submit" value="Log Form Data" />
</form>
)
}
render (
<Form />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

You should create a variable that starts from 0 and adds 1 every time you add a button. That way you will keep track of everyone. Here's an example
let i = 0
const add () => {
//your function to add
i++
//remember i will be the id now
}

Related

How to update hashmap and where can I put a reset for link state on React?

So Im having problems with a form in which takes in text inputs to be set on an object. This object will then be updated in a hashmap of <key, object>.
So far, I can type in the input areas, but if I add another item in the hashmap which generates another div with the input elements it will contain the same value of the previous inputs.
So I need a way to reset those labels within the form and a way to update the hashmap.
I got an updateMap function, but I don't know where to place it for it to await the changes.
Edit: I need to reset the state of link, but when I do a function for it. It says something about preventing infinite loops.
export default function Admin() {
const [link, setLink] = useState({ name: "", link: "" });
const [links, setLinks] = useState(new Map());
const clickAddLink = () => addToMap(links.size + 1, link);
const deleteOnMap = (key) => {
setLinks((prev) => {
const newState = new Map(prev);
newState.delete(key);
return newState;
});
};
const getName = (key) =>
{
let linkFromKey = links.get(key)
return linkFromKey.name
}
const getLink = (key) =>
{
let linkFromKey = links.get(key)
return linkFromKey.link
}
const addToMap = (key, value) => {
setLinks((prev) => new Map([...prev, [key, value]]));
};
const updateMap = (key, value) => {
setLinks((prev) => new Map([...prev, [key, value]]));
};
const clear = () => {
setLinks((prev) => new Map(prev.clear()));
};
.... skipping code ....
<div>
{console.log(link.name)}
{console.log(link.link)}
{[...links.keys()].map((key) => (
<div key={key}>
{links.size > 0 && (
<div>
<form>
<span>Name</span>
<input
type="text"
placeholder={getName(key)}
required
value={link.name}
onChange={(e) =>
setLink({ name: e.target.value })
}
/>
<span>Link</span>
<input
type="text"
placeholder={getLink(key)}
required
value={link.link}
onChange={(e) =>
setLink({ link: e.target.value })
}
/>
</form>
</div>
)}
</div>
))}
</div>
{console.log(link.name)}
{console.log(link.link)}
</div>
```

Adding rows as objects to the key and sending to backend using react

I've been trying to add new rows and when I click on save button at the bottom the row. Values like order,input1,input2, checkbox( if its selected) should be added as each individual objects to the "overallData" inside the "queryparam". So, If I create 3 new rows then it should add 3 objects to the "overallData" array and send to the axios post. I tried to push it in a new array and tried to get it using map it didn't work. Is there any way to do it?
https://codesandbox.io/s/add-remove-items-p42xr?file=/src/App.js:817-827
I implemnted all the logic you want in this code. Now it works like a charm. Dynamially add new form fields. Provide them value. When you click on save button, only the checked ones are submitted. A few thing to mention:
1).In order to meet the program requirements, I changed the default values for a new form to { orderno: 0, inputValue1: "", inputValue2: "", checked: false }.
2).I added a new state orderNumber to control order numbers efficiently
3).When you click the save button the unchecked forms are filtered out.
import React, { useState } from "react";
const App = () => {
const [formValues, setFormValues] = useState([
{ orderno: 0, inputValue1: "", inputValue2: "", checked: false }
]);
// control order number in a state to make sure
// that it does not get messed when you remove
// an indice from formValues
// !! default was 0. so set it to 1
const [orderNumber, setOrderNumber] = useState(1)
const addFormFields = () => {
setFormValues(prevState => [
...prevState,
{
orderno: orderNumber,
inputValue1: '',
inputValue2: '',
checked: false
}
]);
// increment order number
setOrderNumber(prev => prev+1)
};
const removeFormFields = (i) => {
let newFormValues = [...formValues];
newFormValues.splice(i, 1);
setFormValues(newFormValues);
// decrement order number
setOrderNumber(prev => prev-1)
};
const onChangeFieldValue = (index, key, value) => {
setFormValues(prevState => {
let copyState = [...prevState]
if(value === 'toggle') // toggle 'checked' key
copyState[index][key] = !copyState[index][key]
else
copyState[index][key] = value
return copyState
})
}
const saveFields = (e) => {
const queryparam = {
data: "xxx",
DbData: "xxx",
SQlData: "xxx", // only checked ones
overallData: formValues.filter(el => el.checked)
};
console.log(queryparam)
//axios.post('..',queryparam)
};
return (
<>
{formValues.length <= 4
? formValues.map((element, index) => (
<div className="form-inline" key={index}>
<label>{index + 1}</label>
<input
type="text"
value={element.inputVal1}
onChange={(e) => onChangeFieldValue(index, 'inputValue1', e.target.value)}
/>
<input
type="text"
value={element.inputVal2}
onChange={(e) => onChangeFieldValue(index, 'inputValue2', e.target.value)}
/>
<input
type="checkbox"
checked={element.checked}
onChange={(e) => onChangeFieldValue(index, 'checked', 'toggle')}/>
<button
className="button add"
type="button"
onClick={() => addFormFields()}
>
Add
</button>
<button
type="button"
className="button remove"
onClick={() => removeFormFields(index)}
>
Remove
</button>
</div>
))
: ""}
<button
type="button"
className="button remove"
onClick={(e) => saveFields(e)}
>
Save
</button>
<button
type="button"
className="button remove"
onClick={(e) => cancelFields(e)}
>
cancel
</button>
</>
);
};
export default App;

React: How can I remove a specific div element after clicking its associated button?

I have this component which adds a div and its elements to the dom on button click. The adding part works fine as expected but the issue arises when I want to delete.
Right now when I click on the delete button, it does remove the item but it doesn't remove that specific item which the button is associated with. It just removes the div from the top or bottom.
I have been trying to remove that specific div whose button has been clicked to remove. How can I achieve that?
Here's the CodeSandbox.
And here's the code:
import { useState } from "react";
const App = () => {
const [ counter, setCounter ] = useState( 1 );
const handleAddDiv = () => {
setCounter( counter + 1 );
};
const handleRemoveDiv = () => {
setCounter( counter - 1 );
};
return (
<div className="App">
{
Array.from(Array( counter )).map(( item, idx ) => (
<div>
<div>
<input type="text" />
<button onClick={handleRemoveDiv}>Remove</button>
</div>
</div>
))
}
<button onClick={handleAddDiv}>Add</button>
</div>
);
}
export default App;
This is not prefered react way of doing things, but this will work:
import "./styles.css";
import { useState } from "react";
const App = () => {
const [counter, setCounter] = useState(1);
const handleAddDiv = () => {
setCounter(counter + 1);
};
const removeNode = (idx) => document.getElementById(`id-${idx}`).remove();
return (
<div className="App">
{Array.from(Array(counter)).map((item, idx) => (
<div key={idx} id={`id-${idx}`}>
<div>
<input type="text" />
<button onClick={() => removeNode(idx)}>Remove</button>
</div>
</div>
))}
<button onClick={handleAddDiv}>Add</button>
</div>
);
};
export default App;
Generaly if you would like to have it made correactly then you would want to map on a real array and have every item in array eighter having an unique id or simply use map index and then based on which item you click write a function to remove from that array our specific element.
Map over an array of unique Ids
First of all, you should map over an array of items instead of an integer value.
So, on click of add button, you should push a unique ID to the array of items where each ID would denote an item being rendered in your app.
Now, when you click on remove button, you would need to remove that ID from the array of items, which would result in "deletion" of that div from the app.
In my case, I have considered timestamp as a unique ID but should explore other options for generating unique IDs. Working with indices is anti pattern in React especially when you are mapping over an array in JSX as you would encounter issues at one point of time. So, it's a good idea to maintain unique Ids.
Note: Damian's solution is not ideal as DOM Manipulation is avoided in React.
const { useState, useCallback } = React;
const Item = ({ id, removeDiv }) => {
const clickHandler = useCallback(() => {
removeDiv(id);
}, [id, removeDiv]);
return (
<div>
<input type="text" />
<button onClick={clickHandler}>Remove</button>
</div>
);
};
const App = () => {
const [items, setItems] = useState([]);
const addDiv = useCallback(() => {
// using timestamp as a unique ID
setItems([...items, new Date().getTime()]);
}, [items]);
const removeDiv = useCallback((itemId) => {
// filter out the div which matches the ID
setItems(items.filter((id) => id !== itemId));
}, [items]);
return (
<div className="app">
{items.map((id) => (
<Item key={id} id={id} removeDiv={removeDiv} />
))}
<button onClick={addDiv}>Add</button>
</div>
);
};
ReactDOM.render(<App />,document.getElementById("react"));
.app {
text-align: center;
font-family: sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I would use an array state instead of a counter state, because otherwise you don't know which element has to be removed.
import { useState } from "react";
import "./styles.css";
let counter = 1;
export default function App() {
const [array, setArray] = useState([0]);
const handleAddDiv = () => {
setArray((prev) => [...prev, counter++]);
};
const handleRemoveDiv = (idx) => {
var arrayCopy = [...array];
arrayCopy.splice(idx, 1);//remove the the item at the specific index
setArray(arrayCopy);
};
return (
<div className="App">
{array.map((item, idx) => (
<div key={item}>
<div>
<input type="text" />
<button onClick={()=>handleRemoveDiv(idx)}
>Remove</button>
</div>
</div>
))}
<button onClick={handleAddDiv}>Add</button>
</div>
);
}
When I am adding a new item, I give it the value counter++, because I will use it as a key, and a key should be unique.

How to display a set number of jsx elements depending on number placed in an input field. React

I have an input field that takes in a number.(between 1 and 30) I want to display an array of items depending on what number is placed in that text field. how can this been done with React hooks. I have something basic for a start like this, but this might not even be the best way to start this.
export default function App() {
const [state, setState] = React.useState({ value: "" });
const [myArray, updateMyArray] = React.useState([]);
const onSubmit = () => {
updateMyArray((arr) => [...arr, `${state.value}`]);
};
const handleChange = (event) => {
let { value, min, max } = event.target;
value = Math.max(Number(min), Math.min(Number(max), Number(value)));
setState({ value });
};
return (
<>
<input
type="number"
onChange={handleChange}
value={state.value}
min={""}
max={100}
/>
<button onClick={onSubmit}>Confirm</button>
{state.value && (
<>
<div>
{myArray?.map((e) => (
<div>{e}</div>
))}
</div>
</>
)}
</>
);
}
You can do it like this
updateMyArray(new Array(state.value).fill(""));
This will create a new array with the length of state.value and asign it to myArray
Maybe this example will be helpful for you.
function App() {
const [amount, setAmount] = useState(0);
const [submittedAmount, setSubmittedAmount] = useState(0);
// optionally
const onSubmit = () => {
setSubmittedAmount(amount);
};
const handleChange = (event) => {
let { value, min, max } = event.target;
value = Math.max(Number(min), Math.min(Number(max), Number(value)));
setAmount(value);
};
return (
<>
<input
type="number"
onChange={handleChange}
value={amount}
min={0}
max={100}/>
<button onClick={onSubmit}>Confirm</button>
{ /* you can use amount instead of submitted amount if you want */
{submittedAmount > 0 && Array.from({ length: submittedAmount }, (_, index) => <div key={index}>{index}</div> )}
</>
);
}
In my opinion if you can skip submitting and use only amount state. Thanks to this your UI will change automatically after input value change without submitting.
If you know the value of value, you can loop till that number, before the render, like:
const items = [];
for (let i; i < state.value; i++) {
items.push(<div>{i}</div>);
}
return (
<div>
{items}
</div>
)

Every change of the input is stored separately as data (ex : "t", "te", "tes", "test" instead of just having "test")

The fact is I'm using an object to store all the data my form collects. In this object, I have an array, and the only solution I've found to update it is to create a function that adds every input into an array and then sets : this.setState({ 'invites': array });
But, the problem is that every change is added to the array and stored.
How can I fix this ?
<input
className="form-input"
type="email"
placeholder="nom#exemple.com"
name="invites"
onChange={e => addToArray(e, "invites")}
required
/>
function addToArray(e, name) {
emailList.push(e.target.value);
props.handleChangeArray(name, emailList);
}
handleChangeArray = (name, array) => {
this.setState({ [name]: array });
};
EDIT:
In the StackSnippet below, I have updated your example (OPs example can be found here) showing how you can get values on submit:
const { useState } = React;
const WORD_LIST = ["Foo", "Bar", "Baz", "Rock", "Paper", "Scissors", "Hello", "Goodbye"];
function InviteInput(props) {
const { value, onChange } = props;
const handleChange = e => {
onChange && onChange(e);
};
return (
<li>
<input
value={value}
onChange={handleChange}
className="form-input"
type="email"
placeholder="nom#exemple.com"
name="invites"
required
/>
</li>
);
}
function AddInviteButton(props) {
return (
<button onClick={props.onClick}>
Ajouter une autre personne // (Add another person)
</button>
);
}
function InviteForm({ onSubmit, initialInputCount }) {
const [nbInvites, setNbInvites] = useState(
[...Array(initialInputCount).keys()].map(i=>WORD_LIST[i])
);
const onAddInviteClick = () => {
//let id = nbInvites.length + 1;
setNbInvites([
...nbInvites,
//{
//id,
//***********************************************************
// THIS IS WHERE YOU SET THE DEFAULT VALUE FOR NEW INPUTS
//***********************************************************
/*value:*/ WORD_LIST[Math.floor(Math.random() * WORD_LIST.length)]
//***********************************************************
//}
]);
};
const handleChange = (event, index) => {
let newstate = [...nbInvites];
newstate[index]/*.value*/ = event.target.value;
setNbInvites(newstate);
};
const handleSubmit = event => {
onSubmit(event, nbInvites);
};
return (
<div>
{nbInvites.map((item, index) => {
return (
<InviteInput
key={index}
value={item}
onChange={e => handleChange(e, index)}
/>
);
})}
<AddInviteButton onClick={onAddInviteClick} />
<br />
<button onClick={handleSubmit}>Soumettre // Submit</button>
</div>
);
}
function App() {
const [formVals, setFormVals] = useState();
const doSubmit = (event, formValues) => {
setFormVals(formValues);
};
return (
<div className="page">
<h2 className="box-title">
Qui sont les eleves de cette classe ? // (Who are the students in this
class?)
</h2>
<p>
Vous pourrez toujours en ajouter par la suite // (You can always add
some later)
</p>
<InviteForm onSubmit={doSubmit} initialInputCount={5} />
{formVals ? <pre>{JSON.stringify(formVals, null, 2)}</pre> : ""}
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
ORIGINAL ANSWER:
You'll need to use the value prop and use event.target.value to assign the value of whatever is typed to the input.
Something like this:
function Test() {
const [inputs, setInputs] = React.useState([{
id: 1,
value: "1 initial"
}, {
id: 2,
value: "2 initial"
}]);
const handleChange = (event, index) => {
let newstate = [...inputs];
newstate[index].value = event.target.value;
setInputs(newstate);
}
const addInput = () => {
let id = inputs.length + 1;
setInputs([...inputs, {
id,
value: `${id} initial`
}])
}
return(
<div>
<button onClick={addInput}>Add Input</button>
{inputs.map((item, index) => {
return <div><input type="text" value={inputs[index].value} onChange={e=>handleChange(e,index)} /></div>
})}
</div>
);
}
ReactDOM.render(<Test />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
Instead of onChange, we can use onKeyUp to identify user typing complete event,
We can use onKeyUp event to identify user input event and debounce is to identify when user completes entering all the keywords, we need to use loadash for this functionality
It's just one line with underscore.js debounce function:
onKeyUp={e => _.debounce(addToArray(e, "invites") , 500)}
This basically says doSomething 500 milliseconds after I stop typing.
For more info: http://underscorejs.org/#debounce
Updated the above line
<input
className="form-input"
type="email"
placeholder="nom#exemple.com"
name="invites"
onKeyUp={e => _.debounce(addToArray(e, "invites") , 500)}
required
/>
function addToArray(e, name) {
emailList.push(e.target.value);
props.handleChangeArray(name, emailList);
}
handleChangeArray = (name, array) => {
this.setState({ [name]: array });
};
Source - Run javascript function when user finishes typing instead of on key up?

Categories

Resources