I am using Formik's arrayHelpers to push and remove objects from an array of objects. The push works as expected and adds the object and its values to the array. The remove works when you only select one item in the array and then deselect it. The problem, however starts when you select multiple items. For instance, you select the first item and it is added to the array and then select the second item and it is added to the array, but say you want to deselect that second item and you click on it, the first item is deselected. Then if you click on it again, it deselects the second item.
initialValue: lenders: []
/*mock array of lenders*/
const lenders = [
{
id: 1,
text: 'Lender1',
value: 'Lender1',
},
{
id: 2,
text: 'Lender2',
value: 'Lender2',
},
{
id: 3,
text: 'Lender3',
value: 'Lender3',
},
]
<FieldArray
name="lenders"
render={arrayHelpers => (
<div className="lenders">
{lenders.map(lender => (
<div key={lender.id}>
<Field name={`lender${lender.id}`}>
{({ field, meta }) => (
<SelectableCard
id={field.name}
name={field.name}
text={lender.value}
isSelected={props.values.lenders.some(
len => len.id === lender.id
)}
label={lender.value}
inputName={field.name}
value={lender.value}
ref={field.ref}
onClick={evt => {
let isSelected = props.values.lenders.some(
len => len.id === lender.id
);
isSelected
? arrayHelpers.remove(lender)
: arrayHelpers.push({
id: lender.id,
value: lender.value,
});
field.onChange({
target: {
id: lender.id,
value: !isSelected,
},
});
}}
/>
)}
</Field>
</div>
))}
</div>
)}
This was solved by changing the isSelected const in the onClick to finding the index like this:
const index = props.values.lenders.findIndex(
len => len.id === lender.id
);
Then wrapping the arrayHelpers to check if the index is greater than -1:
index > -1
? arrayHelpers.remove(index)
: arrayHelpers.push({
id: lender.id,
value: lender.value,
});
FieldArray's arrayHelpers render props takes an index of the item to be removed and not the object to be removed
Pass on the index that you obtain from map
<FieldArray
name="lenders"
render={arrayHelpers => (
<div className="lenders">
{lenders.map((lender, index) => (
<div key={lender.id}>
<Field name={`lender${lender.id}`}>
{({ field, meta }) => (
<SelectableCard
id={field.name}
name={field.name}
text={lender.value}
isSelected={props.values.lenders.some(
len => len.id === lender.id
)}
label={lender.value}
inputName={field.name}
value={lender.value}
ref={field.ref}
onClick={evt => {
let isSelected = props.values.lenders.some(
len => len.id === lender.id
);
isSelected
? arrayHelpers.remove(index)
: arrayHelpers.push({
id: lender.id,
value: lender.value,
});
field.onChange({
target: {
id: lender.id,
value: !isSelected,
},
});
}}
/>
)}
</Field>
</div>
))}
</div>
)}
As mentioned above you should remove the index of the item/object not the object itself.
I encountered this while integrating Formik with ChipInput and solved as follows:
<FieldArray
name="topics"
render={arrayHelpers => (
<Field
name="topics"
component={ChipInput}
type="text"
label="Topics"
value={values.topics}
margin="none"
onAdd={chip => arrayHelpers.push(chip)}
onDelete={chip =>
arrayHelpers.remove(values.topics.indexOf(chip))
}
fullWidth
chipRenderer={chipRenderer}
helperText={errors.topics ? errors.topics : ''}
error={errors.topics ? true : false}
/>
Related
thanks for reading this.
I am trying to build a form using Formik. And it includes a FieldArray inside a FieldArray.
For some reason, setFieldValue works as I can console.log the correct e.target.name and e.target.value
The problem is upon submitting the form, all the values from the inputs are not where they supposed to be. The expected behavior is the values should be inside of exclusionGroups, not outside.
Would anyone be able to give me some insight ? I've been stuck on this the whole day and feels like my head is going to explode.
Expected:
exclusionGroups: [
{
exclusion: [
{
param: 'delayMin',
operator: '<',
value: 'test1',
},
{
param: 'airlineCode',
operator: '>',
value: 'test2',
},
],
},
{
exclusion: [
{
param: 'flightNumber',
operator: '=',
value: 'test3',
},
],
},
],
Reality:
exclusionGroups: (2) [{…}, {…}]
group-1-operator-1: "<"
group-1-operator-2: ">"
group-1-param-1: "delayMin"
group-1-param-2: "airlineCode"
group-1-value-1: "test1"
group-1-value-2: "test2"
group-2-operator-1: "="
group-2-param-1: "flightNumber"
group-2-value-1: "test3"
My Code:
Index.tsx
type ExclusionRuleValues = {
param: string;
operator: string;
value: string;
};
interface ExclusionGroupProps {
exclusionRules?: ExclusionRuleValues[];
}
const Exclusion = ({ data, type }: any) => {
const onSubmit = (values: any) => {
console.log(values);
};
const initialValues = {
exclusionGroups: [
{
exclusion: [
{
param: 'group1',
operator: 'group1',
value: 'group1',
},
],
},
{
exclusion: [
{
param: 'group2',
operator: 'group2',
value: 'group2',
},
],
},
],
};
const emptyGroup = {
exclusion: [
{
param: '',
operator: '',
value: '',
},
],
};
return (
<React.Fragment>
<Formik
enableReinitialize
onSubmit={onSubmit}
initialValues={initialValues}
render={(formProps) => {
const { values, handleSubmit, submitForm } = formProps;
const { exclusionGroups } = values;
const setFieldValue = (e: ChangeEvent<HTMLInputElement>) => {
return formProps.setFieldValue(e.target.name, e.target.value);
};
return (
<React.Fragment>
<Header>
Model will return excluded message code if the following condition is true.
<Button onClick={submitForm} color="primary">
Save Changes
</Button>
</Header>
<Form onSubmit={handleSubmit}>
<FieldArray
name="exclusionGroups"
render={(arrayHelper) => (
<React.Fragment>
{exclusionGroups.map((exclusionRulesGroup: any, index: number) => {
return (
<TargetFields
type={type}
group={index + 1}
key={`field-${index}`}
name={`${arrayHelper.name}.${index}`}
setFieldValue={setFieldValue}
/>
);
})}
<AddNewGroupButton type="button" onClick={() => arrayHelper.push(emptyGroup)}>
+ New Group
</AddNewGroupButton>
</React.Fragment>
)}
/>
</Form>
</React.Fragment>
);
}}
/>{' '}
</React.Fragment>
);
};
export default Exclusion;
TargetFields.tsx
interface TargetFieldsProps {
group: number;
name: string;
type: string;
data?: ExclusionRuleValues[];
setFieldValue: (e: ChangeEvent<HTMLInputElement>) => void;
}
const TargetFields = ({ group, data, name, type, setFieldValue }: TargetFieldsProps) => {
const emptyTarget = {
param: '',
operator: '',
value: '',
};
return (
<React.Fragment>
<Field name={name}>
{(fieldProps: any) => (
<React.Fragment>
<ExclusionGroupHeader>
<b>Group {group}</b>
</ExclusionGroupHeader>
<Wrapper>
<FieldArray
name={`${fieldProps.field.name}.exclusion`}
key={`exclusion-${group}`}
render={(targetHelper) => (
<React.Fragment>
{fieldProps.field.value.exclusion.map(
(target: ExclusionRuleValues, key: number) => {
const { param, operator, value } = target;
return (
<ExclusionRuleGroup key={`group-${key}`}>
<ExclusionRuleHeader>
<b>Target {key + 1}</b>
<DeleteButton type="button" onClick={() => targetHelper.remove(key)}>
remove
</DeleteButton>
</ExclusionRuleHeader>
<StyledRow>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={param}
label="Params"
name={`group-${group}-param-${key + 1}`}
options={
type === 'input' ? InputExclusionParams : OutputExclusionParams
}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={operator}
label="Operator"
name={`group-${group}-operator-${key + 1}`}
options={SelectOptions}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Input
onChange={setFieldValue}
// value={value}
label="Value"
name={`group-${group}-value-${key + 1}`}
type="text"
placeholder="Value"
/>
</CCol>
</StyledRow>
</ExclusionRuleGroup>
);
}
)}
<AddNewRuleButton type="button" onClick={() => targetHelper.push(emptyTarget)}>
+ New Rule
</AddNewRuleButton>
</React.Fragment>
)}
/>
</Wrapper>
</React.Fragment>
)}
</Field>
</React.Fragment>
);
};
export default TargetFields;
The problem is when you pass the name property to your form's inputs.
In the component TargetFields you are passing the name like name={`group-${group}-operator-${key + 1}`} so formik think you want to store that value in the property that will result from that string, e.g. group-1-operator-1 and that is why it's going of the object you want, but going in a property with that name.
If you want to use nested objects / arrays, you need to concatenate the name with object with want using . or [${index}], just like you did in here
<TargetFields
type={type}
group={index + 1}
key={`field-${index}`}
name={`${arrayHelper.name}.${index}`} // You use the . with the name and the index.
setFieldValue={setFieldValue} // It could also be using [] instead of . like this => `${arrayHelper.name}[${index}]`
/>
and like here
<FieldArray
name={`${fieldProps.field.name}.exclusion`} // You use the . with the name and the property of the object you want
key={`exclusion-${group}`}
render={(targetHelper) => ( ... )
/>
So to solve you problem, you need to change the following.
<StyledRow>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={param}
label="Params"
name={`${targetHelper}[${key}].param`}
options={
type === 'input' ? InputExclusionParams : OutputExclusionParams
}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Select
onChange={setFieldValue}
// value={operator}
label="Operator"
name={`${targetHelper}[${key}].operator`}
options={SelectOptions}
placeholder="Operator"
/>
</CCol>
<CCol sm="4">
<Input
onChange={setFieldValue}
// value={value}
label="Value"
name={`${targetHelper}[${key}].value`}
type="text"
placeholder="Value"
/>
</CCol>
</StyledRow>
And just a guess, you didn't code all that by your self right? Because in one place you are doing exactly what you need to do to solve your problem. If you don't know how that name thing works with FieldArray, I recommend you reading this part of the formik docs. Know how this works is very important for using nested objects / arrays.
I am having the hardest time figuring out how to determine if the checkbox is checked using Formik and React. So far i am able to return the value of the checked box but can't seem to return true or false.
I am trying to add a class to the checkbox if it has been checked, which I can do if it has a value but when unchecked, it doesn't remove the class. I think i may be approaching this incorrectly...
Any help is much appreciated. Thank you
Initial Values:
const initialValues = {
// step two
processorOptions: [],
};
processorOptions:
const processorOptions = [
{ type: 'cc', value: 'test1', name: 'Test 1' },
{ type: 'cc', value: 'test2', name: 'Test 2' },
{ type: 'cc', value: 'test3', name: 'Test 3' },
];
Custom Field Component:
const Processors = () => {
const { values } = useFormikContext();
return (
<>
<FieldArray
name='processorOptions'
render={(arrayHelpers) => (
<>
{processorOptions.map((processorOption, index) => (
<Col lg={4} key={index}>
<div className='form-group'>
<div className='form-check'>
<label className={`onboard__processor-label ${processorOption.checked ? 'is-checked' : ''}`}>
<input
name='processorOptions'
type='checkbox'
value={processorOption.id}
checked={values.processorOptions.includes(processorOption.id)}
onChange={(e) => {
if (e.target.checked) {
arrayHelpers.push(processorOption.id);
} else {
const idx = values.processorOptions.indexOf(processorOption.id);
arrayHelpers.remove(idx);
}
}}
/>
{processorOption.name}
</label>
</div>
</div>
</Col>
))}
</>
)}
/>
<pre>{JSON.stringify(values.processorOptions, null, 2)}</pre>
</>
);
};
I am rendering a customized react-querybuilder. Whenever I add a rule the input box is rendered with default empty value. The problem is that when I enter one character in the Input box it loses focus.
This does seem like a duplicate question. But, after trying out the solutions mentioned below -
Storing value in state.
autoFocus on input tag (this is messed it up even further!)
I am not able to figure it out.
I have added the code to stackblitz
Please find the relevant code:
const [queryOutput, setQueryOutput] = useState("");
...
<QueryBuilder
{...props}
controlElements={{
combinatorSelector: props => {
let customProps = {
...props,
value: props.rules.find(x => x.combinator) ? "or" : props.value
};
return (
<div className="combinator-wrapper">
<button className="form-control-sm btn btn-light mt-2">
{customProps.value.toUpperCase()}
</button>
</div>
);
},
addRuleAction: props => {
return (
<button
className={props.className}
title={props.title}
onClick={e => {
return props.handleOnClick(e);
}}
>
+ Add New Rule
</button>
);
},
addGroupAction: props => {
return (
<button
className={props.className}
title={props.title}
onClick={e => {
return props.handleOnClick(e);
}}
>
{props.label}
</button>
);
},
valueEditor: ({
className,
field,
operator,
inputType,
value,
handleOnChange,
level
}) => {
if (field === "enabled") {
return (
<input
className={className}
type="checkbox"
checked={value !== "" ? value : false}
onChange={e => handleOnChange(e.target.checked)}
/>
);
}
return (
<input
className={className}
value={value}
onChange={e => handleOnChange(e.target.value)}
/>
);
}
}}
onQueryChange={query => {
let customQuery = { ...query, combinator: "or" };
return setQueryOutput(
formatQuery(customQuery ? customQuery : query, "sql")
);
}}
/>
Needed to assign a reference of the valueEditor component rather than defining it inline(so that it does not create a new instance on every render).
Updated the relevant code:
const valueEditor = ({
className,
field,
operator,
inputType,
value,
handleOnChange,
level
}) => (
<input
className={className}
value={value}
onChange={e => handleOnChange(e.target.value)}
/>
);
.....
<QueryBuilder
{...props}
controlElements={{
...
valueEditor
...
}}
/>
I am trying to create a multi-select form control, however, whenever I select something it does not get rendered. The function handleChange does get the event.target.value but it does not seem to add to the roleIds state. Furthermore, the console.log for the variable selected does not log anything to console.
Component Code:
const allRoleIds = [
"12345678",
"98765423",
"56465735683578",
];
const [roleIds, setRoleIds] = React.useState([]);
function handleChange(event) {
setRoleIds(event.target.value);
}
const [cassowaries, setCassowaries] = React.useState({
columns: [
{ title: "Cassowary Name", field: "name" },
{
title: "Cassowary Roles",
field: "roles",
render: (rowData) => {
return (
<li>
{rowData.roles.map((role) => (
<Chip label={role} className={classes.chip} />
))}
</li>
);
},
editComponent: (props) => (
<FormControl className={classes.formControl}>
<InputLabel>Roles</InputLabel>
<Select
multiple
value={roleIds}
onChange={handleChange}
input={<Input id="select-multiple-chip" />}
renderValue={(selected) => {
console.log(selected);
return (
<div className={classes.chips}>
{selected.map((value) => (
<Chip
key={value}
label={value}
className={classes.chip}
/>
))}
</div>
);
}}
// MenuProps={MenuProps}
>
{allRoleIds.map((id) => (
<MenuItem key={id} value={id}>
{id}
</MenuItem>
))}
</Select>
</FormControl>
),
},
{ title: "Penguin", field: "penguin" },
],
data: [{ name: "Mehmet", roles: roleIds, penguin: true }],
});
You are doing concat in your handleChange. Material ui already gives you the array of selected values to you. Sp fix your handleChange everything should be fine.
Like this
function handleChange(event) {
setRoleIds(event.target.value);
}
Working demo is here
EDIT:
Based on additional req (see comments):
If a custom multi select component needs to be used inside materail table editcomponent, then the state of select should not be managed outside but state & onChagne needs to managed inside editComponent using the prop it provides.
See working demo here
Code snippet - material ui table
...
columns: [
{ title: "Tag Name", field: "name" },
{
title: "Tag Roles",
field: "roles",
render: rowData => {
return (
<li>
{rowData.roles.map(role => (
<Chip label={role} className={classes.chip} />
))}
</li>
);
},
editComponent: props => {
console.log("props", props);
return (
<FormControl className={classes.formControl}>
<InputLabel>Roles</InputLabel>
<Select
multiple
value={props.value}
onChange={e => props.onChange(e.target.value)}
input={<Input id="select-multiple-chip" />}
renderValue={renderChip}
>
{allRoleIds.map(id => (
<MenuItem key={id} value={id}>
{id}
</MenuItem>
))}
</Select>
</FormControl>
);
}
},
{ title: "Penguin", field: "penguin" }
],
data: [{ name: "Mehmet", roles: [], penguin: true }]
...
I'm using react-material-table.
I'm trying to validate the row edit.
Everything works fine for "native" material column.
But I had to use a custom editComponent for one column rendering an Autocomplete.
{
title: "test",
field: "foo",
editComponent: (props) => {
return (
<div>
<Autocomplete
onChange={(e, v) => this.handleFooChange(props.rowData.id, v)}
//onChange={(e,v) => props.onChange(v)}
id="people-company"
options={[{ id: 1, name: "bar" }, { id: 2, name: "foo" }]}
getOptionLabel={option => option.name}
value={props.rowData.foo}
filterSelectedOptions
renderInput={params => (
<TextField
{...params}
variant="outlined"
label="company"
placeholder="company"
margin="normal"
fullWidth
/>
)}
/>
</div>
);
},
My MaterialTable define EditRow and EditField
const FormikMTInput = props => {
return (
<Field name={props.columnDef.field}>
{({ field, form, meta }) => {
const { name } = field;
const { errors, setFieldValue } = form;
//console.log("ShowError", errors);
const showError = !!getIn(errors, name);
return (
<div>
<MTableEditField
{...props}
{...field}
error={showError}
onChange={newValue => setFieldValue(name, newValue)}
/>
{errors[field.name] && (
<div style={{ color: "#f44336" }}>{errors[field.name]}</div>
)}
</div>
);
}}
</Field>
);
};
const MuiTableEditRow = ({ onEditingApproved, ...props }) => {
return(
<Formik
validate={values => {
const errors = {};
if (!values.name) {
errors.name = "Required";
}
if (!values.foo) {
errors.foo = "Required";
}
return errors;
}}
initialValues={props.data}
onSubmit={values => {
if (values) {
if (values.name.length > 2 && values.foo)
onEditingApproved(props.mode, values, props.data);
}
}}
render={({ submitForm }) => (
<MTableEditRow {...props} onEditingApproved={submitForm} />
)}
/>
)};
<MaterialTable
title="Editable Preview"
columns={this.state.columns}
data={this.state.data}
components={{
EditRow: MuiTableEditRow,
EditField: FormikMTInput
}}
/>
FormikMTInput is well fired on "default" columns. It is not the case for the overwrited editComponent. How can I pass errors to this column?
Here the sandbox
The problem is, that the current value differs from the rowData value.
So change value={props.rowData.foo} to value={props.value} in the autocomplete, to get the correct value. Previously, the rowData never changed on edit because the row still had the data until you save it.
Additionally, you should pass the already transformed value to the onChange function and not the event like this: onChange={v => props.onChange(v.target.value)}.
if (!id || id === undefined) return; is equal to if (!id) return;because !id returns true for undefined.
Last but not least, you are mutating your state data in
var myData = this.state.data;
myData.forEach(d => {
if (d.id === id) {
d.foo = v;
}
});
which is a no-go. Change it to:
handleFooChange(id, v) {
if (!id) return;
const myData = this.state.data.map(d => d.id === id ? {...d, foo: v} : d);
this.setState({ data: myData });
}