Redux Form using Material Ui with nested MenuItem not working - reactjs

I am new to React, Redux Form and Material. I would like to create a nested drop down selector component that can be dropped in a Redux Form similar to this:
Here is the renderSelectField used to create the select component.
const renderSelectField = ({
input,
label,
meta: { touched, error },
children,
...custom
}) => (
<SelectField
floatingLabelText={label}
errorText={touched && error}
{...input}
onChange={(event, index, value) => input.onChange(value)}
children={children}
{...custom}
/>
);
const MaterialUiForm = (props) => {
const { handleSubmit, pristine, reset, submitting, classes } = props;
return (
<form onSubmit={handleSubmit}>
<div>
<Field
name="favoriteColor"
component={renderSelectField}
label="Favorite Color"
>
<MenuItem value="ff0000" primaryText="Red" />
<MenuItem value="00ff00" primaryText="Green" />
<MenuItem value="0000ff" primaryText="Blue" />
</Field>
</div>
<div>
<Field
id='chapter'
name='chapter'
component={SelectMenu}
label='Chapter'
/>
</div>
<div>
<button type="submit" disabled={pristine || submitting}>
Submit
</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>
Clear Values
</button>
</div>
</form>
);
};
My SelectMenu component is
const chapterFormValues = [
{
key: "international-4",
caption: "France"
},
{
key: "international-5",
caption: "Africa"
},
{
key: "international-6",
caption: "United Kingdom"
},
{
key: "usa-key",
caption: "North America",
subMenuItems: [
{
key: "usaChapter-1",
caption: "Central"
},
{
key: "usaChapter-2",
caption: "East"
}
]
}
];
const SelectMenu = (props) => {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((open) => !open);
};
const handleSubMenuClose = () => {
setOpen((open) => !open);
};
const { label, classes } = props;
const renderMenuItems = () => {
return (
chapterFormValues !== undefined &&
chapterFormValues.map((option) => {
if (option.hasOwnProperty("subMenuItems")) {
return (
<React.Fragment>
<MenuItem onClick={handleClick} className={classes.menuItem}>
{option.caption}
{open ? <IconExpandLess /> : <IconExpandMore />}
</MenuItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<hr />
{option.subMenuItems.map((item) => (
<MenuItem
key={item.key}
className={classes.subMenuItem}
onClick={handleSubMenuClose}
>
{item.caption}
</MenuItem>
))}
</Collapse>
</React.Fragment>
);
}
return (
<MenuItem
className={classes.menuItem}
key={option.key}
value={option.caption === "None" ? "" : option.caption}
>
{option.caption}
</MenuItem>
);
})
);
};
return (
<FormControl>
<InputLabel>{label}</InputLabel>
<MuiSelect
input={<Input id={`${props.id}-select`} />}
value={props.value}
{...props.input}
{...props.custom}
>
{renderMenuItems()}
</MuiSelect>
</FormControl>
);
};
Here is a link to the code sandbox I created. Material UI ReduxForm Select
It works except the nested drop down does not update the selector field. I have researched this and found this issue Stackoverflow redux form with nested lists but no solution.
Can anyone give me advice as to what I am missing? I believe I need to pass the event in the handleSubMenuClose function back to the Redux Form somehow but am stumped as to how to do this.

Well using Material UI MenuItem didn't work but I was able to use redux form and create a nested drop down that did.
This is a screen shot of what I created. It did not have the functionality to open/close a panel but it still gave the user a sense of a nested dropdown.
Here is the code that I changed in the SelectMenu method. The key was to use the native form of the Material UI Select component and the optgroup element.
const SelectMenu = (props) => {
const { label, classes } = props;
const renderMenuItems = () => {
return (
chapterFormValues !== undefined &&
chapterFormValues.map((option) => {
if (option.hasOwnProperty("subMenuItems")) {
return (
<React.Fragment>
<optgroup label={option.caption} className={classes.menuItem}>
{option.subMenuItems.map((item) => (
<option
key={item.key}
className={classes.subMenuItem}
>
{item.caption}
</option>
))}
</optgroup>
</React.Fragment>
);
}
return (
<option
className={classes.menuItem}
key={option.key}
value={option.caption === "None" ? "" : option.caption}
>
{option.caption === "None" ? "" : option.caption}
</option>
);
})
);
};
return (
<FormControl>
<InputLabel>{label}</InputLabel>
<Select
native
input={<Input id={`${props.id}-select`} />}
value={props.value}
{...props.input}
{...props.custom}
>
{renderMenuItems()}
</Select>
</FormControl>
);
};
Helpfully links were :
HTML / CSS: Nested <options> in a <select> field?
Redux Form Material UI: Select with Nested Lists not working

