I have a function react component which has an object consisting of different values that renders in react form.
const initialInputValues = {
domain: '',
title1: '',
title2: '',
title3: '',
title4: '',
title5: '',
title6: '',
title7: '',
description1: '',
description2: '',
};
const [values, setValues] = useState(initialInputValues);
//handling input
const handleInputChange = (e) => {
const { name, value } = e.target;
setValues({...values, [name]: value });
}
//handle submit
const handleSubmit = (e) => {
e.preventDefault();
}
I have a reusable FormGroup component like this
<FormGroup
label='Domain'
name='domain'
type='url'
value={values.domain}
onChange={handleInputChange}
/>
<FormGroup
label='Title 1'
name='title1'
value={values.title1}
onChange={handleInputChange}
/>
<FormGroup
label='Title 2'
name='title2'
value={values.title2}
onChange={handleInputChange}
/>
<FormGroup
label='Title 2'
name='title2'
value={values.title2}
onChange={handleInputChange}
/>
<FormGroup
label='Title 3'
name='title3'
value={values.title3}
onChange={handleInputChange}
/>
I have a button under this FormGroup Title fields, now I want to append title4, title5, title6, title7 one by one on click of this Add button.
<button
className='button__addField'
type='button'
onClick={handleAddTitle}
>
Add Title
</button>
Is there any other way except using useRef and hide/show using CSS classes? I have tried that but looks like that is not a feasible solution if in future I want to expand title fields up to 12 or description fields up to 8 etc.
Codesandbox: https://codesandbox.io/s/admiring-estrela-r84fv3?file=/src/App.js:2517-2534
Save the number of title fields in a state and create an array of components with the length of that state.
You don't need to change handleInputChange since it'll just merge the new name properties to the state.
export default function App() {
const initialInputValues = {
domain: "",
title1: "",
title2: "",
title3: "",
title4: "",
title5: "",
title6: "",
title7: "",
description1: "",
description2: ""
};
const [values, setValues] = useState(initialInputValues);
const [titleNum, setTitleNum] = useState(3)
const handleInputChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<div className="sidebar-area">
<form className="form-area" onSubmit={handleSubmit}>
<div className="form__buttons">
<button className="button__share" value="Share" type="submit">
Share
</button>
<button type="button" className="button__export">
Export
</button>
</div>
<div className="form__heading">
<div className="heading__iconTitle">
<h4 className="heading__title">Campaign Basics</h4>
</div>
</div>
<FormGroup
label="Domain"
name="domain"
type="url"
value={values.domain}
onChange={handleInputChange}
/>
{Array.from({length: titleNum}, (_,i, ind = i + 1) => (
<FormGroup
label= {`Title ${ind}`}
name={`title${ind}`}
value={values[`title${ind}`] || ''}
onChange={handleInputChange}
/>
))}
<div className="form__addField">
<div>
<button
className="button__addField"
type="button"
onClick={() => setTitleNum(prev => prev + 1)}
>
Add Title
</button>
</div>
</div>
<div className="form__group__inlineField">
<FormGroup
label="Path 1"
name="path1"
value={values.path1}
onChange={handleInputChange}
/>
<FormGroup
label="Path 2"
name="path2"
value={values.path2}
onChange={handleInputChange}
/>
</div>
<FormGroup
rows="2"
label="Description Line 1"
name="description1"
value={values.description1}
onChange={handleInputChange}
/>
<FormGroup
rows="2"
label="Description Line 2"
name="description2"
value={values.description2}
onChange={handleInputChange}
/>
</form>
</div>
);
}
If you are effectively wanting a dynamic list of titles then I'd suggest storing an array of titles in the state and appending to this array when you want to add another title input. You will want to use functional state updates to correctly update from the previous values state value and not whatever values value is closed over in callback scope.
Example:
...
import { nanoid } from "nanoid"; // used for GUIDs
const MIN_TITLES = 3;
const MAX_TITLES = 7;
export default function App() {
const initialInputValues = {
domain: "",
// Prepopulate titles array with minimum to display initially
titles: Array.from({ length: MIN_TITLES}).map(() => ({
id: nanoid(),
value: ""
})),
description1: "",
description2: ""
};
const [values, setValues] = useState(initialInputValues);
const handleInputChange = (e) => {
const { name, value } = e.target;
setValues((values) => ({ ...values, [name]: value }));
};
// Update title by id
const handleTitleChange = (e) => {
const { id, value } = e.target;
setValues((values) => ({
...values,
titles: values.titles.map((title) =>
title.id === id
? {
...title,
value
}
: title
)
}));
};
const addTitle = () => {
setValues((values) => {
// Shallow copy titles array and append new entry can still add
if (values.titles.length < MAX_TITLES) {
return {
...values,
titles: values.titles.concat({
id: nanoid(),
value: ""
})
};
}
// Otherwise return previous state, no change
return values;
});
};
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<div className="sidebar-area">
<form className="form-area" onSubmit={handleSubmit}>
...
{values.titles.map((title, i) => (
<FormGroup
key={title.id}
id={title.id}
label={`Title ${i + 1}`}
name={`Title${i + 1}`}
value={title.value}
onChange={handleTitleChange}
/>
))}
<div className="form__addField">
<div>
<button
className="button__addField"
disabled={values.titles.length === MAX_TITLES}
type="button"
onClick={addTitle}
>
Add Title
</button>
</div>
</div>
...
</form>
</div>
);
}
Using arrays for these dynamic fields makes it trivial to add new fields, delete existing entries, and sort them later if necessary. In other words, storing the titles in an array makes them much easier to work with.
Similarly, if the plan is to eventually also make the "Path" and "Description" fields dynamic you would convert them also into arrays.
Related
I have dynamic inputs I can add and save to the state, but I want to be able to set initial values, to begin with. I would like to update those values and resave those edits at any time.
Here is the full code below. You can also check out the SANDBOX HERE
import { useState } from "react";
// I want to use these as my initial values. This is the object in my database:
const InitialValuesDB = [{name: "John", age: "108"}, {name: "Jane", age: "204"}]
function Form() {
const [formFields, setFormFields] = useState([{ name: "", age: "" }]);
// I can iterate the values like this:
function LoggingMap() {
InitialValuesDB.map((item, i) => {
console.log('Index:', i, 'name:', item.name);
console.log(item.name)
// But I can't access theme outside of this function:
});
}
LoggingMap()
const handleFormChange = (event, index) => {
let data = [...formFields];
data[index][event.target.name] = event.target.value;
setFormFields(data);
};
const submit = (e) => {
e.preventDefault();
console.log(formFields);
};
const addFields = () => {
let object = {
name: "",
age: "",
};
setFormFields([...formFields, object]);
};
const removeFields = (index) => {
let data = [...formFields];
data.splice(index, 1);
setFormFields(data);
};
return (
<div className="App">
<form onSubmit={submit}>
{formFields.map((form, index) => {
return (
<div key={index}>
{/* But how do I set my initial values (item.name, item.age) as initial values, so that when I reload, the saved values return */}
<input
name="name"
placeholder="Name"
onChange={(event) => handleFormChange(event, index)}
value={form.name}
/>
<input
name="age"
placeholder="Age"
onChange={(event) => handleFormChange(event, index)}
value={form.age}
/>
<button onClick={() => removeFields(index)}>Remove</button>
</div>
);
})}
</form>
<button onClick={addFields}>Add More..</button>
<br />
<button onClick={submit}>Submit</button>
</div>
);
}
export default Form;
Expected Results
If I have 5 inputs with values submitted, I want those values saved in a state and on reload, have those as initial values. I want to edit the inputs, resave that, etc.
For initially putting the items
you should replace your useState with the initial value.
Replace this with:
const [formFields, setFormFields] = useState([{ name: "", age: "" }]);
This
const [formFields, setFormFields] = useState(InitialValuesDB);
Use localStorage, write the state values to localStorage when state updates and read from localStorage on initial render to set the state back to what it was previously before the reload.
EDIT: Try out the following code and see if it fits your usecase.
import { useEffect, useState } from "react";
// I want to use these as my initial values. This is the object in my database:
const InitialValuesDB = [
{ name: "John", age: "108" },
{ name: "Jane", age: "204" },
];
function Form() {
const [formFields, setFormFields] = useState(JSON.parse(localStorage.getItem("key"))|| InitialValuesDB || [{ name: "", age: "" }]);
useEffect(() => {
localStorage.setItem("key", JSON.stringify(formFields))
},[formFields])
// I can iterate the values like this:
function LoggingMap() {
InitialValuesDB.map((item, i) => {
console.log("Index:", i, "name:", item.name);
console.log(item.name);
// But I can't access theme outside of this function:
});
}
LoggingMap();
const handleFormChange = (event, index) => {
let data = [...formFields];
data[index][event.target.name] = event.target.value;
setFormFields(data);
};
const submit = (e) => {
e.preventDefault();
console.log(formFields);
};
const addFields = () => {
let object = {
name: "",
age: "",
};
setFormFields([...formFields, object]);
};
const removeFields = (index) => {
let data = [...formFields];
data.splice(index, 1);
setFormFields(data);
};
return (
<div className="App">
<form onSubmit={submit}>
{formFields.map((form, index) => {
return (
<div key={index}>
{/* But how do I set my initial values (item.name, item.age) as initial values, so that when I reload, the saved values return */}
<input
name="name"
placeholder="Name"
onChange={(event) => handleFormChange(event, index)}
value={form.name}
/>
<input
name="age"
placeholder="Age"
onChange={(event) => handleFormChange(event, index)}
value={form.age}
/>
<button onClick={() => removeFields(index)}>Remove</button>
</div>
);
})}
</form>
<button onClick={addFields}>Add More..</button>
<br />
<button onClick={submit}>Submit</button>
</div>
);
}
export default Form;
I think I don't fully understand you question but here my solution.
Just add useEffect after your removeFields function
useEffect(() => {
setFormFields(InitialValuesDB)
}, [])
Use usefieldarry api of react hook form to maintain dynamic input fields in react form that's great and very simple.
Here is working code sandbox link
https://codesandbox.io/s/nice-swartz-7exhy2?file=/src/form.jsx
Note: I have no knowledge of typescript but implemented it in JavaScript I hope you can convert it into typescript
I am having a simple form that has firstName and lastName.
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
For this I am trying to add validation.
The validation rules are,
Both fields should accept only text
First name is required and should have at least 4 characters.
If Last name field has value, then it needs to be at least 3 characters.
Things I have tried to achieve this,
components/utils.js
export function isLettersOnly(string) {
return /^[a-zA-Z]+$/.test(string);
}
components/basic_details.js
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
return;
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
On handle input field, I am making the validation to check whether the input has value but I am unable to get the point how to catch the actual validation error and display below respective input box.
Kindly please help me to display the validation message on the respective fields.
Working example:
I suggest adding an errors property to the form data in form_context:
const [formValue, setFormValue] = useState({
basicDetails: {
firstName: '',
lastName: '',
profileSummary: '',
errors: {},
},
...
});
Add the validation to basic_details subform:
const ErrorText = ({ children }) => (
<div style={{ color: 'red' }}>{children}</div>
);
const BasicDetails = () => {
const [value, setValue] = React.useContext(FormContext);
const { basicDetails } = value;
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: 'Can have only letters.',
},
},
}));
return;
}
switch (name) {
case 'firstName': {
const error = value.length < 4 ? 'Length must be at least 4.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
case 'lastName': {
const error = value.length < 3 ? 'Length must be at least 3.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
default:
// ignore
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
return (
<>
<br />
<br />
<div className="form-group col-sm-6">
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.firstName && (
<ErrorText>{basicDetails.errors.firstName}</ErrorText>
)}
<br />
<br />
<div className="form-group col-sm-4">
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.lastName && (
<ErrorText>{basicDetails.errors.lastName}</ErrorText>
)}
<br />
</>
);
};
Lastly, check the field values and errors to set the disabled attribute on the next button in index.js. The first !(value.basicDetails.firstName && value.basicDetails.lastName) condition handles the initial/empty values state while the second condition handles the error values.
{currentPage === 1 && (
<>
<BasicDetails />
<button
disabled={
!(
value.basicDetails.firstName && value.basicDetails.lastName
) ||
Object.values(value.basicDetails.errors).filter(Boolean).length
}
onClick={next}
>
Next
</button>
</>
)}
This pattern can be repeated for the following steps.
First you must be getting converting controlled component to uncontrolled component error in your console. For controlled component it is always preferred to use state to set value for the input. And with onChange handler you set the state. I will try to put into a single component so you would get the idea and apply your case
import React, {useState} from 'react';
import {isLettersOnly} from './components/utils'; // not sure where it is in your folder structure
const MyInputComponent = ({value, ...props}) => {
const [inputValue, setInputValue] = useState(value || ''); // input value should be empty string or undefined. null will not be accepted.
const [error, setError] = useState(null);
const handleChange = event => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setError('Invalid Input');
}
setInputValue(value);
}
return (
<>
<input
value={inputValue}
onChange={handleChange}
{...props}
/>
{error && (
<span className={"error"}>{error}</span>
)}
</>
)
}
export default MyInputComponent;
This is a very rudimentary component. just to show the concept. You can then import this component as your input field and pass necessary props like name, className etc from parent.
import React from 'react';
import MyInputComponent from 'components/MyInputComponent';
const MyForm = (props) => {
return props.data && props.data.map(data=> (
<MyInputComponent
name="lastName"
className="form-control"
value={data.lastName}
));
}
I have a search box in my header. i can clear my state after searching but the input doesn't get cleared.
I'm purely using the Searchbox to generate a dropdown that contains links to their respective field. So the input field is purely used to mimic a searc
I tried targeting it with refs but when i finally reach the value i can't use the search anymore.
There is a ref for SearchBarHeader, SearchBox and SearchField. But i'm not sure if that is the correct way to do it.
clearSearchBar = () => {
this.searchBarHeader.current.searchBox.current.searchField.current.value = '';
};
and the code for the searchbox.
class Search extends Component {
state = {
organisationNames: [],
errorMessage: null,
};
searchField = React.createRef();
async componentDidMount() {
const organisations = await getSearch();
this.setState({
organisationNames: organisations,
});
}
searchHandler = (e) => {
const searchValue = e.target.value.toLowerCase();
if (!searchValue) {
this.props.clearSearchResult();
} else {
const result = this.state.organisationNames.filter((organisationName) => {
return organisationName.toLowerCase().includes(searchValue);
});
this.props.setSearchResult(result, () => {
if (this.props.searchResult.length === 0) {
this.setState({
errorMessage: "No Results...",
});
} else {
this.setState({
errorMessage: null,
});
}
});
}
};
clearSearchInput = () => {
this.props.clearSearchResult();
};
render() {
return (
<div className="search">
<div className="form-group">
<input
ref={this.searchField}
type="search"
placeholder="Search for company"
onChange={this.searchHandler}
/>
</div>
<div className="search-result-wrapper">
<ul className="search-results">
{this.props.searchResult === undefined ? (
<Skeleton />
) : (
this.props.searchResult.map((res, id) => {
return (
<Link
key={id}
to={"/r/" + res}
onClick={this.clearSearchInput}
>
<li className="search-item">{res || <Skeleton />} </li>
</Link>
);
})
)}
{this.state.errorMessage === null ? (
""
) : (
<li>{this.state.errorMessage}</li>
)}
</ul>
</div>
</div>
);
}
}
export default Search;
It seems to me that you're missing the "value" attribute on your input that makes it reactive to changes in your state. Grabbing one example from react docs, here's the ideal setup:
this.state = {value: ''};
(...)
handleChange(event) {
this.setState({value: event.target.value});
}
(...)
<input type="text" value={this.state.value} onChange={this.handleChange} />
By following the method above, you won't need to use refs to manually clear the input value. Once the form is submitted, you can simply clear your state...
this.setState({value: ''});
... and your input should be cleared.
Here's the link for the docs: https://reactjs.org/docs/forms.html
You are clearing the ref, not the state. There is also not a value attached to your input, so even if the state was cleared, it will not reflect.
You will of course be able to make the form data more dynamic, without having to set and keep companyName constant.
Here is a simple working example is here: https://codesandbox.io/s/flamboyant-voice-oj85u?file=/src/App.js
export default function App() {
const [formData, setFormData] = useState({ companyName: "" });
const handleChange = (e) => {
setFormData({ companyName: e.target.value });
};
const handleClear = () => {
setFormData({ companyName: "" });
};
return (
<div className="search">
<div className="form-group">
<input
type="search"
name="companyName"
value={formData.companyName}
placeholder="Search for company"
onChange={handleChange}
/>
<button onClick={handleClear}>Clear</button>
</div>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
);
}
Full example on CodeSandbox
(Css is a bit borked)
Writing anything into the input field or the textarea and then clicking on the select wipes the input field & the textarea, I am not sure why -
It seems that is because I am passing jsx elements to the HoverWrapper element.
When I just inlined the WrapInHover element it behaved as expected. Am I passing Elements in a bad way ?
Adding a key to the passed elements didn't seem to solve the issue ...
const Form = () => {
const selectInit = {
open: false,
initial: true,
selected: 'please select',
};
const selectReducer = (state, action) => {
switch (action.type) {
case 'toggle': {
return { ...state, open: !state.open };
}
case 'select': {
return { ...state, selected: action.selected, open: !state.open, initial: false };
}
}
};
const [selectState, selectDispatch] = useReducer(selectReducer, selectInit);
const selectHelp = selected => selectDispatch({ type: 'select', selected });
const OptionComp = ({ txt, value, onClick }) => (
<Option onClick={onClick} state={selectState} value={value}>
{selectState.open && selectState.selected === value ? null : <HoverBorder />}
{txt}
</Option>
);
const WrapInHover = ({ elements }) => {
const [hover, setHover] = useState(false);
return (
<div
css={css`
position: relative;
`}
onMouseEnter={() => {
setHover(true);
}}
onMouseLeave={() => {
setHover(false);
}}>
{elements}
<HoverBorder hover={hover} />
</div>
);
};
return (
<FormEl>
<WrapInHover elements={<Input key='ContactEmailInput' type='email' required />} />
<Label htmlFor='subject' onClick={() => selectDispatch({ type: 'toggle' })}>
Subject
</Label>
<Select>
<OptionComp
onClick={() => selectHelp('art')}
txt='I want you to paint something !'
value='art'
/>
{selectState.initial && !selectState.open ? (
<OptionComp
txt='Please Select An Option'
value='please select'
onClick={() => selectDispatch({ type: 'toggle' })}
/>
) : null}
</Select>
</FormEl>
);
};
Store value of input and message inside state. Also input will lose focus if your WrapInHover is inside main function
export default function App() {
const Form = () => {
const [formState, setFormState] = useState({ email: "", message: "" });
...
const handleFormDataChange = (e, type) => {
const {target: { value }} = e;
setFormState((prevState) => ({ ...prevState, [type]: value }));
};
return (
<FormEl>
<FormTitle>Contact me</FormTitle>
<Label htmlFor="email">Email</Label>
<WrapInHover
elements={
<Input
key="ContactEmailInput"
type="email"
value={formState.email}
onChange={(e) => handleFormDataChange(e, "email")}
required
/>
}
/>
...
<Label htmlFor="message">Message</Label>
<WrapInHover
elements={
<TextArea
key="ContactMessageTextArea"
name="message"
value={formState.message}
onChange={(e) => handleFormDataChange(e, "message")}
/>
}
/>
CSB Example - I will delete after 24 hours.
I'm building a website with react. I create a component which has grouped Textfield , I don't know how to set the value of those Textfield to the state.
The state format should be like : state:{products:[{},{},{}]}
I've tried to create a button to insert new group of Textfield, and an handleChange method to capture the Textvalue,
but still stuck in how to set states.
export default class extends Component {
state = {
count: 0,
products: []
};
handleAddClick = () => {
this.setState(({count}) => ({
count: count + 1
}))
};
handleChange = e => {
//this is where i stuck
};
render() {
const {count} = this.state;
let items = [];
for (let i = 0; i <= count; i++) {
items.push(
<div key={i}>
<TextField
label="product"
margin="normal"
onChange={this.handleChange}
/>
<TextField
label="color"
margin="normal"
onChange={this.handleChange}
/>
<TextField
label="quantity"
margin="normal"
onChange={this.handleChange}
/>
<TextField
label="price"
margin="normal"
onChange={this.handleChange}
/>
</div>
)
}
return <Fragment>
<Button onClick={this.handleAddClick}>
<AddIcon/>
</Button>
{items}
</Fragment>
}
}
I realize that I have to use some id to identify the different group of TextFields, but where to put it , and how to get it in handleChange method?
Your state seems wrong, you need add one more key which handles input changes, lets call it as product. So, this product will handle the current textboxes and once user click add button you can add that to your products array. this produce will an object.
state = {
count: 0,
product:{},
products: [],
};
pass the textfield value with keys so you can fill the product object key,
<TextField value={this.state.product.product}
label="product"
type="text"
margin="normal"
onChange={(e) => { this.handleChange(e, 'product') }}
/>
And set it like this
handleChange = (e, type) => {
this.setState({
product: {
...this.state.product,
[type]: e.target.value,
},
});
};
And when user clicks on add you can push this produce to products array
handleAddClick = () => {
this.setState({
products: this.state.products.concat(this.state.product),
product: {},
})
};
Here is how your component will look:
Replace input with TextField and button with Button
import React, { Component } from 'react';
import { render } from 'react-dom';
import Hello from './Hello';
import './style.css';
class App extends Component {
state = {
count: 0,
product: {},
products: [],
};
handleAddClick = () => {
this.setState({
products: this.state.products.concat(this.state.product),
product: {
product: "",
color: "",
quantity: "",
price: "",
}
})
};
handleChange = (e, type) => {
//this is where i stuck
this.setState({
product: {
...this.state.product, [
type]: e.target.value,
}
});
};
render() {
console.log(this.state);
const { count } = this.state;
let items = [];
for (let i = 0; i <= count; i++) {
items.push(
<div key={i}>
<input value={this.state.product.product}
label="product" type="text"
margin="normal"
onChange={(e) => { this.handleChange(e, 'product') }}
/>
<input value={this.state.product.color}
label="color"
margin="normal"
onChange={(e) => { this.handleChange(e, 'color') }}
/>
<input value={this.state.product.quantity}
label="quantity"
margin="normal"
onChange={(e) => { this.handleChange(e, 'quantity') }}
/>
<input value={this.state.product.price}
label="price"
margin="normal"
onChange={(e) => { this.handleChange(e, 'price') }}
/>
</div>
)
}
return (
<div>
<button onClick={this.handleAddClick}>
add
</button>
{items}
</div>
)
}
}
render(<App />, document.getElementById('root'));
EDIT:
As you want to iterate over products and change the values,You do not need product any more we will use the product as base values for the products, now following things will be changed, you need to get index of each products and by default we will assign one value
like this
let product = {
product: "",
color: "",
quantity: "",
price: "",
};
class App extends Component {
state = {
products: [Object.assign({},product)],
};
handleAddClick = () => {
var newProduce = {
product: "",
color: "",
quantity: "",
price: "",
}
this.setState({
products: this.state.products.concat(newProduce),
})
};
handleChange = (e, type, index) => {
const copiedData = Object.assign({}, this.state);
copiedData.products[index][type] = e.target.value;
this.setState(copiedData);
};
...
Here is the demo