i have implemented dynamic input fields that are created whenever u click add Product button but i dont why i can only pick itemAmount values but not input values for productId and itemQuantity.
iam using react v16.0.2
productItems: Array(2)
0: {"": "450000"} //productId is empty
1: {"": "670000"} //why does it only pick itemAmount but not the quantity and productId
i happen to use react-bootstarp for styling.
constructor(){
this.state = {
invoice_notes: '',
itemValues: [] //suppose to handle the productItems that are being added.
}
//for the product items
handleItemChange(i,evt){
const {name,value} = evt.target;
const items = [...this.state.itemValues]
items[i] = {[name]: value}
//this.setState({items}) //it just has empty values
this.setState({itemValues: items}) //works but empty for productId n amount
}
addProductItem(){
const item = {
productId: '',
itemQty:'',
itemAmount: ''
}
this.setState({itemValues: [...this.state.itemValues, item]})
}
createItemsUI(){
//Use #array.map to create ui (input element) for each array values.
//while creating the fields, use a remove button with each field,
return this.state.itemValues.map((elem,i)=> //recall arrow functions do not need return statements like {}
<div key = {i}>
<FormGroup controlId="formControlsSelect">
<ControlLabel>Product</ControlLabel>
<FormControl
componentClass="select"
name = "productId"
bsClass="form-control"
value = {this.state.itemValues[i].productId}
onChange = {this.handleItemChange.bind(this,i)}>
<option value = "" disabled>{'select the product'}</option>
{productOptions.map((item,i)=>{
return(
<option
key = {i} label= {item} value = {item}
>
{item}
</option>
)
})}
</FormControl>
</FormGroup>
<FormInputs
ncols={["col-md-4","col-md-4"]}
proprieties={[
{
label: "Quantity",
type: "number",
bsClass: "form-control",
defaultValue:this.state.itemValues[i].itemQty,
onChange: this.handleItemChange.bind(this,i)
},
{
label: "Amount",
type: "number",
bsClass: "form-control",
defaultValue: this.state.itemValues[i].itemAmount,
onChange: this.handleItemChange.bind(this,i)
}
]}
/>
<Button onClick = {this.removeProductItem.bind(this,i)}>Remove</Button>
</div>
)
}
}
please i have read many similar questions so i think iam on the right track.. I prefer to be inquisitive
So after #steve bruces comment, i made some changes to the handleItemChange function but i happen to get almost the same behaviour of not picking the productId and itemQty input values.
handleItemChange(i,evt){
const name = evt.target.getAttribute('name');
console.log('let see name', name) //i getting the name of the atrribute --> productId, itemQty, ItemAmount
const items = [...this.state.itemValues]
items[i] = {[name]: value}
this.setState({itemValues: items})
}
This is what happens if i try using setState({items}) as suggested by similar questions
items[i] = {[name]: value}
this.setState({items})
RESULT of the productItems when u click the submit button
productItems: Array(2)
0: {productId: "", itemQty: "", itemAmount: ""}
1: {productId: "", itemQty: "", itemAmount: ""}
but when i use this.setState({itemValues: items}) atleast i can get the last value of the itemAmount
productItems: Array(2)
0: {itemAmount: "25000"}
1: {itemAmount: "45000"}
length: 2
Any help is highly appreciated
I believe your error is coming up in your handleItemChange(evt,i) function.
It looks like you want to return "placeholder" by destructuring evt.target. This does not work because evt.target doesn't understand "name". I think you want to try evt.target.attribute('name') to return the name. evt.target.value will return the value of that input and evt.target.name does not exist so it returns undefined.
A small tip. You no longer have to bind functions in your constructor. You can can get rid of your constructor and update your functions to be arrow functions.
For example this:
handleItemChange(i,evt){
const {name,value} = evt.target;
const items = [...this.state.itemValues]
items[i] = {[name]: value}
//this.setState({items}) //it just has empty values
this.setState({itemValues: items}) //works but empty for productId n amount
becomes this:
handleItemChange = (i,evt) => {
// const {name,value} = evt.target;
//updated that to this here
const { value } = evt.target;
const name = evt.target.attribute("name");
...
And the call to in your onChange would look like this:
onChange = {() => { this.handleItemChange(evt,i)}
you must add the () => in your onChange so the handleItemChange is not invoked on every render before it's actually changed. It's invoked the moment you pass the params evt, and i. So the () => function is invoked onChange and then handleItemChange(evt,i) is invoked.
Hope this helps.
Related
I am working on creating form inputs, where the IDs of the inputs match an item id from the backend data. I need to keep track of the dynamic IDs, so I want the name of the fields to match also.
How can I create a form with an object of dynamic field names? It seems that RHF creates an array instead, so if my itemID is 7841 the array is 7841 in length?
interface DynamicCharges {
...other fields with static names
additionalCharges: {
[id: number]: { // where the `id` comes from the backend so i need the input to follow this
qty: number;
amt: string;
};
}
}
and then implementing the UI:
const chargesWithDynamicIds = (additionalCharges || []).reduce(
(acc, additionalCharge) => {
const keyName = `_${additionalCharge.id}`;
return {
...acc,
additionalCharges: {
...acc.additionalCharges,
[keyName]: {
qty: 0,
amt: '$0.00',
},
},
};
},
otherStaticChargesToInitObj
);
const methods = useForm<ReservationChargesShape>({
defaultValues: chargesWithDynamicIds,
});
<Controller
name={`additionalCharges.${additionalCharge?.id}.qty`}
control={control}
render={({ value, onChange }) => (
<Input
value={value}
onChange={e => {
onChange(e.target.value); // sets the actual QTY input
setValue( // sets the AMT field that is dependent on this QTY change
`additionalCharges.${additionalCharge?.id}.amt`,
formatMoney(
Number(e.target.value) *
centsToDollars(
additionalCharge?.maximum_unit_amount
)
)
);
}}
/>
)}
/>
the original /default form state looks OK
But it turns into an array once the form re-renders
What am I doing wrong with this?
EDIT: I think this is related to the small snippet in their docs:
can not start with a number or use number as key name
What I have done is to prefix the field with _ so the key is no longer numeric, but _7841. Is there any other solution to numeric keys?
The problem come from that you're calling onChange inside your onChange event.
To set the actual QTY input you should use a useState.
The error is: TypeError: filters.brand.find is not a function
and it says that it happened here:
checked={
173 | filters.brand.find((item) => item === brand)
| ^
174 | ? true
175 | : false
176 | }
Below is what I'm trying to achieve:
I'm building an e-commerce site using NextJS and amongst other features, users can choose to view products by category or subcategory. On either of these pages, users can choose to refine the results even further using filters. One of these filters should allow the user to display only the items of a certain brand/s.
These brands are presented to the user as checkboxes. So a checked checkbox should mean "include this brand" otherwise do not:
<FormGroup>
{brands.map((brand) => (
<FormControlLabel
control={
<Checkbox
size="small"
name={brand}
checked={
filters.brand.find((item) => item === brand)
? true
: false
}
onChange={() => handleBrandChange(brand)}
/>
}
label={brand}
key={brand}
/>
))}
</FormGroup>
btw: I tried using "includes" instead of "find" above but still ran into the same problem.
The initial value for filter.brand comes from the URL's query string.
const { query, ...router } = useRouter();
const [filters, setFilters] = useState({
brand: query.brand ? (typeof query.brand === "string" ? [query.brand] : query.brand) : [],
});
I check if the query string has "brand", which could either come back as a string or an array. If it is there, I check its type. If it's a string, I set filter.brand's value to an array with that string inside otherwise I take it as is. If the query string does not have a brand, I assign filter.brand an empty array.
btw, I am leaving out most of the code that is of no relevance to this problem.
Below is how I tried to handle the change (when the user clicks on a brand checkbox):
const handleBrandChange = (brand) => {
const brandArray = filters.brand;
if (brandArray.find((item) => item === brand)) {
setFilters({ ...filters, brand: brandArray.filter((item) => item !== brand) });
} else setFilters({ ...filters, brand: brandArray.push(brand) });
};
and below is what should happen every time the state changes:
useEffect(() => {
const urlObject = {
query: {
category: query.category,
sortBy: filters.sortBy,
maxPrice: filters.maxPrice,
},
};
if (filters.brand.length > 0) urlObject.query.brand = filters.brand;
if (query.subcategory) {
urlObject.pathname = "/[category]/[subcategory]";
urlObject.query.subcategory = query.subcategory;
} else {
urlObject.pathname = "/[category]";
}
router.push(urlObject);
}, [filters, query.subcategory, query.category]);
Sorry for the messy question. I am not used to asking questions on here (this is only my second time), I have not been coding for that long and English is not my first language. Any help would be greatly appreciated.
I have a Form with a couple Form.Select attributes. My onChange() works for the <Form.Select> attributes without multiple set. However, it cannot handle selections from <Form.Select> attributes that do have multiple set.
I would like to have a single onChange function that can handle data changing for instances of Form.Select with or without the "multiple" flag set.
The Form Class:
class SomeForm extends React.Component {
handleSubmit = event => {
event.preventDefault();
this.props.onSubmit(this.state);
};
onChange = event => {
const {
target: { name, value }
} = event;
this.setState({
[name]: value
});
console.log("name: " + name)
console.log("value: " + value)
};
render() {
return (
<Form onSubmit={this.handleSubmit} onChange={this.onChange}>
<Form.Select label="Size" options={sizes} placeholder="size" name="size" />
<Form.Select
label="Bit Flags"
placeholder="Bit flags"
name="flags"
fluid
multiple
search
selection
options={bits}
/>
<Form.Button type="submit">Submit</Form.Button>
</Form>
);
}
}
The logs are never called when I select options from the Form with multiple set.
Some possible bits to populate the options prop of Form.Select:
const bits = [
{key: '1', text: "some text 1", value: "some_text_1"},
{key: '2', text: "some text 2", value: "some_text_2"},
];
How can I modify onChange() to handle both Form.Select attributes as listed above?
Please note that this question is different from question on StackOverflow which are concerned only with an onChange that can only be used for updating an array.
Multiple selects are a weird beast. This post describes how to retrieve the selected values.
If you want to define a form-level handler, you need to make an exception for multiple selects:
const onChange = (event) => {
const value = isMultipleSelect(event.target)
? getSelectedValuesFromMultipleSelect(event.target)
: event.target.value
this.setState({[event.target.name]: value})
};
const isMultipleSelect = (selectTag) => selectTag.tagName === 'SELECT' && selectTag.multiple
const getSelectedValuesFromMultipleSelect = (selectTag) => [...selectTag.options].filter(o => o.selected).map(o => o.value)
I have a dropdown list which is populated from a database. The first option is 'none' (actual record in database with objectId) which should be the default option and only needs to be changed if the user wants to, otherwise it should just use that initial value when submitting the form. However, even though it is selected and has a valid objectId, I still get a validation error saying the field is empty. The validation error only goes away if I select something else from the select menu or select something else and then select 'none' again. I am using Joi-browser for validation.
schema = {
subcategoryId: Joi.string()
.required()
.label("Subcategory"),
}
This is the select menu:
<Form onSubmit={this.handleSubmit}>
<Form.Group controlId="subcategoryId">
<Form.Label>Sub-category</Form.Label>
<Form.Control
as="select"
name="subcategoryId"
value={this.state.data.subcategoryId}
onChange={this.handleChange}
error={this.state.errors.subcategory}
>
{this.state.subcategories.map(subcategory => (
<option key={subcategory._id} value={subcategory._id}>
{subcategory.name}
</option>
))}
</Form.Control>
{this.state.errors.subcategoryId && (
<Alert variant="danger">
{this.state.errors.subcategoryId}
</Alert>
)}
</Form.Group>
And here is my state:
state = {
data: {
name: "",
description: "",
categoryId: "",
subcategoryId: "",
price: ""
},
categories: [],
subcategories: [],
errors: {}
};
const { data: subcategories } = await getSubcategories();
this.setState({ subcategories });
And this is the html output of the dropdown's first field which I want selected by default:
<option value="5d4b42d47b454712f4db7c67">None</option>
The error I get back is that the category Id cannot be empty, yet each option in the select menu has a value. I am new to react but perhaps the value is only actually assigned upon change?
You need to edit componentDidMount. After you get your subcategories, you'll need to set the state to of this.state.data.subcategoryId to one of the categories. This is because you're using a controlled component. Otherwise, it'll still be set to "", which isn't one of the valid values for the <select> component, and likely why it's failing validation.
async componentDidMount() {
// getting a copy of this.state.data so as not to mutate state directly
const data = { ...this.state.data };
const { data: subcategories } = await getSubcategories();
// filter the array to a new array of subcategories that have the name === 'none'
const arrayOfSubcategoriesWhereNameIsNone = subcategories.filter(i => i.name === 'none');
const getIdOfFirstElementOfArray = arrayOfSubcategoriesWhereNameIsNone [0]._id;
//set getIdOfFirstElementOfArray equal to the function's local copy of this.state.data.subcategoryId
data.subcategoryId = getIdOfFirstElementOfArray;
// update the state with the mutated object (the function's local copy of this.state.data)
this.setState({ subcategories, data });
}
I'm trying to update my state which is :
inputData: {
id: '',
inputArr: ['']
}
From a form I'm getting from my state and I generate him like so :
inner = arr.input.map((inputs, index) => (
<li key={index} name={index} id={inputs.id}>
<label>{inputs.inputLabel}</label>
<input
// value={inputs.inputValue}
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
</li>
))
It will have mulitple inputs, so I wish to add all their inputs in the array by the order so i can extract it later, but my handleChange function doesn't seem to work, anyone know why (I cant get the ID that I'm passing with the event, nor to update the array)?
handleChangeInp = e => {
const id = e.target.id;
console.log(`the id is ${id}`);
const index = Number(e.target.name);
const inputData = this.state.inputData.inputArr.map((ing, i) => {
console.log(i);
i == index ? e.target.value : ing;
});
}
thanks for the help!
e.target will reference to the element and to get its props, like id, name, etc. you all need to have the props on the element itself.
To get the id, you'll need to pass the id props:
<input
id={inputs.id}
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
Now, e.target.id will get the inputs id. To get the name, do the same as above.
In this example, e.target is <input /> element. And to get its properties, you just need to add the props.
I think the solution is simple. Instead of putting the id and name attribute on the li element, add it to the input. This should allow you to get name from e.target.
UPDATED: Based on additional research:
The Synthetic onChange event in React does not pass the id attribute along. You need to come up with another solution. If you need to keep both the name and index of the input within an array, you can combined them with a special character and split them in the event handler. Looking back on some forms I have created, I see that I found it easy to use the pipe character |, since it is usually not used, but even still, I know it will be the last pipe in my string.
Updated Map of Inputs Array
inner = arr.input.map((inputs, index) => {
console.log({inputs});
return (
<li key={index}>
<label>{inputs.inputLabel}</label>
<input
name={inputs.id + "|" + index}
value={inputs.inputValue} /* try not commenting this out */
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
</li>
)
});
Updated onChange handler
handleChangeInp = e => {
const name = e.target.name.split("|");
// index will be last element of array, pop it off
const index = Number(name.pop());
// join the name array back with the pipe character just in case it's used in the name
const id = name.join("|");
console.log(`the id is ${id}`);
console.log(`the index is ${index}`);
const inputData = this.state.inputData.inputArr.map((ing, i) => {
console.log(i);
i == index ? e.target.value : ing;
});
}
First update:
Proper parameters for generated inputs:
let inner = arr.input.map((inputs, index) => (
<li key={index} >
<label>{inputs.inputLabel}</label>
<input
id={index}
name={inputs.id}
// value={inputs.inputValue}
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
</li>
))
This way we'll have id and name available in event but deep state structure forces us to write sth like this:
handleChangeInp = e => {
const id = e.target.id;
console.log(`the id is ${id}`);
const index = Number(e.target.id);
// deep state update
const newInputArr = this.state.inputData.inputArr.map((ing, i) => {
console.log(i, e.target.name);
return (i == index) ? e.target.value : ing;
});
const newInputData = {...this.state.inputData, inputArr: newInputArr}
this.setState({ inputData: newInputData }, ()=>{console.log(this.state.inputData)})
}
It 'works' ... but this method of state update has 2 issues/drawbacks:
values are stored in array by index - we have only index-value pairs;
the second issue is more serious - it stores only first value! For map usage ('searching') we should have array initialized for each index - otherwise it doesn't store values.
My advise - use object (map) to store values:
this.state = {
inputData: {
id: '',
inputArr: {}
}
};
and handler like this:
handleChangeInp = e => {
const {name, value} = e.target;
console.log(name, value);
// deep state update
const newInputArr = {...this.state.inputData.inputArr,[name]:value};
const newInputData = {...this.state.inputData, inputArr: newInputArr}
this.setState({ inputData: newInputData }, ()=>{console.log(this.state.inputData)})
}
This way we have id-value pairs and only for 'touched' fields.
Working example.
Of course value for inputs can be read from state (if defined):
value={this.state.inputData.inputArr[inputs.id]
? this.state.inputData.inputArr[inputs.id] : ''}
// or
value={this.state.inputData.inputArr[inputs.id]===undefined ? ''
: this.state.inputData.inputArr[inputs.id] }
// f.e. when we're using type conversions
// and numerical value `0` could be used as valid input