Related

setFieldValue in Formik not working correctly on cancel

I have two components. My Formik initial values are in the MyDetails component. I have a form in NewDetailsForm. I am trying to set a default value to material UI select (identityType field in NewDetailsForm). I am setting an initial value in the first form and getting it via values using useFormikContext in the second. When I click the next button, I have another component where all the values appear. For example, I have 2 identity types - passport(default) and ID. When I select ID, it appears on the next screen which means that setFieldValue is working. The problem is that when I click previous or cancel, the other fields remain filled, but the identityType is being set to the default.
MyDetails:
const validateSchema = yup.object({
identityType: yup.string().required("This field is required"),
});
const PersonalDetails: React.FC<IProps> = ({
setValues,
formikValues,
progress,
regress,
formRef,
}: IProps): JSX.Element => {
return (
<Formik<NewClientPersonalDetailsProps>
validationSchema={validateSchema}
initialValues={{
...formikValues.personalDetails,
//ID Type default value id
identityType: "Passport",
}}
innerRef={formRef}
onSubmit={(values) => {
setValues({
...formikValues,
personalDetails: {
...values,
},
});
values.submitDirection === "progress" ? progress() : regress();
}}
>
{({ values }) => {
return (
<div >
<NewClientDetailsForm
identityTypes={removeOtherType(lookupData.identityTypes)}
/>
</div>
);
}}
</Formik>
);
};
interface IProps {
formikValues: NewClientInputProps;
setValues(values: NewClientInputProps): void;
progress(): void;
regress(): void;
formRef: React.RefObject<FormikProps<NewClientPersonalDetailsProps>>;
}
interface ExportProps {
formikValues: NewClientInputProps;
setValues(values: NewClientInputProps): void;
progress(): void;
regress(): void;
formRef: React.RefObject<FormikProps<NewClientPersonalDetailsProps>>;
}
function MyDetails({
setValues,
formikValues,
regress,
progress,
formRef,
}: ExportProps) {
return (
<Suspense fallback={<EnhancedCircularProgress />}>
<PersonalDetails
formikValues={formikValues}
setValues={setValues}
regress={regress}
progress={progress}
formRef={formRef}
/>
</Suspense>
);
}
export default MyDetails;
NewDetailsForm:
const NewDetailsForm: React.FC<IProps> = ({
submitForm,
identityTypes,
genders,
}: IProps) => {
const { values, setFieldValue, touched, errors } = useFormikContext<NewClientInputDTO>();
const getOptions = (myArr: ILookup[]) => {
if (myArr) {
return myArr.map((option: ILookup) => (
<MenuItem value={option.id} key={option.id}>
{option.description}
</MenuItem>
));
}
return null;
};
return (
<Form>
<Box>
<Box width="100%" paddingRight={2}>
<StyledSelectFormControl required fullWidth size="small">
<InputLabel id="identityType-select-label">ID Type</InputLabel>
<Select
label="ID Type *"
name="identityType"
value={values.identityType}
onChange={(event) => {
setFieldValue("identityType", event.target.value);
}}
error={
touched.identityType &&
Boolean(touched.identityType)
}
>
{getOptions(identityTypes)}
</Select>
<FormHelperText>{touched.identityType && errors.identityType}</FormHelperText>
</StyledSelectFormControl>
</Box>
</Box>
<Box className={classes.rowContainer}>
<FTextField
variant={"outlined"}
label="Gender"
fullWidth
select
field={{ name: "genderId" }}
size="small"
required
>
{genders &&
genders.map((option: ILookup) => (
<MenuItem value={option.id} key={option.id}>
{option.description}
</MenuItem>
))}
</FTextField>
</Box>
{isEditable && submitForm && (
<Box display={"flex"} justifyContent={"flex-end"}>
<Button
style={{ width: 176 }}
variant={"contained"}
size={"small"}
color={"primary"}
disableElevation
onClick={() => submitForm()}
>
Save
</Button>
</Box>
)}
</Box>
</Form>
);
};
interface IProps {
submitForm?: () => void;
identityTypes: Array<ILookup>;
genders: Array<ILookup>;
}
export default NewDetailsForm;

