Is there a way to clear a MUI TextField Input from the form.restart() method in React-Final-Form and capture the event? - reactjs

The form.restart() in my reset button resets all fields states and values as per my understanding of this Final-Form.
The method fires and resets all fields in my form and I can capture the event in the autocomplete, but I am unable to capture the clear event in the textfield - I have a state (not related to the value of the field) I need tor reset.
My form reset button
<Button
type={"button"}
disabled={submitting || pristine}
variant={"outlined"}
onClick={() => {
form.getRegisteredFields().forEach((field) => form.resetFieldState(field));
form.restart();
if (clearActionHandler) {
clearActionHandler();
}
setFormSubmittedOnce(false);
}}
>
Clear
</Button>;
My textfieldadapter
const [shrink, setShrink] = useState < boolean > false;
const countCharacters: (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => boolean = (e) => {
setCount(e.target.value.length);
return maxCharacterCount === 0 || e.target.value.length < maxCharacterCount;
};
return (
<TextField
{...input}
{...rest}
onChange={(e) => {
if (countCharacters(e)) {
input.onChange(e);
}
}}
value={input.value}
onBlur={(e) => {
!input.value && input.onBlur(e);
!input.value && setShrink(false);
}}
error={meta.error && meta.touched}
helperText={
meta.touched ? (
<React.Fragment>
{maxCharacterCount > 0 ? (
<React.Fragment>
<Typography variant={"body1"} textAlign={"end"}>
{count}/{maxCharacterCount}
</Typography>
<br />
</React.Fragment>
) : null}{" "}
{meta.error}
</React.Fragment>
) : maxCharacterCount > 0 ? (
<React.Fragment>
<Typography variant={"body1"} textAlign={"end"}>
{count}/{maxCharacterCount}
</Typography>
<br />
</React.Fragment>
) : (
""
)
}
placeholder={placeholder}
fullWidth={true}
margin={"dense"}
multiline={multiline > 1}
rows={multiline}
inputProps={inputProps}
InputProps={{
startAdornment: (
<InputAdornment position={"start"} sx={{ width: "24px" }}>
{startAdornment}
</InputAdornment>
),
}}
InputLabelProps={{
shrink: shrink,
}}
onFocus={() => setShrink(true)}
sx={{
"& .MuiInputLabel-root:not(.MuiInputLabel-shrink)": {
transform: "translate(50px, 17px)",
},
}}
/>
);
Versions of packages:
"#mui/material": "^5.11.1",
"react": "^18.2.0",
"react-final-form": "^6.5.9"
I have tried to capture the onChange event with bluring all elements before the reset method is called, that doesn't call the textfield onblur method. I am just not sure how to clear it away.

Make sure that the field in question has an entry in initialValues and that that entry is initialised to an empty string. This is a long shot, so take everything I say as theory, but it looks like what would happen if the initial value was not defined or was explicitly set to undefined.
That would mean when you reset the form, the value prop of the TextField would be set to undefined. This is not a valid controlled value of a TextField, so the behavior of MUI could well be to switch to uncontrolled mode, where the value is kept internally and not actually controlled by the value prop anymore. It depends on the library what happens next as this is generally unexpected and leads to non-deterministic behavior, but it's likely that MUI just keeps the previous controlled value around internally, or it's sitting in a transient DOM state on the element itself since value is judged to be no longer the source of truth and so it was never written to the actual DOM element. When the user types again, onChange would be called, the state in your form set to a string, and it would become controlled again. Base react <input> gives warnings in the console when you transition like this but MUI might not.
This might explain why it does not capture the transition to the original form state.
Note null is usually not allowed either but there's some final form magic that protects you from that one.
I verified MUI does this on a codesandbox (excuse the old class style I just used an old TextField sandbox to build on). If my long shot is correct, the problem is more related to a core issue between your state consistency and the mui lib and less about final form.
The fix would be to make sure the field has an entry in initialValues set to a string. You could optionally put in guards to check for undefined and use '' instead when that's the case.
Take note that '' is actually still falsey so !input.value would evaluate to true when its empty string still. Not sure you care, but something to keep in mind.

Related

Material UI - Array is resetting to 0 elements after onChange event. (TextField component)

Im currently building a application where i want people to choose from a list of users who they want to create a group chatroom with. After that, i have a TextField that allows them to set a name to their group.
The problem: The onChange event for the textfield seems to reset the array to 0 elements when typing. See GIF below.
Have anyone encountered something like this? I will paste my relevant code snippet down below.
return (
<div>
<Dialog
open={MultiplelistClick}
keepMounted
className='grpchatDialog'
onClose={() => setMultiplelistClick(false)}
aria-describedby="alert-dialog-slide-description"
fullWidth
maxWidth="xl"
>
<DialogTitle className='dialogTitle'>{"Choose users"}</DialogTitle>
<DialogContent>
<DialogContentText className="dialog-information">
2 minimum users required here.
</DialogContentText>
</DialogContent>
{
userData && userData.filter((val: any) => {
return (searchTerm === '' && (val.id != myIdNumber) || (val.firstName.toLowerCase().includes(searchTerm.toLocaleLowerCase()) && (val.id != myIdNumber)));
}).map((user: UserModel) => {
return <li key={user.id}>
<img className='liAvatar' src={placeholderimg} alt='xx' />
<p className='nameDisplay'onClick={() => createChatRoom(user)}> {user.firstName} {user.lastName}</p>
<Checkbox value={user.id} onClick={() => CheckBoxEvent(user.id)} className='myCheckBox' />
</li>
})
}
<TextField
label='Give your group a name.'
variant='outlined'
rows={1}
className='grpNameInput'
onChange={(e) => setGroupName(e.target.value)}
/>
<DialogActions>
<Button onClick={() => CreateGroupChatRoom(GroupChatList)}>Skapa Chattrum</Button>
</DialogActions>
</Dialog>
</div>
UPDATE: I verified it is actually the onChange typing event in my TextField that resets my current array of user ids to 0. See GIF here: https://gyazo.com/f306b6907ca755ad07fa13b542b87d27
For anyone wondering how my event for pushing in the user ids into the array, heres the event for that:
const CheckBoxEvent = (userid: number) => {
if (!GroupChatList.includes(userid)) {
GroupChatList.push(userid); console.log(GroupChatList)
}
else {
var index = GroupChatList.indexOf(userid);
GroupChatList.splice(index, 1);
}
}
React re-renders components frequently. By re-render, especially with Functional Components, we mean: "run the Function"...which re-creates the declared variables and functions within each and every time. So when the component re-renders, GroupChatList is a new variable reset to whatever the code within the component says.
To maintain a value in a variable across renders, use the useState hook to put the variable into "state".
Also, when you use the "set state function" to update the value in the state variable, that is the trigger to React that it needs to re-render the component (because "information in state has changed").

How can I clear MUI DatePicker input?

I would like to add a 'clear' button to a DatePicker from #mui/lab (5.0.0-alpha.55).
What I am attempting:
Store a date field in state, passed as the value prop to DatePicker
Change the date to null when desired to 'clear' the value & input
The behaviour I observe:
If the date is valid, it works as expected, and the input is cleared
If the date is not valid, the error is cleared, but the invalid date still stays in the input.
The rudimentary version I have attempted which shows the behaviour above can be seen here.
If you input a partial date, then click clear, you can observe that the input does not get cleared.
I would prefer to avoid a solution that involves changing the key, as that brings other issues, such as not respecting an external source changing the date to null, and needing additional hacks to respect the label transition when clearing the input.
My theory is that internally, DatePicker only updates the input value if it's different with the last valid value. Below is how the bug can occur:
You clear the DatePicker when there is a valid value (like the initial Date), the state is reset successfully at first (value=null, inputValue='')
You enter some invalid date. The state is now (value=Invalid Date, inputValue='invalid Text')
Because the current value is invalid, it does not count and the DatePicker references the last valid value instead which is null, then decide that the current value doesn't change and skip dispatching the new input value (value=null, inputValue='invalid Text').
I'd classify this as a bug from MUI, you can create an issue on github if you want it to be fixed. In the meanwhile, you can fix the bug by applying this patch using patch-package:
Install patch-package: npm i patch-package
Add postinstall script in the package.json
"scripts": {
"postinstall": "patch-package"
}
Open the buggy file at node_modules\#mui\lab\internal\pickers\hooks\useMaskedInput.js and change it based on this commit.
Run npx patch-package #mui/lab to create the patch that will be applied every time after you ran npm i #mui/lab again.
Restart your dev server.
As of today you could do:
<DatePicker
componentsProps={{
actionBar: {
actions: ['clear'],
},
}}
onAccept={(newDate) => {
console.log(newDate);
}}
/>
and see if newDate is null.
MUI DatePicker has a clearable property. When set to true, it shows a clear button. You can change the text of the clear button using the clearText property.
I would point out however, that it does not trigger an onChange event, nor is there an onClear event (at the time of writing this post). So when I am using it, its not possible to change the state that way. However the onAccept event is fired, and if you pass the value parameter, it will be null.
<MobileDatePicker
open
clearable
label="some label"
inputFormat="yyyy-MM-dd"
value={stateDate}
onChange={(date) => updateAvailableDate(date)}
renderInput={(params) => (
<TextField
{...params}
/>
)}
onClose={() => hideModal()}
clearText="Clear me"
onAccept={(value) => handleSave(value)}
/>
See all the api settings here.
https://mui.com/api/date-picker/
how about this custom component?.
const DatePickerClear = (props) => {
const { onClear } = props;
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
{...props}
renderInput={(params) => (
<div style={{ position: "relative", display: "inline-block" }} >
<TextField {...params} />
{props.value &&
<IconButton style={{ position: "absolute", top: ".5rem", margin: "auto", right: "2rem" }} onClick={() => onClear()}>
<ClearIcon />
</IconButton>
}
</div>
)
}
/></LocalizationProvider >
)
}
export default DatePickerClear
I give you too a github basic project in react.
https://github.com/HastaCs/MUI_DatePicker_ClearButton
This is what I did to make the clear function work. I recently found this solution so I might change this in the future... but for now it's fine.
<MobileDateTimePicker
...
clearable
clearText="Clear"
onChange={(newScheduleDateTime) => {
if (newScheduleDateTime) {
setScheduleDateTime(newScheduleDateTime);
} else {
setScheduleDateTime(null);
}
}}
/>

MUI & Formik: Validation doesn't trigger for useField

Codesandbox with minimal working example: https://codesandbox.io/s/mystifying-lake-m0oxc?file=/src/DemoForm.tsx
Essentially, I have a AutoComplete dropdown which is tied into a Formik form with the useField hook. This sets the value correctly on any change, but the validation doesn't seem to trigger when I expect it to.
Validation runs successfully and removes the error if:
Another field is changed
I click on the "background" after selecting a value in the dropdown
What I expected and wanted was that the validation should run immediately when a value is selected.
To reproduce:
Click the category dropdown
Select a value, which closes the dropdown
Confirm that the error is still indicated (on the field and in the printout)
Click elsewhere on the screen. This should trigger validation and clear the error.
Any suggestions?
Edit:
I've tested an equivalent implementation with react-select as the dropdown and had the same issue, so I don't think it's directly tied to MUI.
I just reproduced based on the working example what you provided and realized that you set helpers.setTouched manually.
Just don't overuse the setTouched and also you need to handle if you remove the selected item from the autocomplete.
<Autocomplete
disablePortal
id="category-selector"
options={options}
onBlur={() => helpers.setTouched(true, true)}
isOptionEqualToValue={(option, value) => option.id === value.id}
onOpen={() => helpers.setTouched(true, true)}
onChange={(_, option) => {
if (option) {
helpers.setValue(option.id);
} else {
helpers.setValue(0);
}
}}
renderInput={(params) => (
<TextField
{...params}
label="Category"
error={meta.touched && Boolean(meta.error)}
/>
)}
/>

Material-ui autocomplete clear value

I have one problem in my react code.
I use Material-ui and redux-form. I have select input like and after change this select i should reset value in . I use action 'change' from react-form and set value for textfield. But label in still remains. Can i clear or reset value in ?
<Autocomplete
options={list}
getOptionLabel={option => option.name}
onInputChange={onChange}
onChange={onChangeAutoComplete}
noOptionsText='Нет доступных вариантов'
loadingText='Загрузка...'
openText='Открыть'
renderInput={params => (
<Field
{...params}
label={label}
name={fieldName}
variant="outlined"
fullWidth
component={renderTextField}
className={classes.textField}
margin="normal"
/>
)}
/>
Using hooks on the value prop breaks the functionality of the autocomplete component ( at least for me ). Using class, and setting the local state is the same.
Luckily it is a react component, so it have a "key" prop. When the key prop changes, the component is re-rendered with the default values ( which is an empty array since nothing is selected). I used hooks in the parent component and passed the values to the key prop, whenever reset is needed.
<Autocomplete
key={somethingMeaningful} // Bool, or whatever just change it to re-render the component
//...other props
/>
Hope this helps!
Material UI Autocomplete onInputChange callback provides reason argument. If input has been changed by input, reason will be input and if you selected option then reason will be reset.
onInputChange={(event, newInputValue, reason) => {
if (reason === 'reset') {
setValue('')
return
} else {
setValue(newInputValue)
}
}}
setValue is useState and you can pass value state to autocomplete value property.
use value in your <Autocomplete /> like this:
<Autocomplete
value={this.state.value} //insert your state key here
//...other props
/>
Then clear state of that key, to clear the autocomplete field value
I am going to post a very dirty way of clearing the value of Autocomplete. Try it ONLY when nothing else works;
import React, { useRef } from 'react';
...
const autoC = useRef(null);
...
<Autocomplete
...
ref={autoC}
/>
and then when you want to clear the value;
const ele = autoC.current.getElementsByClassName('MuiAutocomplete-clearIndicator')[0];
if (ele) ele.click();
This is what worked for me.
const [name, setName] = useState('');
<Autocomplete
inputValue={name}
onChange={(e,v)=>setName(v?.name||v)}
...
/>
<Button onClick={()=>setName('')}>
Clear
</Button>
You can use something like the following to clear the autocomplete field when an item is selected.
<Autocomplete
value={null}
blurOnSelect={true} />
Note that you may also need to set clearOnBlur={true} if you're using the freeSolo option.
Source https://mui.com/api/autocomplete/#props
I achieved this by updating the inputValue prop where multiple prop is false. If you are using multiple prop, then there is a propblem (bug). Selected values does not get erased.
When I encountered this, it was when options for the autocomplete changed, and wanted to clear the input value. It wouldn't clear with just the options changing. What worked for me is adding a key value onto the autocomplete which depended on the change which necessitated clearing.
To solve this, I created a hook that watches the value state of the autocomplete and set the value of the input if the checkClear returns true;
function useAutocompleteInputClear(watch, checkClear) {
const elmRef = useRef(null);
useMemo(() => {
if (!elmRef || !elmRef.current) return;
if (!checkClear || typeof checkClear !== "function") return;
const button = elmRef.current.querySelector("button")
if (checkClear(watch) && button) {
button.click();
}
}, [watch])
return elmRef;
}
Its first argument is the value that should be watched and its second argument is a function that returns a boolean. if it is true the clearing will happen.
Also, the hook returns a ref that needs to pass as ref prop to Autocomplete.
const elmRef = useAutocompleteInputClear(value, v => !v || !v.id)
<Autocomplete ref={elmRef}
value={value}
...
using onChange property we can clear the value by clicking the clear icon in the following way
<Autocomplete
fullWidth={true}
label={'Source'}
margin={'noraml'}
multiple={false}
name={'Source'}
getOptionSelected={useCallback((option, value) => option.value === value.value)}
ref={SourceRef}
value={formValues.Source === '' ? {label: ''} : {label: formValues.Source}}
options={SourceStatus}
onChange={useCallback((e, v) => {
if (typeof v === 'object' && v !== null) {
handleInputChange(e, v) // help to set the value
} else {
handleInputChange(e, {label: ''}) // help to reset the value
}
})}
/>
In my case for multiselect freeSolo onChange props 3rd argument reason solved my all issues.
AutocompleteChangeReason can be:
blur
clear
createOption
removeOption
selectOption
and 2nd arg of this props gives u already updated list of (multiselect) value/s.
onChange={(_event, newOptions, reason) => {
setOptions(
reason === 'clear' ? [] : [...newOptions.map((o) => Number(o))],
);
}}
If you need only the selected value, set the value to an empty object and render the option to your needs.
<Autocomplete
value={{}}
onChange={handleSelectionChanged}
options={options ?? []}
getOptionLabel={x => (!x ? '' : x?.name ?? '')}
renderInput={params => <TextField {...params} label="" />}
/>
If you are using objects, you can use the following code to clear the field.
Ensure you add isOptionEqualToValue:
<Autocomplete
style={{ width: 250 }}
multiple
id="checkboxes-tags-demo"
options={list}
isOptionEqualToValue={(option, newValue) => {
return option.id === newValue.id;
}}
value={selected}
onChange={(e, val) => handleSelected(e, val)}
getOptionLabel={(option) => option.name}
renderInput={(params) => (
<TextField
{...params}
label="Add to Multiple"
placeholder="Favorites" />
)} />
Just set an empty array in your state through functions, and it'll be cleared.
Try this method:
use onChange method and pass third parameter reason and compare to clear text if reason is clear then executed this function.
<Autocomplete
onChange={(event, newValue, reason) => {
if (reason === 'clear') {
console.log("Put your clear logic here: this condition executed when clear button clicked")
setValue({ title: '', year: '' }) //for reset the value
return
}
}}
/>
One easy way to do this is to pass these props to autocomplete like this:
onChange={handleSkillChange}
inputValue=''
clearOnBlur={true}
onChange is an event handler, which stores the value in the state.
inputValue='' helps to ensure that the text field inside autocomplete will always be empty
clearOnBlur={true} helps to clear the value of the autocomplete component when it loses focus.

How to I keep a Material-ui Select open when I click on only one of the items in it

I have been writing a custom Material-UI Select dropdown which has an optional text field at the top to allow the user to search / filter items in the Select if there were many entries.
I am struggling with how to keep the Select open when I click on the text field (rendered as an InputBase) and just have the normal behavior (of closing the Select when a regular MenuItem is selected.
CodeSandbox here : https://codesandbox.io/s/inspiring-newton-9qsyf
const searchField: TextField = props.searchable ? (
<InputBase
className={styles.searchBar}
onClick={(event: Event) => {
event.stopPropagation();
event.preventDefault();
}}
endAdornment={
<InputAdornment position="end">
<Search />
</InputAdornment>
}
/>
) : null;
return (
<FormControl>
<Select
className={styles.root}
input={<InputBase onClick={(): void => setIconOpen(!iconOpen)} />}
onBlur={(): void => setIconOpen(false)}
IconComponent={iconOpen ? ExpandMore : ExpandLess}
{...props}
>
{searchField}
{dropdownElements.map(
(currEntry: string): HTMLOptionElement => (
<MenuItem key={currEntry} value={currEntry}>
{currEntry}
</MenuItem>
)
)}
</Select>
</FormControl>
);
As you can see above I've tried using stopPropagation and preventDefault but to no avail.
check out this codesandbox link: https://codesandbox.io/s/busy-paper-9pdnu
You can use open prop of Select API
I was able to make a controlled open select by providing open prop as a react state variable and implementing correct event handlers. To make it controlled you must provide onOpen and onClose props of the Select and make sure the open prop stays true when the custom textfield is clicked.
One more important thing I had to do was override the default keyDown behavior of the Select component. If you open up a Select and start typing into it, it shifts focus to the select option that matches what you are typing. For example, if you Select had an option with the text Foobar and if you start typing Food and water, it would cause focus to shift from your custom text input onto the Foobar option. This behavior is overridden in the onKeyDown handler of the custom textfield
Working sandbox here
Edit: even though this worked in the codepen, I had to add onChange={handleOpen} to the Select as well to get it working on a real browser with React and Next.
You can still use stopPropagation to make it work
// open state
const [isSelectorOpen, setisSelectorOpen] = useState(false)
// handle change
const handleChange = event => {
const { value } = event.target
event.stopPropagation()
// set your value
}
// selector
<Select
multiple
open={isSelectorOpen}
onChange={handleChange}
input={(
<Input
onClick={() => setisSelectorOpen(!isSelectorOpen)}
/>
)}
// other attribute
>
<MenuItem>a</MenuItem>
<MenuItem>b</MenuItem>
<MenuItem>c</MenuItem>
</Select>
In my case, none of the above worked, but this did the trick to stop closing the Select:
<MenuItem
onClickCapture={(e) => {
e.stopPropagation();
}}>
You can also change onMouseEnter to change some default styling that comes with using MenuItem (like pointer cursor when mouse enters its layout)
onMouseEnter={(e) => {
e.target.style.backgroundColor = "#ffffff";
e.target.style.cursor = "default";
}}
In my case i also needed to remove the grey clicking effect that MenuItem makes on click, which is a new object generated in MuiTouchRipple-root, so changing display to none did the trick.
sx={{
"& .MuiTouchRipple-root": {
display: "none",
},
}}

Resources