React useRef not working with Material UI TextField - reactjs

why is useRef not working with Material UI TextField but works fine with a traditional html input element?
The UI:
I have a Textfield input element and above it I have a list of buttons (letters from the latin alphabet, special characters). I can focus the input bar when the component renders the first time, but if I click on one of the buttons above the input, the focus is not back on the input bar.
What I tried so far
const Search = (props) => {
const [searchTerm, setSearchTerm] = useState("");
const inputRef = useRef(null);
const handleLatinButton = (letter) => {
inputRef.current.focus();
setSearchTerm(searchTerm + letter);
};
const handleSubmit = (e) => {
e.preventDefault();
History.push("/?q=" + searchTerm);
props.onFormSubmit(searchTerm, optionValue);
setSearchTerm("");
};
useEffect(() => {
const params = new URLSearchParams(location.search);
const q = params.get("q");
setSearchTerm(q);
inputRef.current.focus();
}, []); // Am I missing something here in the array?
JSX
<Button letter="č" handleLatinButton={handleLatinButton} />
<Button letter="ḍ" handleLatinButton={handleLatinButton} />
...
<form onSubmit={handleSubmit}>
<TextField
fullWidth
label="Search..."
value={searchTerm}
onChange={handleChange}
ref={inputRef}
autoFocus="true"
InputProps={{
endAdornment: (
<InputAdornment position="start">
<IconButton type="submit">
<SearchIcon />
</IconButton>
</InputAdornment>
),
}}
/>
</form>
If I replace TextField with input, the focus works after I click on one of the latin buttons.

It's just the way MUI handles the ref...
<TextField
fullWidth
label="Search..."
value={searchTerm}
onChange={handleChange}
change this >>> ref={inputRef}
for this >>> inputRef={inputRef}
autoFocus="true"
InputProps={{
endAdornment: (
<InputAdornment position="start">
<IconButton type="submit">
<SearchIcon />
</IconButton>
</InputAdornment>
),
}}
/>
perhaps to avoid confusion change the ref name

Related

ReactJS Typescript Material UI Dialog Reusable Component

Please assist me. I was creating a Reusable Material UI Modal Dialog component so that I can call it from any component but it is not showing whenever I click the button on any component. See the code below :
*********************THE MODAL COMPONENT
--->ReusableDialog.tsx
import React, { useState, ReactNode } from 'react';
import { createStyles, Theme, withStyles, WithStyles } from '#material-ui/core/styles';
import { Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions } from '#mui/material';
import IconButton from '#material-ui/core/IconButton';
import Typography from '#material-ui/core/Typography';
type Props = {
show: boolean,
title: string,
body: ReactNode,
onSubmitButtonClicked: () => void;
closeDialog: () => void;
};
export default function AppModalDialog(Props: Props) {
const [open, setOpen] = useState(Props.show)
return <>
<Dialog
open={Props.show}
onClose = {() => setOpen(false)}
aria-labelledby='dialog-title'
aria-describedby='dialog-description'
>
<DialogTitle id='dialog-title'> {Props.title} </DialogTitle>
<DialogContent>
<DialogContentText id='dialog-description' >{Props.body}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() =>Props.closeDialog}>Cancel</Button>
<Button autoFocus onClick={() => Props.onSubmitButtonClicked()}>Submit</Button>
</DialogActions>
</Dialog>
</>
}
*************************THE FORM THAT WILL DISPLAY IN THE MODAL
childFormDialog.tsx
interface paymentTypeProps{
paymenttype: string;
}
const baseURL = process.env.REACT_APP_API_BASE_URL_LOCAL;
const childFormDialog = (Props: paymentTypeProps) => {
const navigate = useNavigate();
const [submitting, setSubmitting] = useState("");
const [errormsg, setErrorMsg] = useState("");
const [fullname, setFullName] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [phone, setPhone] = useState<string>('');
const [paymenttypes, setPaymenttypes] = useState(Props.paymenttype)
useLayoutEffect(()=>{
setFullName(secureLocalStorage.getItem('fullname').toString());
setEmail(secureLocalStorage.getItem('email').toString());
setPhone(secureLocalStorage.getItem('phone').toString());
//setPaymentType(Props.)
}, []);
const validationSchema = yup.object({
fullname: yup.string().required('Your full name is needed'),
email: yup.string().required('An email address is needed'),
phone: yup.string().required('Phone number is needed'),
amount: yup.string().required('Please type an amount'),
paymenttitle: yup.string().required('Please indicate title for this payment'),
paymentdescription: yup.string(),
});
const formik = useFormik({
initialValues: {
fullname: fullname,
email: email,
phone: phone,
amount: '',
paymenttitle: '',
paymentdescription: '',
},
validationSchema: validationSchema,
onSubmit: async (values) => {
handleFormSubmit();
}
})
const makePayment = () => {
}
const handleFormSubmit = () => {
var data = {
fullname: formik.values.fullname,
email: formik.values.email,
phone: formik.values.phone,
amount: formik.values.amount,
paymenttitle: formik.values.paymentdescription,
paymentdescription: formik.values.paymentdescription,
}
if(paymenttypes =='A') {
configPaymentAPI_A(data.amount, data.email, data.phone, data.fullname, data.paymenttitle, data.paymentdescription)
}
else{
configPaymentAPI_B(data.email, data.amount)
}
} //function closed
return <>
<form style={{width: '100%}'}}>
<div className="row px-3">
<Grid container direction={"column"} spacing={2}>
<Grid item>
<TextField
style ={{width: '100%'}}
label="Payment Title"
name="paymenttitle"
placeholder="Payment Title"
variant="standard"
type="text"
value={formik.values.paymenttitle}
onChange={formik.handleChange}
error={formik.touched.paymenttitle && Boolean(formik.errors.paymenttitle)}
helperText={formik.touched.paymenttitle && formik.errors.paymenttitle}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<CallToActionIcon color="primary" />
</InputAdornment>
),
}} />
</Grid>
<Grid item>
<TextField
style ={{width: '100%'}}
label="Full Name"
name="fullname"
placeholder="Your Full Name"
variant="standard"
type="text"
aria-readonly
value={formik.values.fullname}
onChange={formik.handleChange}
error={formik.touched.fullname && Boolean(formik.errors.fullname)}
helperText={formik.touched.fullname && formik.errors.fullname}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<PersonOutlineIcon color="primary" />
</InputAdornment>
),
}} />
</Grid>
<Grid item>
<TextField
style ={{width: '100%'}}
label="Email Address"
name="email"
placeholder="Your Email"
variant="standard"
type="email"
aria-readonly
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<EmailIcon color="primary" />
</InputAdornment>
),
}} />
</Grid>
<Grid item>
<TextField
style ={{width: '100%'}}
label="Phone Number"
name="phone"
placeholder="Your Phone Number"
variant="standard"
type="number"
aria-readonly
value={formik.values.phone}
onChange={formik.handleChange}
error={formik.touched.phone && Boolean(formik.errors.phone)}
helperText={formik.touched.phone && formik.errors.phone}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<PhoneIphoneIcon color="primary" />
</InputAdornment>
),
}} />
</Grid>
<Grid item>
<TextField
style ={{width: '100%'}}
label="Amount"
name="amount"
placeholder="Amount To Be Paid"
variant="standard"
type="number"
value={formik.values.amount}
onChange={formik.handleChange}
error={formik.touched.amount && Boolean(formik.errors.amount)}
helperText={formik.touched.amount && formik.errors.amount}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<CallToActionIcon color="primary" />
</InputAdornment>
),
}} />
</Grid>
<Grid item>
<TextField
style ={{width: '100%'}}
label="Payment Description"
name="paymentdescription"
placeholder="Type some notes for this payment"
variant="standard"
type="text"
value={formik.values.paymentdescription}
onChange={formik.handleChange}
error={formik.touched.paymentdescription && Boolean(formik.errors.paymentdescription)}
helperText={formik.touched.paymentdescription && formik.errors.paymentdescription}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<EventNoteIcon color="primary" />
</InputAdornment>
),
}} />
</Grid>
<Grid item>
<Button
type="submit"
variant="contained"
startIcon={<SendIcon />}
disableRipple
>Remit Your Payment </Button>
</Grid>
</Grid>
</div>
</form>
</>
}
export default childFormDialog
********************THE PARENT FORM THAT CALL/INVOKE THE DIALOG ONCLICK OF A LINK
--->Parent.tsx
const [paymentTypState, setPaymentTypState] = useState<string>('');
const [modalDisplay, setModalDisplay] = useState<boolean>(false);
const [close, setClose] = useState<boolean>(true);
const [open, setOpen] = useState(false);
const onclickPaymentA = () => {
setOpen(true);
setPaymentTypState('A');
}
const onclickPaymentB = () => {
setOpen(true);
setPaymentTypState('B');
}
const handleDialogOnSubmit = () => {
}
return <>
<a onClick={onclickPaymentA}>
<img src="../assets/images/paymentA.png" style={{width:300, height: 150}}/>
</a>
<a onClick={onclickPaymentB}>
<img src="../assets/images/paymentB.png" style={{width:300, height: 150}}/>
</a>
<ReusableDialog
show = { open }
title = 'Top Up Your Wallet'
body = {<childFormDialog paymenttype={paymentTypState} />}
closeDialog = {() => setOpen(false) }
onSubmitButtonClicked = { handleDialogOnSubmit }
/>
</>
**********THE ROUTER OF THE APPLICATION THAT USES BROWSERROUTER
import React from "react";
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './Login';
import Register from './Register';
import Dashboard from './Dashboard';
import Parent from './Parent';
<Route path="/login" element={<Login />}>
</Route>
<Route path="/register" element={<Register />}>
</Route>
<Route path="/dashboard" element={<AuthenticatedRoute element={<Dashboard />} />}>
<Route path="/parent" element={<AuthenticatedRoute element={<Parent />} />}>
I can navigate from other page to Parent.tsx and I can navigate from Parent.tsx to other pages. I don't have childFormDialog that will be displayed in the Material UI Dialog in the router because no page calls the childFormDialog through hyperlink or the menu link. The childFormDialog is called from the Parent.tsx file and it should displays in the Reusable Dialog Component on the same page.
The current behavior of the form when I click on the hyperlink is that it only shows a blank page and remain on the same URL without reloading/refreshing the page. The expected behavior is for the Material UI Dialog to display with the childFormDialog component displays in the Material UI Dialog.
I will appreciate your kind assistance
If you forgot to wrap your app in a BrowserRouter and attempt to instantiate the useNavigate hook, then your childFormDialog component would quietly fail and not render anything.