Is it possible to simple-react-code-editor as a Formik field component?

Trying to get this form field component to take a simple-react-code-editor:
not sure if I'm going about this the right way by trying to pass props form the useField hook, but it works for textfield tags, so thought the same method could apply to this as well. Although, I get the feeling the onValueChange callback is different from the onChange callback that this component doesn't have. Is there a way to add it somehow?
Editor Component:
const MyEditor = ({value, onChange}) => {
const highlight = (value) => {
return(
<Highlight {...defaultProps} theme={theme} code={value} language="sql">
{({ tokens, getLineProps, getTokenProps }) => (
<>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</>
)}
</Highlight>
)
};
return (
<Editor
value={value}
onValueChange={onChange}
highlight={highlight}
padding={'40px'}
style={styles.root}
/>
);
}
export default MyEditor;
Form with Field Component as MyEditor (tried using useField hook):
const FormQueryTextBox = ({...props}) => {
const [field] = useField(props);
return (
<MyEditor onChange={field.onChange} value={field.value}/>
)
}
const validationSchema = yup.object({
query_name: yup
.string()
.required()
.max(50)
});
const AddQueryForm = () => {
return (
<div>
<Formik
validateOnChange={true}
initialValues={{
query:""
}}
validationSchema={validationSchema}
onSubmit={(data, { setSubmitting }) => {
console.log(data);
}}
>
{() => (
<Form>
<div>
<Field
placeholder="query name"
name="query_name"
type="input"
as={TextField}
/>
</div>
<div>
<Field
name="query"
type="input"
as={FormQueryTextBox}
/>
</div>
</Form>
)}
</Formik>
</div>
)
}
components render without errors, but as I type the text doesn't appear.
I figured out that I just need to customize my onChange with the setter from the useFormikContext hook like this:
const FormQueryTextBox = ({...props}) => {
const [field] = useField(props);
const { setFieldValue } = useFormikContext();
return (
<MyEditor {...field} {...props} onChange={val => {
setFieldValue(field.name, val)
}}/>
)
}

input tag loses focus after one character in react-querybuilder

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
...
}}
/>

Material-UI Autocomplete onChange not updates value

