I've created a Dialog component via Material UI, dynamically imported from another file.
It works fine, except the checkboxes (also created with Material UI) inside this Dialog do not reset after each time Dialog closes. They only reset on page refresh. Other types of input, such as text or password do reset themselves automatically.
Please notice that the Dialog component does not have keepMounted = true, and that's how it resets its inputs automatically. Because, this value is false by default.
Here is the code for the original Dialog modal component:
import React, {useState, useEffect} from "react";
import Button from "#material/react-button";
import Divider from "#material-ui/core/Divider";
import Dialog from "#material-ui/core/Dialog";
import DialogActions from "#material-ui/core/DialogActions";
import DialogContent from "#material-ui/core/DialogContent";
import DialogContentText from "#material-ui/core/DialogContentText";
import DialogTitle from "#material-ui/core/DialogTitle";
import TextField from "#material-ui/core/TextField";
import FormGroup from "#material-ui/core/FormGroup";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
const SearchModal = (props) => {
const [checkState, setCheckState] = useState({
checkedQuestions: true,
checkedAnswers: true,
checkedVerified: true,
checkedPending: true,
checkedDisputed: false
});
useEffect(() => {
if(
checkState.checkedQuestions === false &&
checkState.checkedAnswers === false
){
setCheckState({
...checkState,
checkedQuestions: true,
checkedAnswers: true
});
}
if(
checkState.checkedVerified === false &&
checkState.checkedPending === false &&
checkState.checkedDisputed === false
){
setCheckState({
...checkState,
checkedVerified: true,
checkedPending: true,
checkedDisputed: false
});
}
});
const checkSet = (event) => {
setCheckState({
...checkState,
[event.target.name]: event.target.checked
});
}
return(
<Dialog
open={props.searchOpen}
onClose={props.handleClose}
aria-labelledby="searchModalTitle"
aria-describedby="searchModalDescription"
id="searchModal"
>
<DialogTitle id="dialog">{"Search tolodire."}</DialogTitle>
<DialogContent>
<DialogContentText className="marginBottom-17" id="searchModalDescription">
Search for questions or answers.
</DialogContentText>
<TextField
required
type="search"
id="searchQuery"
label="Enter keywords or sentences"
placeholder="Required"
variant="outlined"
data-owner="searchModal"
autoFocus
/>
<DialogContentText className="marginTop-20 marginBottom-10">
Use filters to search in detail.
</DialogContentText>
<FormGroup row className="marginTop-5">
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedQuestions}
onChange={(e) => checkSet(e)}
name="checkedQuestions"
/>
}
label="Questions"
/>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedAnswers}
onChange={(e) => checkSet(e)}
name="checkedAnswers"
/>
}
label="Answers"
/>
</FormGroup>
<Divider/>
<FormGroup row>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedVerified}
onChange={(e) => checkSet(e)}
name="checkedVerified"
/>
}
label="Verified"
/>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedPending}
onChange={(e) => checkSet(e)}
name="checkedPending"
/>
}
label="Pending Verification"
/>
<FormControlLabel
control={
<Checkbox
color="default"
checked={checkState.checkedDisputed}
onChange={(e) => checkSet(e)}
name="checkedDisputed"
/>
}
label="Disputed"
/>
</FormGroup>
</DialogContent>
<DialogActions>
<Button raised className="button regularButton font-body" onClick={props.handleClose}>
Search
</Button>
</DialogActions>
</Dialog>
);
}
export default SearchModal
I've already tried searching this issue on Google and StackOverflow, yet, I haven't found any solution. Any contribution is appreciated.
P.S: The handleClose const is on another file;
const [searchOpen, setSearchOpen] = useState(false);
const handleSearchOpen = () => {
setSearchOpen(true);
};
const handleClose = () => {
setSearchOpen(false);
};
I've found the solution myself.
In the same file for SearchModal, I've created another useEffect field to check if the modal is open or not. When I was trying the same inside the pre-existing useEffect field, it created infinite loop.
For the reset process, I'm assigning the default values from scratch independently from other consts.
I am aware that the implementation could be much better, with more code-reuse or dynamic elements. But, this is just a primitive solution until a better one appears any time soon.
So, the solution is:
useEffect(() => {
if(props.searchOpen === false){
setCheckState({
checkedQuestions: true,
checkedAnswers: false,
checkedVerified: true,
checkedPending: true,
checkedDisputed: false
});
}
}, [props.searchOpen]);
Related
I'm implementing the multi-step wizard example with Material-UI components and it works well with the useField() hook but I cannot figure out how to bring setFieldValue() into scope, so I can use it from a wizard step.
I've seen suggestions to use the connect() higher-order component but I have no idea how to do that.
Here is a snippet of my code: CodeSandbox, and the use case:
A wizard step has some optional fields that can be shown/hidden using a Material-UI Switch. I would like the values in the optional fields to be cleared when the switch is toggled off.
I.e.
Toggle switch on.
Enter data in Comments field.
Toggle switch off.
Comments value is cleared.
Toggle switch on.
Comments field is empty.
Hoping someone can help! Thanks.
I came across this answer the other day but discarded it because I couldn't get it working.
It does actually work but I'm in two minds as to whether it's the right approach.
const handleOptionalChange = (form) => {
setOptional(!optional)
form.setFieldValue('optionalComments', '', false)
}
<FormGroup>
<FormControlLabel
control={
// As this element is not a Formik field, it has no access to the Formik context.
// Wrap with Field to gain access to the context.
<Field>
{({ field, form }) => (
<Switch
checked={optional}
onChange={() => handleOptionalChange(form)}
name="optional"
color="primary"
/>
)}
</Field>
}
label="Optional"
/>
</FormGroup>
CodeSandbox.
I believe this is what you're after: CodeSandbox. I forked your CodeSandbox.
I tried to follow your code as closely as possible and ended up not using WizardStep. The step variable is returning a React component that is a child to Formik. Formik is rendered with props e.g. setFieldValue, which can be passed down to its children. In order to pass the setFieldValue as a prop to step, I had to use cloneElement() (https://reactjs.org/docs/react-api.html#cloneelement), which allows me to clone the step component and add props to it as follows.
// FormikWizard.js
<Formik
initialValues={snapshot}
onSubmit={handleSubmit}
validate={step.props.validate}
>
{(formik) => (
<Form>
<DialogContent className={classes.wizardDialogContent}>
<Stepper
className={classes.wizardDialogStepper}
activeStep={stepNumber}
alternativeLabel
>
{steps.map((step) => (
<Step key={step.props.name}>
<StepLabel>{step.props.name}</StepLabel>
</Step>
))}
</Stepper>
<Box
className={classes.wizardStepContent}
data-cy="wizardStepContent"
>
{React.cloneElement(step, {
setFieldValue: formik.setFieldValue
})}
</Box>
</DialogContent>
<DialogActions
className={classes.wizardDialogActions}
data-cy="wizardDialogActions"
>
<Button onClick={handleCancel} color="primary">
Cancel
</Button>
<Button
disabled={stepNumber <= 0}
onClick={() => handleBack(formik.values)}
color="primary"
>
Back
</Button>
<Button
disabled={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{isFinalStep ? "Submit" : "Next"}
</Button>
</DialogActions>
</Form>
)}
</Formik>
To access the setFieldValue prop in the child component, in App.js, I created a new component called StepOne and used it to wrap around the inputs, instead of using WizardStep. Now I am able to access setFieldValue and use it in the handleOptionalChange function.
// App.js
import React, { useState } from "react";
import "./styles.css";
import { makeStyles } from "#material-ui/core/styles";
import Box from "#material-ui/core/Box";
import CssBaseline from "#material-ui/core/CssBaseline";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import FormGroup from "#material-ui/core/FormGroup";
import Switch from "#material-ui/core/Switch";
import FormikTextField from "./FormikTextField";
import { Wizard, WizardStep } from "./FormikWizard";
const useStyles = makeStyles((theme) => ({
content: {
display: "flex",
flexFlow: "column nowrap",
alignItems: "center",
width: "100%"
}
}));
const initialValues = {
forename: "",
surname: "",
optionalComments: ""
};
const StepOne = ({ setFieldValue }) => {
const classes = useStyles();
const [optional, setOptional] = useState(false);
const displayOptional = optional ? null : "none";
const handleOptionalChange = () => {
setFieldValue("optionalComments", "");
setOptional(!optional);
};
return (
<Box className={classes.content}>
<FormikTextField
fullWidth
size="small"
variant="outlined"
name="forename"
label="Forename"
type="text"
/>
<FormikTextField
fullWidth
size="small"
variant="outlined"
name="surname"
label="Surname"
type="text"
/>
<FormGroup>
<FormControlLabel
control={
<Switch
checked={optional}
onChange={handleOptionalChange}
name="optional"
color="primary"
/>
}
label="Optional"
/>
</FormGroup>
<FormikTextField
style={{ display: displayOptional }}
fullWidth
size="small"
variant="outlined"
name="optionalComments"
label="Comments"
type="text"
/>
</Box>
);
};
function App(props) {
return (
<>
<CssBaseline />
<Wizard
title="My Wizard"
open={true}
initialValues={initialValues}
onCancel={() => {
return;
}}
onSubmit={async (values) => {
console.log(JSON.stringify(values));
}}
>
<StepOne />
<StepTwo />
</Wizard>
</>
);
}
export default App;
Alternative
To use setFieldValue in Formik, the easiest way would be to have the all input elements within the <Formik></Formik tags. You could conditionally render the input elements based on what step you're on as follows. This gives the inputs a direct access to setFieldValue so you can call setFieldValue("optionalComments", "") on the Switch input which will clear the comments on each toggle. Although this may mean you'll have a longer form, I don't think this is necessarily a bad thing.
<Formik>
<Form>
{step === 1 && <div>
// Insert inputs here
</div>}
{step === 2 && <div>
<TextField
onChange={(event) => setFieldValue("someField", event.target.value)}
/>
<Switch
checked={optional}
onChange={() => {
setFieldValue("optionalComments", "");
setOptional(!optional);
}}
name="optional"
color="primary"
/>
</div>}
</Form>
</Formik>
How can I write a react testing library test script to validate clicking on a Material UI radio button? The below is a recreation of the issue I am having.
import React from 'react';
import Radio from '#material-ui/core/Radio';
import RadioGroup from '#material-ui/core/RadioGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
export default function RadioButtonsGroup() {
const [value, setValue] = React.useState('female');
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<FormControl component="fieldset">
<FormLabel component="legend">Gender</FormLabel>
<RadioGroup aria-label="gender" name="gender1" value={value} onChange={handleChange}>
<FormControlLabel value="female" control={<Radio />} label="Female" />
<FormControlLabel value="male" control={<Radio />} label="Male" />
<FormControlLabel value="other" control={<Radio />} label="Other" />
<FormControlLabel value="disabled" disabled control={<Radio />} label="(Disabled option)" />
</RadioGroup>
</FormControl>
);
}
and the test
import { render, screen, fireEvent } from "#testing-library/react";
import RadioButtonsGroup from "./demo";
test("can click Male radio option", () => {
render(<RadioButtonsGroup />);
const maleLabel = screen.getByLabelText(/Male/);
fireEvent.click(maleLabel);
expect(maleLabel).toBeChecked();
});
Hey – get by role should help you here. Try:
const button = screen.getByRole("radio", { name: "Male" });
This should give you access to the radio button with the lable as Male.
Then you can do
const leftClick = { button: 0 };
userEvent.click(button, leftClick);
expect(button.checked).toBe(true);
One thing I've found though is that the .checked property doesn't play nice with Typescript so if you're writing typescript, you may get warnings about .checked being non-existent
I have a react app, the parent component has a button which when clicked shows a simple dialog with one text input and a submit button. Strict mode is enabled. There are two issues
The form input is set to show an initial value (formik initialValues is set) in the input but that is not being set
When the button is clicked I see an error in the console;
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
The dialog component comes from Material UI and the form comes from Formik. I've created a simple repro here. The error is in the dev tools console. What would cause that error and why is the value not initialising?
Here's the parent component;
import React, { useState } from "react";
import { Button, Typography } from "#material-ui/core";
import ProfileEditor from "./ProfileEditor";
function ProfileManager() {
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
return (
<div>
<Typography variant="h5">Profile Manager</Typography>
<Button variant="outlined" color="primary" onClick={handleOpen}>
Open profile editor dialog
</Button>
<ProfileEditor open={open} onClose={handleClose}></ProfileEditor>
</div>
);
}
export default ProfileManager;
and the dialog component displayed when the button is clicked in the component above;
import React from "react";
import {
Button,
Dialog,
DialogContent,
LinearProgress,
TextField
} from "#material-ui/core";
import { Formik, Form } from "formik";
interface Props {
open: boolean;
onClose: () => void;
}
function ProfileEditor(props: Props) {
return (
<Dialog open={props.open}>
<DialogContent>
<Formik
// initial value not being displayed !!! 😢
initialValues={{
firstName: "Billy"
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
alert(JSON.stringify(values, null, 2));
}, 500);
}}
>
{({ submitForm, isSubmitting }) => (
<Form>
<TextField name="firstName" type="text" label="First name" />
{isSubmitting && <LinearProgress />}
<br />
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={submitForm}
>
Submit
</Button>
<Button variant="contained" onClick={props.onClose}>
Close
</Button>
</Form>
)}
</Formik>
</DialogContent>
</Dialog>
);
}
export default ProfileEditor;
You need to include a value prop to the form field to have it initialized properly.
{({ submitForm, isSubmitting, values }) => (
<Form>
<TextField
name="firstName"
type="text"
label="First name"
value={values.firstName} /* you need this prop */
/>
...
CodeSandBox: https://codesandbox.io/s/so-react-formik-inside-material-dialog-sfq4e?file=/ProfileEditor.tsx
Regarding your issue on the console, I'm not entirely sure at this point what is causing it, but if it bothers you or is causing additional problems, perhaps you can opt to move out of strict mode
<React.Fragment>
<ProfileManager></ProfileManager>
</React.Fragment>
I am trying to use a custom Radio component with React-final-form but it is not acting as a radio button but as a checkbox, ie, all the buttons are open for selection.
The 3rd party Radio button has the following schema:
checked boolean
Whether or not radio is checked
onChange () => void
Called when the user attempts to change the checked state
name string
The input name, used to reference the element in JavaScript
I created a custom Component for using the Radio Component:
const CustomRadio = (props: any) => (
<Radio
{...props.input}
{...props.rest}
name={props.name}
onChange={() => props.input.onChange()}
/>
)
and I am using it as follows:
<Field name="food"
component={CustomRadio}
value="1"
/>
<Field name="food"
component={CustomRadio}
value="2"
/>
Being very new to RFF and new to React, I may be doing something very wrong, hence any help will be appreciated.
Basically, I want to use RFF with my 3rd party components. Though I have been successful to use my Input component with RFF as expected, Radio Button is the one creating problems.
Here is the correct implementation for Radio with react-final-form:-
https://codesandbox.io/s/vibrant-easley-5n1ek?file=/index.js
/* eslint-disable jsx-a11y/accessible-emoji */
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import RadioGroup from "#material-ui/core/RadioGroup";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import FormControl from "#material-ui/core/FormControl";
import Radio from "#material-ui/core/Radio";
import FormLabel from "#material-ui/core/FormLabel";
const RadioWrapper = (props) => {
const {
input: { checked, value, name, onChange, ...restInput },
} = props;
return (
<Radio
name={name}
inputProps={restInput}
onChange={onChange}
checked={checked}
value={value}
/>
);
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => {
return (
<Styles>
<h1>React Final Form - Simple Example</h1>
<a
href="https://final-form.org/react"
target="_blank"
rel="noopener noreferrer"
>
Read Docs
</a>
<Form
onSubmit={onSubmit}
initialValues={{
employed: false,
all_sub_tenants: "true"
}}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}>
<FormControl component="fieldset">
<FormLabel component="legend">
Select Tenants
</FormLabel>
<RadioGroup aria-label="allSubTenants" name="allSubTenants">
<FormControlLabel
value="true"
control={
<Field
name="all_sub_tenants"
component={RadioWrapper}
type="radio"
value={"true"}
/>
}
label="All Sub-Tenants"
/>
<FormControlLabel
value="false"
control={
<Field
name="all_sub_tenants"
component={RadioWrapper}
type="radio"
value={"false"}
/>
}
label="Select Sub-Tenants"
/>
</RadioGroup>
</FormControl>
<div>
<label>Notes</label>
<Field name="notes" component="textarea" placeholder="Notes" />
</div>
<div className="buttons">
<button type="submit" disabled={submitting || pristine}>
Submit
</button>
<button
type="button"
onClick={form.reset}
disabled={submitting || pristine}
>
Reset
</button>
</div>
<pre>{JSON.stringify(values, 0, 2)}</pre>
</form>
)}
/>
</Styles>
);
};
render(<App />, document.getElementById("root"));
I wish to be able to uncheck radio buttons, Idea is like this: if I click on some radio button, it is going to be checked, if I click on another field, this another field is going to be checked instead BUT if I click on field which is already checked, I wish to uncheck it so all fields are empty. I tried to catch the moment of being checked or unchecked but seems like opposite to checkboxes, Radio buttons don't have this field. does anyone has idea how to achieve that?
setTests = (key, e) => {
console.log(e.checked)
if (e.checked) {
// this.setState({[key]: null})
console.log('works')
}
}
RadioGroup
value={this.state.test_mode}
style={{ display: "block" }}
onChange={e => this.setTests({ "test_mode", e.target })}
>
<FormControlLabel value="before" control={<Radio color="primary"/>} label="before tests" />
<FormControlLabel value="progressing" control={<Radio color="primary"/>} label="progressing" />
<FormControlLabel value="done" control={<Radio color="primary"/>} label="done" />
</RadioGroup>
Below is an example of how to do this. Instead of using the onChange of the RadioGroup, you use the onClick event of the Radio. If the new value matches the current value in state, then set the value to empty string.
import React from "react";
import { makeStyles } from "#material-ui/core/styles";
import Radio from "#material-ui/core/Radio";
import RadioGroup from "#material-ui/core/RadioGroup";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import FormControl from "#material-ui/core/FormControl";
import FormLabel from "#material-ui/core/FormLabel";
const useStyles = makeStyles(theme => ({
root: {
display: "flex"
},
formControl: {
margin: theme.spacing(3)
},
group: {
margin: theme.spacing(1, 0)
}
}));
export default function RadioButtonsGroup() {
const classes = useStyles();
const [value, setValue] = React.useState("female");
function handleClick(event) {
if (event.target.value === value) {
setValue("");
} else {
setValue(event.target.value);
}
}
return (
<div className={classes.root}>
<FormControl component="fieldset" className={classes.formControl}>
<FormLabel component="legend">Gender</FormLabel>
<RadioGroup
aria-label="gender"
name="gender1"
className={classes.group}
value={value}
>
<FormControlLabel
value="female"
control={<Radio onClick={handleClick} />}
label="Female"
/>
<FormControlLabel
value="male"
control={<Radio onClick={handleClick} />}
label="Male"
/>
<FormControlLabel
value="other"
control={<Radio onClick={handleClick} />}
label="Other"
/>
</RadioGroup>
</FormControl>
</div>
);
}