How to add Icon to MobileDatePicker in MUI v5

This is part of the code:
<MobileDatePicker
showTodayButton
showToolbar={false}
disableCloseOnSelect={false}
inputFormat="YYYY-MM-DD"
views={['day']}
value={row.value}
onChange={(newValue) => row.onChange(newValue)}
renderInput={(params) => (
<InputBase {...params} className={classes.datePicker} />
)}
/>
On the mobile side, he does not show trigger Icon.
How to display to give users a clear indication.
The MobileDatePicker doesn't have a suffix icon because you can open it by focusing the TextField unlike the DesktopDatePicker where you have to click the icon to open the picker. But if you still want to include the icon anyway, just add one in the endAdornment of the TextField:
import InputAdornment from '#mui/material/InputAdornment';
import EventIcon from '#mui/icons-material/Event';
const [value, setValue] = React.useState<Date | null>(new Date());
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<MobileDatePicker
label="For mobile"
value={value}
open={open}
onOpen={handleOpen}
onClose={handleClose}
onChange={setValue}
renderInput={(params) => (
<TextField
{...params}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton edge="end" onClick={handleOpen}>
<EventIcon />
</IconButton>
</InputAdornment>
),
}}
/>
)}
/>
);
Related answer
How to change the icon in MUI DatePicker?