I want to use onChange event on Autocomplete component to get current selected values.
The problem is that it does not working as expected, so when I click to check/uncheck value checkbox is still unchecked but in console i can see that new value was added
uncoment this part to make it works:
value={myTempVal}
onChange={(event, newValue) => {
setMyTempVal(newValue);
console.log(newValue);
}}
online demo:
https://codesandbox.io/embed/hardcore-snowflake-7chnc?fontsize=14&hidenavigation=1&theme=dark
code:
const [myTempVal, setMyTempVal] = React.useState([]);
<Autocomplete
open
multiple
value={myTempVal}
onChange={(event, newValue) => {
setMyTempVal(newValue);
console.log(newValue);
}}
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
renderOption={(option, { selected }) => {
return (
<>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.title}
</>
);
}}
options={option2}
// groupBy={option => option.groupName}
getOptionLabel={option => option.title}
renderInput={params => (
<div>
<div>
<SearchIcon />
</div>
<TextField
variant="outlined"
fullWidth
ref={params.InputProps.ref}
inputProps={params.inputProps}
/>
</div>
)}
/>
You need to get donors receivers and options variables out of the function. Those variables get re-created at each render, this means that their reference changes at each render, and as Autocomplete makes a reference equality check to decide if an option is selected he never finds the options selected.
const donors = [...new Set(data.map(row => row.donor))].map(row => {
return {
groupName: "Donors",
type: "donor",
title: row || "null"
};
});
const receivers = [...new Set(data.map(row => row.receiver))].map(row => {
return {
groupName: "Receivers",
type: "receiver",
title: row || "null"
};
});
const option2 = [...donors, ...receivers];
export const App = props => {
const [myTempVal, setMyTempVal] = React.useState([]);
return (
<Autocomplete
open
multiple
...
You can also add getOptionSelected to overwrite the reference check :
<Autocomplete
open
multiple
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
getOptionSelected={(option, value) => option.title === value.title}
renderOption={(option, { selected }) => {
return (
<>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.title}
</>
);
}}
options={option2}
// groupBy={option => option.groupName}
getOptionLabel={option => option.title}
renderInput={params => (
<div>
<div>
<SearchIcon />
</div>
<TextField
variant="outlined"
fullWidth
ref={params.InputProps.ref}
inputProps={params.inputProps}
/>
</div>
)}
/>
This can help:
Replace
checked={selected}
To
checked={myTempVal.filter(obj=>obj.title===option.title).length!==0}
The complete solution
import React from "react";
import "./styles.css";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import CheckBoxOutlineBlankIcon from "#material-ui/icons/CheckBoxOutlineBlank";
import CheckBoxIcon from "#material-ui/icons/CheckBox";
import Checkbox from "#material-ui/core/Checkbox";
import SearchIcon from "#material-ui/icons/Search";
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
const data = [
{ donor: "Trader Joe's", receiver: "Person-to-Person" },
{ donor: "Trader Joe's", receiver: "Homes with Hope" },
{ donor: "Santa Maria", receiver: "Gillespie Center" },
{ donor: "Santa Maria", receiver: null }
];
export const App = props => {
const donors = [...new Set(data.map(row => row.donor))].map(row => {
return {
groupName: "Donors",
type: "donor",
title: row || "null"
};
});
const receivers = [...new Set(data.map(row => row.receiver))].map(row => {
return {
groupName: "Receivers",
type: "receiver",
title: row || "null"
};
});
const option2 = [...donors, ...receivers];
const [myTempVal, setMyTempVal] = React.useState([]);
return (
<Autocomplete
open
multiple
value={myTempVal}
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
renderOption={(option, { selected }) => {
return (
<>
<Checkbox
onClick={
()=>{
if(myTempVal.filter(obj=>obj.title===option.title).length!==0){
setMyTempVal([...myTempVal.filter(obj=>obj.title!==option.title)],console.log(myTempVal))
}else{
setMyTempVal([...myTempVal.filter(obj=>obj.title!==option.title),option],console.log(myTempVal))
}
}
}
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={myTempVal.filter(obj=>obj.title===option.title).length!==0}
/>
{option.title}
</>
);
}}
options={option2}
// groupBy={option => option.groupName}
getOptionLabel={option => option.title}
renderInput={params => (
<div>
<div>
<SearchIcon />
</div>
<TextField
variant="outlined"
fullWidth
ref={params.InputProps.ref}
inputProps={params.inputProps}
/>
</div>
)}
/>
);
};
export default App;
It is bit late to Answer this question but it might help someone.
In your code you have added onChange event in Autocomplete. When you click on checkbox it will trigger 2 times, one for checkbox and one for Autocomplte. Hence 2nd time trigger makes again checkbox unchecked so u get value in console but still checkbox is empty.
You can remove your checkbox in renderOption and use checked and uncheked icon instaed of checkbox.
renderOption={(option, { selected }) => {
return (
<React.Fragment>
{selected ? <CheckedIcon> : <uncheckedIcon>}
<div>
{option.title}
</div>
</React.Fragment>
</>
);
}}

How to access field data in other field

I have a modal form with a form that uses Formik. Here are two pictures that show two states of that form that can be toggled with a switch.Initially I fill text into fields which can be added dynamically and stored as an array with .
The second picture shows how I toggled to textarea. There you can also add text with commas that will be turned into an array.
Is there any way to fill data in input fields from the first screen, toggle into textarea and access already inputted data.
I understand formik keeps that state somewhere. But at the moment these fields have a separate state.
Here is my component:
class ModalForm extends React.Component {
constructor(props) {
super(props);
this.state = {
disabled: true,
};
}
onChange = () => {
this.setState({
disabled: !this.state.disabled,
});
};
render() {
var {
visible = false,
onCancel,
onRequest,
submitting,
setSubscriberType,
editing,
subscriptionTypeString,
tested,
selectedGates,
} = this.props;
const { gateId } = selectedGates.length && selectedGates[0];
const handleSubmit = values => {
console.log(values);
onRequest && onRequest({ gateId, ...values });
};
const { disabled } = this.state;
return (
<Modal
footer={null}
closable
title="Список абонентов для выбранного гейта"
visible={visible}
onCancel={onCancel}
onOk={handleSubmit}
destroyOnClose
width="600px"
>
<StyledDescription>
<Switch onChange={this.onChange} />
<StyledLabel>массовый ввод</StyledLabel>
</StyledDescription>
<Formik
initialValues={{ abonents: [''] }}
onSubmit={handleSubmit}
render={({ values, handleChange }) => (
<Form>
{disabled ? (
<FieldArray
name="abonents"
render={arrayHelpers => {
return (
<div>
{values.abonents.map((value, index) => (
<div key={index}>
<MyTextInput
placeholder="Абонент ID"
name={`abonents.${index}`}
value={value}
onChange={handleChange}
/>
<Button
shape="circle"
icon="delete"
onClick={() => {
arrayHelpers.remove(index);
}}
/>
</div>
))}
<Button type="dashed" onClick={() => arrayHelpers.push('')}>
<Icon type="plus" />Добавить абонента
</Button>
</div>
);
}}
/>
) : (
<StyledField
placeholder="Введите ID абонентов через запятую"
name="message"
component="textarea"
/>
)}
<Footer>
<Button type="primary" htmlType="submit">
Запросить
</Button>
</Footer>
</Form>
)}
/>
</Modal>
);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
Pretty easy, formik stores values inside values.abonents, hence you can use it inside textarea
let { Formik, Form, Field, ErrorMessage, FieldArray } = window.Formik;
function App () {
const [disabled, setDisabled] = React.useState(false) // some boilerplate code
function submit (values) {
console.log('submit', values)
}
return (
<Formik
initialValues={{ abonents: [] }}
onSubmit={submit}
render={({ values, handleChange, setFieldValue }) => (
<Form>
<FieldArray
name='abonents'
render={arrayHelpers => {
if (!disabled) {
return (
<textarea onChange={(e) => {
e.preventDefault()
setFieldValue('abonents', e.target.value.split(', '))
}} value={values.abonents.join(', ')}></textarea>
)
}
return (
<div>
{
values.abonents.map((value, index) => (
<div key={index}>
<input
placeholder='Абонент ID'
name={`abonents.${index}`}
value={value}
onChange={handleChange}
/>
<button onClick={(e) => {
e.preventDefault()
arrayHelpers.remove(index)
}}>
-
</button>
</div>
))
}
<button onClick={(e) => {
e.preventDefault()
arrayHelpers.push('')
}}>
+
</button>
</div>
)
}}
/>
<button type='submit'>Submit</button>
<button onClick={e => {
e.preventDefault()
setDisabled(!disabled)
}}>toggle</button>
</Form>
)}
/>
)
}
ReactDOM.render(<App />, document.querySelector('#root'))
<script src="https://unpkg.com/react#16.9.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/formik/dist/formik.umd.production.js"></script>
<div id='root'></div>
I found a solution. You have got to give the same name to the input and textarea, so whe you add text in input will automatically change text in textarea
<FieldArray
name="abonents"
render={arrayHelpers => {
and
<StyledField
placeholder="Введите ID абонентов через запятую"
name="abonents"
component="textarea"
/>
These two fields have same name so they are rendered interchangeably but have common text inside them

Resources