React Hook form with Dynamic key names creates huge array instead - reactjs

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.

Related

Material UI's Autocomplete does not show the selected value in React using react-hook-form

I am having issues making Material UI's Autocomplete show the selected item using react-hook-form with options that are objects containing name and id. When I select an item, the box is just empty.
I have a FieldArray of custom MaterialBars which contain a material:
<Controller
control={control}
name={`materialBars.${index}.materialId`}
render={(
{ field: { value, onChange } }
) => (
<Autocomplete
options={materials.map(material => ({id: material.id, name: material.name})} // materials are fetched from the API
getOptionLabel={(option) => option.name}
value={materialItems.find((item) => `${item.id}` === value) || null}
onChange={(_, val) => onChange(val?.id)}
renderInput={(params) => <TextField {...params} label="Material" />}
/>
)}
/>
There is a working example in the codesandbox.
When I select a material in the list, the box is cleared, and the placeholder text is shown instead of the selected material (when focus is removed from the box). The item is selected under the hood, because in my application, when I press submit, the newly selected material is saved to the backend.
I cannot figure out if the issue lies in the react-hook-form part, material UI or me trying to connect the two. I guess it will be easier if the options are just an array of strings with the name of the material (when the form schema has just the materialId), but it is nice to keep track of the id, for when contacting the API.
You should set the same type on materialId property between FormValue and ListData.
For Example, if I use number type, it should be
https://codesandbox.io/s/autocomplete-forked-mpivv1?file=/src/MaterialBar.tsx
// App.tsx
const { control, reset } = useForm<FormValues>({
defaultValues: {
materialBars: [
// use number instead of string
{ ..., materialId: 1 },
{ ..., materialId: 6 }
]
}
});
// util.ts
export const materials = [
{
id: 1, // keep it as number type
...
},
...
];
// util.ts
export type FormValues = {
materialBars: {
// change it from string type to number
materialId: number;
...
}[];
};
// MaterialBar.tsx
<Controller
...
) => (
<Autocomplete
...
/*
Remove curly brackets
- change`${item.id}` to item.id
*/
value={materialItems.find((item) => item.id === value) || null}
/>
)}
/>

How does a parent know about the state of a list of child forms using formik?

I have a form consisting of a list of building materials which can be materials already registered for the construction and new materials:
The MaterialBar:
function MaterialBar({
materialBarId,
material,
onDelete,
onChange,
}: MaterialBarProps) {
const { values, handleChange } = useFormik<MaterialBar>({
initialValues: material,
onSubmit: console.log,
enableReinitialize: true,
});
const updateField = (event) => {
handleChange(event);
onChange(materialBarId, values);
};
return (
<DropdownWrapper>
<Dropdown
label="Material"
items={/* A list of available materials */}
selectedItem={values.material}
onSelect={updateField}
/>
.... Other fields
<TextField
name="amount"
id="material-amount"
label="Amount"
type="number"
onChange={updateField}
/>
<DeleteButton onClick={() => onDelete(materialBarId)}>
<img src={remove} />
</DeleteButton>
</DropdownWrapper>
);
}
The parent (ConstructionMaterials):
function ConstructionMaterials({
project,
materials,
attachMaterial,
}: ProjectMaterialsProps) {
const [allMaterials, setAllMaterials] = useState<IProjectMaterial[]>(
getProjectMaterialsFrom(project.materials)
);
const saveAllMaterials = () => {
allMaterials.forEach((newMaterial) => {
if (newMaterial.isNew) {
attachMaterial(newMaterial);
}
});
};
const updateNewMaterial = (
materialBarId: number,
updatedMaterial: MaterialBar
): void => {
const updatedList: IProjectMaterial[] = allMaterials.map((material) => {
if (material.projectMaterialId === materialBarId) {
return {
...material,
materialId: materials.find(
(currentMaterial) =>
currentMaterial.name === updatedMaterial.material
)?.id,
type: updatedMaterial.type,
length: updatedMaterial.length,
width: updatedMaterial.width,
value: updatedMaterial.amount,
};
}
return material;
});
setAllMaterials(updatedList);
};
// Adds a new empty material to the list
const addMaterial = (): void => {
setAllMaterials([
...allMaterials,
{
projectMaterialId: calculateMaterialBarId(),
projectId: project.id,
isNew: true,
},
]);
};
return (
<>
{allMaterials.map((material) => {
const materialBar: MaterialBar = {
material: material.name || "",
type: material.type || "",
amount: material.value || 0,
length: material.length || 0,
width: material.width || 0,
};
return (
<AddMaterialBar
key={material.projectMaterialId}
materialBarId={material.projectMaterialId!}
materials={materials}
onDelete={removeMaterial}
onChange={updateNewMaterial}
/>
);
})}
<Button onClick={() => saveAllMaterials()}>
{texts.BUTTON_SAVE_MATERIALS}
</Button>
</>
);
}
I have a hard time figuring out how to manage the list of materials. I use Formik (the useFormik hook) in the MaterialBar to take care of the values of each field.
My challenge is how to keep all the data clean and easily pass it between the components while knowing which materials are new and which already exist. If I just use Formik in the MaterialBar, then ConstructionMaterials does not know about the changes made in each field and it needs the updated data because it calls the backend with a "save all" action (the "Save"-buttons in the image should not be there, but they are my temporary fix).
To circumvent this, I also keep track of each material in ConstructionMaterials with the onChange on MaterialBar, but that seems redundant, since this is what Formik should take care of. I have also added a isNew field to the material type to keep track of whether it is new, so I don't two lists for existing and new materials.
I have had a look at FieldArray in ConstructionMaterials, but shouldn't Formik be used in the child, since the parent should just grab the data and send it to the API?
So: is there a clever way to handle a list of items where the parent can know about the changes made in the childs form, to make a bulk create request without the parent having to also keep track of all the objects in the children?
Sorry about the long post, but I don't know how to make it shorter without loosing the context.