Using React Hook Form with other component

I want to create dynamic form with react-hook-form.
Below is my code.
I want to create a dialog for entering a detailed profile in a separate component and use it in MainForm.
I can display the DetailForm, but the values entered in it are not reflected.
Data on the DetailForm component is not included when submitting.
Any guidance on how to do this would be greatly appreciated.
MainForm
import React from 'react';
import {
useForm,
Controller,
useFieldArray
} from 'react-hook-form';
import {
Button,
TextField,
List,
ListItem,
IconButton,
} from '#material-ui/core';
import DetailForm from '#components/DetailForm'
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
function MainForm(props:any) {
const { control, handleSubmit, getValues } = useForm({
mode: 'onBlur',
defaultValues: {
profiles: [
{
firstName: '',
lastName: '',
email: '',
phone: ''
}
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'profiles',
});
const onSubmit = () => {
const data = getValues();
console.log('data: ', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<List>
fields.map((item, index) => {
return (
<ListItem>
<Controller
name={`profiles.${index}.firstName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="First Name"
/>
}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="Last Name"
/>
}
/>
<DetailForm index={index} />
</ListItem>
)
})
</List>
<IconButton onClick={() => append({})}>
<AddCircleOutlineIcon />
</IconButton>
<Button
type='submit'
>
SAVE
</Button>
</form>
)
}
DetailForm
import React from 'react';
import {
Button,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '#material-ui/core';
export default function DetailForm(props: any) {
const [dialogState, setDialogState] = React.useState<boolean>(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button
onClick={handleOpen}
>
Set Detail Profile
</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<TextField
name={`profiles.${props.index}.email`}
label="Email Address"
/>
<TextField
name={`profiles.${props.index}.phone`}
label="Phone Number"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleClose}>
Add
</Button>
</DialogActions>
</Dialog>
</>
)
}
You have to register those fields inside your <Dialog /> component - just pass control to it. It's also important to set an inline defaultValue when using <Controller /> and useFieldArray. From the docs:
inline defaultValue is required when working with useFieldArray by integrating with the value from fields object.
One other minor thing: You should also pass the complete fieldId to your <Dialog /> component instead of just passing the index. This way it would be easier to change the fieldId in one place instead of editing all fields in your <Dialog /> component.
MainForm
<ListItem key={item.id}>
<Controller
name={`profiles.${index}.firstName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="First Name" />
)}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Last Name" />
)}
/>
<DetailForm control={control} fieldId={`profiles.${index}`} />
</ListItem>
DetailForm
export default function DetailForm({ control, fieldId }) {
const [dialogState, setDialogState] = React.useState(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button onClick={handleOpen}>Set Detail Profile</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<Controller
name={`${fieldId}.email`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Email Address" />
)}
/>
<Controller
name={`${fieldId}.phone`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Phone Number" />
)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>Add</Button>
</DialogActions>
</Dialog>
</>
);
}

onChange useState one character behind

I have a simple search component and I want to build an autocomplete filter, but my onChange event handler is one character behind.
If I type "tsla" into the search bar my value will be "tsl"
<TextField
className={classes.queryField}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<SearchIcon />
</SvgIcon>
</InputAdornment>
)
}}
onChange={event => {
setValue(event.target.value);
setAuto(
tickers
.filter(
f =>
JSON.stringify(f)
.toLowerCase()
.indexOf(value) !== -1
)
.slice(0, 10)
);
console.log(auto);
}}
value={value}
placeholder="Search for a stock"
variant="outlined"
/>
The issue is that you are sill using the old value variable when you call setAuto.
const Search = (props) => {
handleChange = (event) => {
const value = event.target.value
setValue(value);
setAuto(tickers.filter((ticker) => JSON.stringify(ticker).toLowerCase().indexOf(value) !== -1).slice(0, 10));
console.log(auto);
};
return (
<TextField
className={classes.queryField}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<SearchIcon />
</SvgIcon>
</InputAdornment>
),
}}
onChange={handleChange}
placeholder="Search for a stock"
value={value}
variant="outlined"
/>
);
};
The solution is to pass the new value object to both setValue and to setAuto.
handleChange = (event) => {
const value = event.target.value
setValue(value);
setAuto(tickers.filter((ticker) => JSON.stringify(ticker).toLowerCase().indexOf(value) !== -1).slice(0, 10));
console.log(auto);
};

