I am creating a react element which allows users to send emails to friends/family. The element defaults to 3 input fields however, the user can click a button to add additional input fields.
My idea to build this component is to have a button which increments a counter and then to use that counter value to run a loop and add items to an array
<button onClick={handleAddEmail}> Add email </button>
function handleAddEmail() {
setNumberOfEmails(numberOfEmails + 1);
}
Now for this to work I need an NumberOfEmails hook. This hook will be initialized with the first three input fields.
const [numberOfEmails, setNumberOfEmails] = useState(3);
Additionally there is a inviteEmails hook to handle the state of each input.
const [inviteEmails, setInviteEmails] = useState([])
All of these inputs should be rendered in a function:
function renderEmailFields() {
const emailContainer = [
<input
value={inviteEmails[0]}
onChange={(e) => handleEmailChange(0, e)}
placeholder="Enter email here..."
/>,
<input
value={inviteEmails[1]}
onChange={(e) => handleEmailChange(1, e)}
placeholder="Enter email here..."
/>,
<input
value={inviteEmails[2]}
onChange={(e) => handleEmailChange(2, e)}
placeholder="Enter email here..."
/>];
for (let i = 3; i < numberOfEmails; i++) {
emailContainer.push(
<input
value={inviteEmails[i]}
onChange={(e) => handleEmailChange(i, e)}
placeholder="New email..."
/>
);
}
return emailContainer;
The code I have above is close to working however adding a new input field causes the previous inputs to lock and become uneditable.
Any advice would be appreciated.
You don't really need a second state atom for the number of inputs if you ask me.
See live CodeSandbox here.
The useCallback and dataset magic make it so you don't need a separate onClick handler function for each input.
.filter(Boolean) removes all falsy values; empty strings are falsy.
For the heck of it, this also lets you remove items from the array.
function App() {
const [inviteEmails, setInviteEmails] = React.useState(["", "", ""]);
const handleEmailChange = React.useCallback((event) => {
const index = parseInt(event.target.dataset.index, 10);
setInviteEmails((inviteEmails) => {
const newInviteEmails = [...inviteEmails];
newInviteEmails[index] = event.target.value;
return newInviteEmails;
});
}, []);
const removeEmail = React.useCallback((event) => {
const index = parseInt(event.target.dataset.index, 10);
setInviteEmails((inviteEmails) => {
const newInviteEmails = [...inviteEmails];
newInviteEmails.splice(index, 1);
return newInviteEmails;
});
}, []);
const addEmail = React.useCallback(
() => setInviteEmails((inviteEmails) => [...inviteEmails, ""]),
[]
);
return (
<div>
{inviteEmails.map((email, index) => (
<div key={index}>
<input
value={email}
data-index={index}
onChange={handleEmailChange}
placeholder="Enter email here..."
/>
<button onClick={removeEmail} data-index={index}>
×
</button>
</div>
))}
<button onClick={addEmail}>Add email</button>
<pre>{JSON.stringify(inviteEmails.filter(Boolean), null, 2)}</pre>
</div>
);
}
Related
I have this sample codesandbox I made, though, in my original one there were already a lot of values. I only recreated the problem that I had.
Codesandbox: https://codesandbox.io/s/youthful-firefly-40mig3?file=/src/App.js
I have this in my App.js where I define some of my needed values and then pass it to 2 other components.
<div className="App">
{/* some codes here */}
<h1>App page</h1>
Initial Amount: 1000
<br />
Total Amount: {totalAmount}
<br />
----------------------------------------------------------
<h6>Passing the total amount and the second input here</h6>
<Second input2={input2} setInput2={setInput2} />
----------------------------------------------------------
<h6>Third here</h6>
<Third totalAmount={totalAmount} />
</div>
I have this Second.js where I compute the totalAmount. This is the problem that I encountered. The required is not being recognized here. I just wanted to stop the user from submitting the form if this field is empty. Are there other ways where it would not submit if a certain field is empty or just leave the input field to 1 if it is empty?
const Second = ({ input2, setInput2 }) => {
return (
<div>
Second here
<form>
<input
fullWidth
value={input2}
onChange={(e) => setInput2(e.target.value)}
required
/>
</form>
</div>
);
};
export default Second;
This is the Third.js where I passed down the totalAmount and added more values such as the name and age. In here the name and age are being recognized. This is also where I submit everything here.
const Third = ({ totalAmount }) => {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const handleSubmit = (e) => {
e.preventDefault();
try {
console.log(name, age, totalAmount);
} catch (err) {
console.log(err);
}
};
return (
<div>
Third here
<form onSubmit={handleSubmit}>
<label>Name</label>
<input
fullWidth
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<br /> <br />
<label>Age</label>
<input
fullWidth
value={age}
onChange={(e) => setAge(e.target.value)}
required
/>
<br /> <br />
<button type="submit">submit</button>
</form>
</div>
);
};
export default Third;
You can pass the input2 as a prop to Third.js and there you can check if input2 doesn't exist then don't submit the values.
In the App.js pass input2 to the Third component.
<Third totalAmount={totalAmount} input2={input2} />
Then in Third.js check if the input2 value exists.
const Third = ({ totalAmount, input2 }) => {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const handleSubmit = (e) => {
e.preventDefault();
if (input2) {
try {
console.log(name, age, totalAmount);
} catch (err) {
console.log(err);
}
}else {
alert('Please enter the second value');
}
}
}
i am using react.i have 2 inputs that by clicking a button dynamically ganerats.
this the code:
useEffect(() => {
const myFields = fieldsArray.map((field) => {
return (
<div key={field} className="col-12 m-auto d-flex">
<input id={field} name={`question${field}`} onChange={handleChangeFields} placeholder='سوال' className="form-control col-4 m-2" />
<input id={field} name={`answer${field}`} onChange={handleChangeFields} placeholder='جواب' className="form-control col-4 m-2" />
</div>
)
})
setFields(myFields)
}, [fieldsArray])
i need to save value of 2 inputs together in an opjects like this :
[{question : '',answer : ''}]
and the new 2 input's value are going to update the above array like this:
[{question : '',answer : ''}, {question : '',answer : ''}]
i can save each input's value separately like this :
const handleChangeFields = (e) => {
const { name, value } = e.target
setFieldsValue([...fieldsValue, { [name]: value }])
}
but i want each 2 input's value together
i need to save each 2 inputs together.
how do i do that?
This is a general example of how I think you can tie in what you're currently doing, and add in as little new code as possible. This logs the new data state when the button is clicked.
Have two states. One for collecting the updated form information (form), and another for the combined form data array (data).
Have a form with a single onChange listener (event delegation) so you can catch events from all the inputs as they bubble up the DOM. That listener calls the handleChange function which updates the form state.
Have a button with an onClick listener that calls handleClick which takes the current form state and adds it to the data state.
I would be a bit wary of storing JSX in state. You should have that map in the component return and only updating the actual field data with basic information.
One final issue - your inputs cannot have the same id. ids must be unique. I'd get rid of them altogether.
const { useEffect, useState } = React;
function Example() {
// Initialise two states. `data` is an array
// and `form` is an object
const [ data, setData ] = useState([]);
const [ form, setForm ] = useState({});
// Add the form data to the `data` array
function handleClick() {
setData([ ...data, form ]);
}
// We're using event delegation so we need to
// check what element we changed/clicked on
// - in this case the INPUT element. We can then
// update the form state with the name/value pair of that input
function handleChange(e) {
const { nodeName, name, value } = e.target;
if (nodeName === 'INPUT') {
setForm({ ...form, [name]: value });
}
}
// Just logs the data state after a change
useEffect(() => console.log(data), [data]);
return (
<form onChange={handleChange}>
<input name="question" type="text" />
<input name="answer" type="text" />
<button
type="button"
onClick={handleClick}
>Update form
</button>
</form>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
this problem solved for me :
function App() {
const [results, setResults] = useState([{ question: "", answer: "" }])
// handle input change
const handleInputChange = (e, index) => {
const { name, value } = e.target
const list = [...results]
list[index][name] = value
setResults(list)
}
// handle click event of the Remove button
const handleRemoveClick = (index) => {
const list = [...results]
list.splice(index, 1)
setResults(list)
}
// handle click event of the Add button
const handleAddClick = () => {
setResults([...results, { question: "", answer: "" }])
}
// handle submit to servers
const handlesubmit = () => {
// axios
console.log(results)
}
return (
<div className="App">
{results?.map((result, index) => {
return (
<div key={index}>
<input
name="question"
value={result.question}
onChange={(e) => handleInputChange(e, index)}
/>
<input
name="answer"
value={result.answer}
onChange={(e) => handleInputChange(e, index)}
/>
{results.length !== 1 && (
<button onClick={() => handleRemoveClick(index)}>
Remove
</button>
)}
{results.length - 1 === index && (
<button onClick={handleAddClick}>add</button>
)}
</div>
)
})}
<div style={{ marginTop: 20 }}>{JSON.stringify(results)}</div>
<button onClick={handlesubmit}>submit</button>
</div>
)
}
export default App
I have a component in React where when the "Add" button is clicked, new form fields are created. However, when I click on the add button, the new form fields are added at the top of the add button instead of below it.
I need the add button to be fixed at the top and the new form fields created to stack at the top of the other existing form fields. I would like to know what I need to change on my code to achieve this.
function DynamicFormFields() {
const [inputList, setInputList] = useState([{ name: "", quantity: "" }]);
// handle input change
const handleInputChange = (e, index) => {
const { name, value } = e.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
};
// handle click event of the Remove button
const handleRemoveClick = (index) => {
const list = [...inputList];
list.splice(index, 1);
setInputList(list);
};
// handle click event of the Add button
const handleAddClick = () => {
setInputList([{ name: "", quantity: "" }, ...inputList]);
};
return (
<>
<div className="DynamicFormFields">
<h3>
<p>Dynamic Form</p>
</h3>
{inputList.map((x, i) => {
// x -> element, i -> index
return (
<div className="box">
<div className="btn-box">
{inputList.length - 1 === i && (
<button onClick={handleAddClick}>Add</button>
)}
</div>
<input
name="name"
placeholder="Name"
value={x.name}
onChange={(e) => handleInputChange(e, i)}
/>
<input
type="number"
name="quantity"
// className="ml10"
min="1"
max="12"
></input>
{inputList.length !== 1 && (
<button className="mr10" onClick={() => handleRemoveClick(i)}>
Remove
</button>
)}
</div>
);
})}
</div>
</>
);
}
Ok, I'm not sure I completely understand what you want but I made some changes real quick (may contain mistakes). Is this what you're trying to do? Also I would suggest making your input items into small sub components, that may help you see the overall picture more clearly.
function DynamicFormFields() {
const [inputList, setInputList] = useState([{ name: "", quantity: "" }]);
// handle input change
const handleInputChange = (e, index) => {
const { name, value } = e.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
};
// handle click event of the Remove button
const handleRemoveClick = (index) => {
const list = [...inputList];
list.splice(index, 1);
setInputList(list);
};
// handle click event of the Add button
const handleAddClick = () => {
setInputList([{ name: "", quantity: "" }, ...inputList]);
};
return (
<>
<div className="DynamicFormFields">
<h3>
<p>Dynamic Form</p>
</h3>
<div className="box">
<div className="btn-box">
<button onClick={handleAddClick}>Add</button>
</div>
<div>
{inputList.map((x, i) => {
// x -> element, i -> index
return (
<div>
<input
name="name"
placeholder="Name"
value={x.name}
onChange={(e) => handleInputChange(e, i)}
/>
<input
type="number"
name="quantity"
// className="ml10"
min="1"
max="12"
></input>
{inputList.length !== 1 && (
<button
className="mr10"
onClick={() => handleRemoveClick(i)}
>
Remove
</button>
)}
</div>
);
})}
</div>
</div>
</div>
</>
);
}
The main changes here is that I put the button outside of the loop as it shouldn't be dependent on your other input items, as I understand it. I also wrapped your inputs inside another div to completely separate the button and the inputs. Make sure only what needs to be updated gets updated so you don't get unexpected behaviour.
I'm building a controlled form with dynamic fields.
The Parent component get data from a redux store and then set state with the values.
I don't want to make it with too much code lines so I turn the dynamic fields into a component.
States stay in the parent component and I use props to pass the handlechange function.
Parent :
function EditAbout(props) {
const [img, setImg] = useState("");
const [body, setBody] = useState(props.about.body);
const [instagram, setInstagram] = useState(props.about.links.instagram);
const [linkedin, setLinkedIn] = useState(props.about.links.linkedin);
const [press, setPress] = useState(props.about.press)
const handleSubmit = (e) => {
// Submit the change to redux
};
// set states with redux store
useEffect(() => {
setBody(props.about.body);
setInstagram(props.about.links.instagram);
setLinkedIn(props.about.links.linkedin);
setPress(props.about.press);
}, []);
const handleChangeChild = (e, index) => {
e.preventDefault();
let articles = press
const {value, name } = e.target
if (name === "title") {
articles[index].title = value;
} else {
articles[index].link = value;
}
setPress(articles)
console.log(articles[index])
}
return (
<Box>
<h1>CHANGE ABOUT ME</h1>
<Input
label="Image"
name="img"
type="file"
variant="outlined"
margin="normal"
onChange={(e) => setImg(e.target.files)}
/>
<Input
label="body"
value={body}
name="body"
onChange={(e) => setBody(e.target.value)}
variant="outlined"
multiline
rowsMax={12}
margin="normal"
/>
<Input
label="instagram"
value={instagram}
name="instagram"
variant="outlined"
margin="normal"
onChange={(e) => setInstagram(e.target.value)}
/>
<Input
label="Linkedin"
value={linkedin}
name="linkedin"
variant="outlined"
margin="normal"
onChange={(e) => setLinkedIn(e.target.value)}
/>
<Child press={press} onChange={handleChangeChild} />
{props.loading ? (
<CircularProgress color="black" />
) : (
<Button onClick={handleSubmit} variant="contained">
Send
</Button>
)}
</Box>
);
}
Child :
function Child(props) {
const { press, onChange } = props;
const inputsMarkup = () =>
press.map((article, index) => (
<div key={`press${index}`} style={{ display: "flex" }}>
<input
name="title"
value={press[index].title}
onChange={(e) => onChange(e, index)}
/>
<input
name="link"
value={press[index].link}
onChange={(e) => onChange(e, index)}
/>
<button>Delete</button>
</div>
));
return (
<div>
<h1>Press :</h1>
{inputsMarkup()}
</div>
);
}
Everything is fine when I'm typing in the Parent inputs. But when I'm using Child fields state update for one character but come back at its previous state right after.
It also doesn't display the character change. I can only see it in the console.
Thanks you in advance for your help
The problem is that you're mutating the state directly. When you create the articles variable (let articles = press) you don't actually create a copy and articles doesn't actually contain the value. It's only a reference to that value, which points to the object’s location in memory.
So when you update articles[index].title in your handleChangeChild function, you're actually changing the press state too. You might think that's fine, but without calling setPress() React will not be aware of the change. So, although the state value is changed, you won't see it because React won't re-render it.
You need to create a copy of the press array using .map() and create a copy of the updated array element. You can find the updated handleChangeChild() below:
const handleChangeChild = (e, index) => {
e.preventDefault();
const { value, name } = e.target;
setPress(
// .map() returns a new array
press.map((item, i) => {
// if the current item is not the one we need to update, just return it
if (i !== index) {
return item;
}
// create a new object by copying the item
const updatedItem = {
...item,
};
// we can safely update the properties now it won't affect the state
if (name === 'title') {
updatedItem.title = value;
} else {
updatedItem.link = value;
}
return updatedItem;
}),
);
};
I'm creating a webapp that allows users to create custom input components. This is in the form of MUI Select and TextFields:
const CTextField = (
<TextField id="outlined-basic" variant="outlined" size="small" />
);
const CSelect = function(match) {
match = match.substring(1, match.length - 1);
const matches = match.split("/");
let menus = [];
matches.forEach(single => {
menus.push(<MenuItem value={single}>{single}</MenuItem>);
});
return (
<FormControl>
<Select>{menus}</Select>
</FormControl>
);
};
The issue I'm coming across is, how can I grab all the values placed or selected from the components. They're created by the end user, so there can be any number of them, and I'm trying to keep it in order with plain text.
What I think works is simply grabbing the raw data including html and stripping it away, then grabbing the values. But because I want this to be dynamic, that'd end up being slow the larger the text is. Is there an efficient method of grabbing all the raw text as well as the values of the input components?
Don't worry about how many elements you have - that's why Arrays are there:
const { useState } = React
const App = (props) => {
const [inputFieldsList, setInputFieldsList] = useState([]);
return (
<div>
<h1>Input field list:</h1>
<button
onClick={() => {
const newVal = [...inputFieldsList, '']
setInputFieldsList(newVal)
}}>ADD INPUT FIELD</button>
{inputFieldsList.map((item, i) => (
<div>
<input
type="text"
onChange={(e) => {
const newList = [...inputFieldsList]
newList[i] = e.target.value
setInputFieldsList(newList)
console.log(inputFieldsList)
}}
/>
</div>
))}
</div>
);
}
const rootElement = document.getElementById('app')
ReactDOM.render(<App />, rootElement)
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="app"></div>
You should use useState for storing the values of the input, and onChange function to set your state when a user types in the input.
const Hello = () => {
const[values, setValues] = useState({
input1: '',
input2: ''
});
const changeHandler = () => {
setValues(
...values,
[event.target.name]: event.target.value
);
}
return (
<>
<input type="text" value={values.input1} name="input1" onChange={changeHandler} />
<input type="text" value={values.input2} name="input2" onChange={changeHandler} />
</>
);
}