Description
I'm working with a formik form using mui for styling, I wanted to replace an input component with something more advanced as shown below:
My intention is to update formik context and make this selection behave as a regular input form. Unfortunatelly I have not found a lot of documentation or examples of this, and I would like to know best practices for a case like this.
What I tried
This is what I tried, I used mui FormControl enclosing some custom HTML. This works but I can not clear the error when the user moves to the next page. See code below:
Question
I would like to know how to manually clear the error in Formik or if there is a better way to achieve this using mui and formik. Any help is appreciated. Thanks.
import React, { useState } from 'react';
import { useFormikContext } from 'formik';
import { at } from 'lodash';
import { useField } from 'formik';
import { FormLabel, FormControl, FormHelperText, Paper, makeStyles } from '#material-ui/core';
import Grid from '#material-ui/core/Grid';
import { clsx } from 'clsx';
const useStyles = makeStyles({
paper: {
padding: 20,
cursor: 'pointer'
},
active: {
color: '#4285F4'
},
label: {
paddingBottom: 20
}
});
export default function Toggle(props) {
const classes = useStyles();
const { name, label, options, ...rest } = props;
const [field, meta] = useField(props);
const [touched, error] = at(meta, 'touched', 'error');
const isError = touched && error && true;
const { values: formValues } = useFormikContext();
const [activeValue, setActiveValue] = useState(formValues[name]);
function _renderHelperText() {
if (isError) {
return <FormHelperText>{error}</FormHelperText>;
}
}
function _handleSelection(event) {
formValues[name] = event.target.dataset.value;
setActiveValue(event.target.dataset.value);
}
return (
<FormControl {...rest} error={isError}>
<FormLabel className={classes.label}>{label}</FormLabel>
<Grid sx={{ flexGrow: 1 }} container spacing={2}>
{options.map((option, index) => (
<Grid key={index} item>
<Paper
name={name}
data-name={name}
data-value={option.value}
onClick={_handleSelection}
className={clsx(classes.paper, { [classes.active]: option.value == activeValue })}>
{option.lowerValue} - {option.higherValue}
</Paper>
</Grid>
))}
</Grid>
{_renderHelperText()}
</FormControl>
);
}
My yup validation section
CreditScore: {
name: 'Credit Score',
render: <CreditScoreStep />,
formField: {
credit_score: {
name: 'credit_score',
label: 'Credit Score',
initial: ''
}
},
validationSchema: Yup.object().shape({
credit_score: Yup.number().required('Credit Score is required')
})
},
Related
I tried to use react-number-format with mui TextField and Formik, when I submit the form I get a required error. Its seems that nested TextFiled with react-number-format can't get the values.
I tried to console log the input but whit react-number-format I get empty string.
//App.jsx
import * as React from "react";
import { Typography, InputAdornment, Grid } from "#mui/material";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { NumericFormat } from "react-number-format";
import axios from "axios";
import { FieldWrapper as TextField } from "./InputTem";
import { BtnTemp as Button } from "./Btn";
import "./styles.css";
const FORM_STATE = {
price: ""
};
const priceProps = {
name: "price",
label: "Price",
InputProps: {
startAdornment: <InputAdornment position="start">€</InputAdornment>
},
required: true
};
const FORM_VALIDATION = Yup.object().shape({
price: Yup.number().required("require")
});
const App = () => {
return (
<Grid container flex justifyContent={"center"}>
<Formik
initialValues={{ ...FORM_STATE }}
validationSchema={FORM_VALIDATION}
onSubmit={(values) => {
console.log(values);
}}
>
<Form>
<Grid
container
spacing={2}
rowGap={1}
padding={3}
maxWidth={1000}
justifyContent={"center"}
>
<Grid item xs={12}>
<Typography
variant="h3"
component="h1"
align="center"
color="#280982"
>
Price Validation Error
</Typography>
</Grid>
<Grid item xs={12}>
<Grid item xs={12}>
<NumericFormat
name="price"
id="price"
thousandSeparator=","
allowNegative={false}
decimalScale={2}
decimalSeparator="."
fixedDecimalScale={true}
customInput={TextField}
{...priceProps}
/>
</Grid>
<Grid container marginTop="1rem">
<Button>submit</Button>
</Grid>
</Grid>
</Grid>
</Form>
</Formik>
</Grid>
);
};
export default App;
// Input.jsx
import * as React from "react";
import TextField from "#mui/material/TextField";
import { useField } from "formik";
export const FieldWrapper = ({ name, ...otherProps }) => {
const [field, meta] = useField(name);
if (meta && meta.touched && meta.error) {
otherProps.error = true;
otherProps.helperText = meta.error;
}
const configText = {
...field,
...otherProps,
fullWidth: true,
variant: "outlined"
};
return <TextField {...configText} />;
};
export default FieldWrapper;
//BtnTemplate.jsx
import { Button } from "#mui/material";
import { useFormikContext } from "formik";
export const BtnTemp = ({ children, ...otherProps }) => {
const { submitForm } = useFormikContext();
const formHandler = () => {
submitForm();
};
const btnConfig = {
...otherProps,
fullWidth: true,
variant: "outlined",
onClick: formHandler
};
return <Button {...btnConfig}>{children}</Button>;
};
andhere is a link for CodeSandbox
Thanks in advancce !
Now its works ! just passed the usedField props.
here is the solution in sandbox
import * as React from "react";
import { NumericFormat } from "react-number-format";
import { useField } from "formik";
const PriceTemp = ({ name, ...otherProps }) => {
const [field, meta] = useField(name);
const passwordConfig = {
...field,
...otherProps,
name: "price",
label: "Price",
thousandSeparator: ",",
allowNegative: false,
decimalScale: 2,
decimalSeparator: ".",
fixedDecimalScale: true
};
return <NumericFormat {...passwordConfig} />;
};
export default PriceTemp
;
I am using DatePicker in a custom component and everything works fine including react-hook-form Controllers component for validation. But the top-part of the DatePicker does not display properly. Below is a preview of how it displays.
Here is is how I am using this component to create my own re-useable component. I am at a loss to explain why this is happening. Please any help would be very well appreciated.
Thanks in advance.
import React, { Fragment } from "react";
import { makeStyles } from '#material-ui/core/styles';
import DateFnsUtils from '#date-io/date-fns';
import { MuiPickersUtilsProvider, DatePicker} from '#material-ui/pickers';
import 'date-fns';
import { Control, Controller } from "react-hook-form";
import { Keys } from "../Profile/interfaces";
import { alpha } from '#material-ui/core/styles'
const useStyles = makeStyles((theme) => ({
formControl: {
marginTop: "10px",
marginBottom: "10px",
minWidth: 220,
},
}));
interface Props {
id: string,
label: string,
control: Control<any,any>
required?: boolean,
name: Keys,
requiredMsg?: string,
disabled?: boolean
}
const CustomDate: React.FC<Props> = ({id,label,control, required=false, name, requiredMsg,
disabled = false}) => {
const classes = useStyles();
return(
<div className={classes.formControl}>
<Controller
name={name}
control={control}
rules={{required: required ? requiredMsg : null}}
render={({ field }) =>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<DatePicker
disabled={disabled}
format="dd/MM/yyyy"
inputVariant="filled"
id={id}
autoOk
label={label}
clearable
disableFuture
{...field}
/>
</MuiPickersUtilsProvider>
}
/>
</div>
);
}
export default CustomDate;
https://codesandbox.io/s/large-data-array-with-redux-toolkit-forked-7tp63?file=/demo.tsx
In the given codesandbox example I am using react typescript with redux-toolkit
in codesandbox example. I am trying to bind countries with checkbox and textbox.
When I check on checkboxes it looks slow and textbox edit also feels very slow.
some time it breaks the page.
I am not sure what I am doing wrong.
You should make CountryItem it's own component and make it a pure component:
import React, { FC, useEffect } from "react";
import { createStyles, Theme, makeStyles } from "#material-ui/core/styles";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import { countryList } from "./dummyData";
import { Checkbox, Grid, TextField } from "#material-ui/core";
import { useAppDispatch, useAppSelector } from "./store/hooks";
import {
setCountries,
setCountrySelected,
setCountryValue
} from "./store/slice/geography-slice";
import { Country } from "./interface/country.modal";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: "100%",
backgroundColor: theme.palette.background.paper
}
})
);
const CountryItem: FC<{ country: Country }> = ({ country }) => {
console.log('render item',country.name)
const dispatch = useAppDispatch();
const handleCheckboxChange = (
event: React.ChangeEvent<HTMLInputElement>,
country: Country
) => {
const selectedCountry = { ...country, isSelected: event.target.checked };
dispatch(setCountrySelected(selectedCountry));
};
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedCountry = { ...country, value: event.target.value };
dispatch(setCountryValue(selectedCountry));
};
return (
<ListItem button>
<Checkbox
checked={country?.isSelected ?? false}
onChange={(event) => {
handleCheckboxChange(event, country);
}}
/>
<ListItemText primary={country.name} />
<TextField value={country?.value ?? ""} onChange={handleTextChange} />
</ListItem>
);
};
const PureCountryItem = React.memo(CountryItem)
export default function SimpleList() {
const classes = useStyles();
const dispatch = useAppDispatch();
const { countries } = useAppSelector((state) => state.geography);
useEffect(() => {
dispatch(setCountries(countryList));
}, []);
return (
<div className={classes.root}>
<Grid container>
<Grid item xs={6}>
<List component="nav" aria-label="secondary mailbox folders">
{countries.map((country, index) => (
<PureCountryItem country={country} key={`CountryItem__${index}`} />
))}
</List>
</Grid>
</Grid>
</div>
);
}
this is my first thread I hope i do this right,
I am trying to send data for mysql server and it goes well, however when I use TextField from MUI the validation schema starts looking funny and gives error ("require field") even with the text there, However if i submit the data the mysql receives the data.
and here goes the code...
//uses Express send data to mysql
import { useState, useEffect } from 'react'
import axios from "axios"
import { Container, Grid, Typography} from '#material-ui/core';
import { makeStyles } from '#material-ui/core/styles';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import Textfield from './Forms/IndividualComponents/TextField';
import Button from './Forms/IndividualComponents/Button'
const useStyles = makeStyles((theme) => ({
formWrapper: {
marginTop: theme.spacing(10),
marginBottom: theme.spacing(8),
},
}));
const INITIAL_FORM_STATE = {
firstName: ''
};
const FORM_VALIDATION = Yup.object().shape({
firstName: Yup.string("enter your first name")
.required('Required')
});
export default function ImagesUpload() {
const [firstName, setFirstName] = useState()
const [submitform, setSubmitForm] = useState([])
useEffect(() => {
(async() => {
const result = await axios.get('/submitform')
setSubmitForm(result.data.submitform)
})()
}, [])
const submit = async event => {
event.preventDefault()
const data = new FormData()
data.append('firstName', firstName)
const result = await axios.post('/submitForm', data)
setSubmitForm([result.data, ...submitform])
console.log(firstName)
}
const classes = useStyles();
return (
<Grid item xs={12}>
<Container maxWidth="md">
<div className={classes.formWrapper}>
<Formik
initialValues={{
...INITIAL_FORM_STATE
}}
validationSchema={FORM_VALIDATION}
>
<Form onSubmit={submit}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
Personal details
</Typography>
</Grid>
<Grid item xs={6}>
<Textfield
name="firstName"
label="First Name"
value={firstName}
onChange={(event => setFirstName(event.target.value) )}
type="text"
/>
</Grid>
<button type="submit">Submit</button>
</Grid>
</Form>
</Formik>
<main>
{submitform.map(post => (
<figure key={post.id}>
<figcaption>{post.firstName}</figcaption>
</figure>
))}
</main>
</div>
</Container>
</Grid>
);
}
the Textfield im using is a costum one with the following code:
import React from 'react';
import { TextField } from '#material-ui/core';
import { useField } from 'formik';
const TextfieldWrapper = ({
name,
...otherProps
}) => {
const [field, mata] = useField(name);
const configTextfield = {
...field,
...otherProps,
fullWidth: true,
};
if (mata && mata.touched && mata.error) {
configTextfield.error = true;
configTextfield.helperText = mata.error;
}
return (
<TextField {...configTextfield} />
);
};
export default TextfieldWrapper;
the problem here is the onChange: If i don't use it I cant setFirstName to send the data, but when I use it it breaks the validation... I tried everything i can find online but this is taking away my sleep for real ! :)
I am fetching list of testsuites using api call on component mount. Api returns list in chronological order.
Setting them as options for a select dropdown(Material-UI).
Then set the selected option to latest testSuite and using its Id get the corresponding testSuite data.
Data is retrieved successfully and pie chart is getting displayed.
Api calls are working fine and React dev tools shows the selectedTestSuite value to be set correctly.But DOM doesn't show the selection in the select dropdown.
Can someone please advise what is the mistake I am doing in this code? Thanks in advance.
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { Doughnut } from 'react-chartjs-2';
import { makeStyles } from '#material-ui/styles';
import axios from 'axios';
import { useSpring, animated } from 'react-spring';
import '../../Dashboard.css';
import MenuItem from '#material-ui/core/MenuItem';
import {
Card,
CardHeader,
CardContent,
Divider,
TextField,
} from '#material-ui/core';
import CircularProgress from '#material-ui/core/CircularProgress';
const useStyles = makeStyles(() => ({
circularloader: {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
},
actions: {
justifyContent: 'flex-end',
},
inputField: {
width: '150px',
},
}));
const TestSuiteVsScanCount = (props) => {
const { className, ...rest } = props;
const classes = useStyles();
const [doughData, setDoughData] = useState([]);
const [dataLoadedFlag, setDataLoadedFlag] = useState(false);
const [testSuites, setTestSuites] = useState([]);
const [selectedTestSuite, setSelectedTestSuite] = useState({});
useEffect(() => {
function getTestSuites() {
axios.get('http://localhost:3000/api/v1/testsuite/12').then((resp) => {
setTestSuites(resp.data.reverse());
});
}
getTestSuites();
}, []);
useEffect(() => {
if (testSuites.length > 0) {
setSelectedTestSuite(() => {
return {
type: testSuites[0].TestSuiteName,
id: testSuites[0].TestSuiteId,
};
});
}
}, [testSuites]);
useEffect(() => {
function getTestSuiteData() {
let doughData = [];
if (selectedTestSuite.id) {
axios
.get(
'http://localhost:3000/api/v1/summary/piechart/12?days=30&testsuiteid=' +
selectedTestSuite.id,
)
.then((resp) => {
resp.data.forEach((test) => {
doughData = [test.TestCount, test.ScanCount];
});
setDoughData({
labels: ['Test Count', 'Scan Count'],
datasets: [
{
data: doughData,
backgroundColor: ['#FF6384', '#36A2EB'],
hoverBackgroundColor: ['#FF6384', '#36A2EB'],
},
],
});
setDataLoadedFlag(true);
});
}
}
getTestSuiteData();
}, [selectedTestSuite]);
const ChangeType = (id) => {
testSuites.forEach((suite) => {
if (suite.TestSuiteId === id) {
setSelectedTestSuite({
type: suite.TestSuiteName,
id: suite.TestSuiteId,
});
}
});
};
return (
<Card {...rest} className={clsx(classes.root, className)}>
<CardHeader
action={
<TextField
select
label="Select Test Suite"
placeholder="Select Tests"
value={selectedTestSuite.id}
className={classes.inputField}
name="tests"
onChange={(event) => ChangeType(event.target.value)}
variant="outlined"
InputLabelProps={{
shrink: true,
}}
>
{testSuites.map((testSuite) => (
<MenuItem
key={testSuite.TestSuiteId}
value={testSuite.TestSuiteId}
>
{testSuite.TestSuiteName}
</MenuItem>
))}
</TextField>
}
title="Test Suite vs Scan Count"
/>
<Divider />
<CardContent>
<div>
{dataLoadedFlag ? (
<Doughnut data={doughData} />
) : (
<CircularProgress
thickness="1.0"
size={100}
className={classes.circularloader}
/>
)}
</div>
</CardContent>
<Divider />
</Card>
);
};
TestSuiteVsScanCount.propTypes = {
className: PropTypes.string,
};
export default TestSuiteVsScanCount;
I was able to fix this issue with the help of my colleague by setting the initial state of selectedTestSuite to {type:'', id:0} instead of {}.
Changed this
const [selectedTestSuite, setSelectedTestSuite] = useState({});
To this
const [selectedTestSuite, setSelectedTestSuite] = useState({type:'', id:0});
But I am not sure why this worked.
I believe that the main problem is when you pass a value to TextField component with undefined, the TextField component will assume that is an uncontrolled component.
When you set you initial state for selectedTestSuite to be {} the value for selectedTestSuite.id will be undefined. You can find value API reference in https://material-ui.com/api/text-field/