How to clear the autocomplete's input in Material-ui after an onChange?

In the hooks version of material UI I can't seem to be able to clear the autocomplete after an onChange event:
// #flow
import React, { useRef, useState } from "react";
import "./Autocomplete.scss";
import AutocompleteUI from "#material-ui/lab/Autocomplete";
import TextField from "#material-ui/core/TextField";
function Autocomplete(props) {
const { options } = props;
const [value, setValue] = useState();
const container = useRef();
const input = useRef();
function onChange(event, newValue) {
if (!newValue) return;
props.onChange(newValue);
setValue(undefined);
input.current.value = "";
event.target.value = "";
}
function renderInput(params) {
return (
<TextField
inputRef={input}
{...params}
inputProps={{
...params.inputProps,
autoComplete: "disabled", // disable autocomplete and autofill
}}
margin="none"
fullWidth
/>
);
}
return (
<div className="Autocomplete-container">
{value}
<AutocompleteUI
ref={container}
options={options}
autoHightlight={true}
clearOnEscape={true}
autoSelect={true}
// freeSolo={true}
getOptionLabel={option => option.title}
renderInput={renderInput}
value={value}
onChange={onChange}
/>
</div>
);
}
export default Autocomplete;
Diving into the source code I've noticed the component uses useAutocomplete hook internally. However, neither setInputValue nor resetInputValue which live internally inside that hook are exposed outside. Is there a way to accomplish an input clear after an onChange?
You need to set the inputValue prop to your valueState and on onhange function just clear the valueState
<Autocomplete
inputValue={valueState}
onChange={(value, option) =>
{
setOptions([])
setValueState("")
}}
renderInput={params => (
<TextField
dir="rtl"
onChange={(event) =>
{
setValueState(event.target.value)
}}
{...params}
label="Search Patient"
variant="filled"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
I had the same issue and I solved it with this :
const [search, setSearch] = useState("");
...
<Autocomplete
id="grouped-demo"
options={tagsList}
getOptionLabel={(option) => option.tag}
onChange={(event, value) =>value ? setSearch(value.tag) : setSearch(event.target.value)}
style={{width: 700}}
renderInput={(params) => <TextField {...params} label="Search" variant="outlined"/>}
/>
I encountered a similar scenario using Autocomplete/Textfield in a Search/Nav bar. The value would always be left behind after using a history.push or other Router function in the onChange event. The trick is to set the inputValue = "" . Every time the component renders, the previous value will be removed. See below
<Autocomplete
{...defaultProps}
onChange={(event, value) => {
if(value)
router.history.push(`/summary`);
}}
filterOptions={filterOptions}
clearOnEscape={true}
inputValue=""
renderInput={params => <TextField {...params}
label="Quick Search" fullWidth
InputProps={{...params.InputProps,
'aria-label': 'description',
disableUnderline: true,
}}/>}
/>
yo! I'm pretty sure the Textfield component from material takes an "autoComplete" prop, and you can pass that the string "false". Also, it does not go in inputProps, try that out.
<Textfield autoComplete="false" />
I had the same issue and I solved it with this :
const [value, setValue] = useState(null);
Then you don't need to use refs.

Resources