Populating a Material-UI dropdown with Redux store data

I am trying to get my Redux store fields to automatically populate a method I have imported. Am I going about this the right way in order to get this done? Do I need to create a mapping options for each field?
I have each of my dropdowns inserted with a PopulateDropdown list and the fields in each of them but I need them split as per the id and text.
Am I accessing my redux store correctly below? I have the array declared on up my function component by using const fields = useSelector(state => state.fields);
Update
I have the method inserted into where the dropdowns should be however I don't think I am accessing the data correctly which is causing the problem. The fields array has been de-structured into the six different fields for each dropdown and different mappingOptions have been created for each one.
What do I need to do to get the data into the method? the examples I have seen have static arrays declared on the component rather than use the Redux store.
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList, currentList, regionList, diveTypeList, visibilityList, diveSpotList } = fields;
populateDropdown method that I have imported
export const PopulateDropdown = ({ dataList = [], mappingOptions, name, label }) => {
const { title, value } = mappingOptions;
return (
<FormControl style={{ width: 200 }} >
<InputLabel id={label}>{label}</InputLabel>
<Select labelId={label} name={name} >
{dataList.map((item) => (
<MenuItem value={item[value]}>{item[title]}</MenuItem>
))}
</Select>
</FormControl>
);
};
imported dropdown menu
<PopulateDropdown
dataList={diveType}
mappingOptions={mappingOptions}
name="fieldName"
label="Select dive type"
value={dive.typeID}
onChange={handleChange}/>
Update
I have updated my action, reducer and populateFields method however I am still having trouble mapping the redux data to my two property fields. In the Redux tree the fields should be under the fields.data.fieldlists as they print when I console log them.
What way should I be populating them into the titleProperty etc? It is currently looking like it might be populating but a large box drops downs that I can't see any values inside.
// select user object from redux
const user = useSelector(state => state.user);
// get the object with all the fields
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList = [],
currentList = [],
regionList = [],
diveTypeList = [],
visibilityList = [],
diveSpotList = [],
marineTypeList = [],
articleTypeList = []
} = fields;
.........
<PopulateDropdown
dataList={fields.data.diveTypeList} // the options array
titleProperty={fields.data.diveTypeList.diveTypeID} // option label property
valueProperty={fields.data.diveTypeList.diveType} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange(setDive.typeID)} // update state on change
/>
Your PopulateDropdown component looks correct except that we need it to use the value and onChange that we passed down as props.
My personal preference would be to use separate properties valueProperty and titleProperty instead of passing a single mappingOptions. That way you don't need to create objects for every dropdown, you just set the two properties in your JSX. You could get rid of this part entirely if you normalized your data such that the elements of every list have the same properties id and label.
<PopulateDropdown
dataList={diveTypeList} // the options array
titleProperty={"diveTypeId"} // option label property
valueProperty={"diveType"} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange("typeId")} // update state on change
/>
export const PopulateDropdown = ({
dataList = [],
valueProperty,
titleProperty,
label,
...rest // can just pass through all other props to the Select
}: Props) => {
return (
<FormControl style={{ width: 200 }}>
<InputLabel id={label}>{label}</InputLabel>
<Select {...rest} labelId={label}>
{dataList.map((item) => (
<MenuItem value={item[valueProperty]}>{item[titleProperty]}</MenuItem>
))}
</Select>
</FormControl>
);
};
It looks like the ids in currentId are actually a number, so at some point in your code you will want to convert that with parseInt because e.target.value is always a string, though maybe the backend can handle that.
Loading the API Data
It looks like you figured out how to fetch all of the fields in one API call which is great. You are saving it to a property fields on the fields reducer which creates the structure state.fields.fields. Since you are replacing the whole state, you can just return the whole thing as the entire slice state.
You can initialize your state object with empty arrays, or you can use an empty object {} as your initial state and fallback to an empty array when you destructure the arrays off of it, like const {diveSchoolList = [], currentList = []} = fields.
export const requireFieldData = createAsyncThunk(
"fields/requireData", // action name
// don't need any argument because we are now fetching all fields
async () => {
const response = await diveLogFields();
return response.data;
},
// only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
{
// _ denotes variables that aren't used - the first argument is the args of the action creator
condition: (_, { getState }) => {
const { fields } = getState(); // returns redux state
// check if there is already data by looking at the didLoadData property
if (fields.didLoadData) {
// return false to cancel execution
return false;
}
}
}
);
const fieldsSlice = createSlice({
name: "fields",
initialState: {
currentList: [],
regionList: [],
diveTypeList: [],
visibilityList: [],
diveSpotList: [],
diveSchoolList: [],
marineTypeList: [],
articleTypeList: [],
didLoadData: false,
},
reducers: {},
extraReducers: {
// picks up the pending action from the thunk
[requireFieldData.pending.type]: (state) => {
// set didLoadData to prevent unnecessary re-fetching
state.didLoadData = true;
},
// picks up the success action from the thunk
[requireFieldData.fulfilled.type]: (state, action) => {
// want to replace all lists, there are multiple ways to do this
// I am returning a new state which overrides any properties
return {
...state,
...action.payload
}
}
}
});
So in the component we now only need to call one action instead of looping through the fields.
useEffect(() => {
dispatch(requireFieldData());
}, []);

Single onChange that can handle single- and multi-select Form changes

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)

failed to get some values from dynamic multiple input fields using reactjs

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.

Resources