Is there some way to instruct react-select to select an option on menu close or select blur, but only if it is the one created (not from default list)?
Context:
I have a list of e-mail addresses and want to allow user to select from the list or type new e-mail address and then hit Submit button. I do the select part with react-select's Creatable component and it works.
import CreatableSelect from 'react-select/creatable';
<CreatableSelect
options={options}
isMulti={true}
isSearchable={true}
name={'emailAddresses'}
hideSelectedOptions={true}
isValidNewOption={(inputValue) => validateEmail(inputValue)}
/>
But what happens to my users is that they type new e-mail address, do not understand they need to click the newly created option in dropdown menu and directly hit the Submit button of the form. Thus the menu closes because select's focus is stolen and form is submitted with no e-mail address selected.
I look for a way how can I select the created option before the menu is closed and the typed option disappears.
You can keep track of the inputValue and add the inputValue as a new option when the onMenuClose and onBlur callbacks are triggered.
Keep in mind that both onBlur and onMenuClose will fire if you click anywhere outside of the select area. onMenuClose can also fire alone without onBlur if you press Esc key so you will need to write additional logic to handle that extra edge case.
function MySelect() {
const [value, setValue] = React.useState([]);
const [inputValue, setInputValue] = React.useState("");
const isInputPreviouslyBlurred = React.useRef(false);
const createOptionFromInputValue = () => {
if (!inputValue) return;
setValue((v) => {
return [...(v ? v : []), { label: inputValue, value: inputValue }];
});
};
const onInputBlur = () => {
isInputPreviouslyBlurred.current = true;
createOptionFromInputValue();
};
const onMenuClose = () => {
if (!isInputPreviouslyBlurred.current) {
createOptionFromInputValue();
}
else {} // option's already been created from the input blur event. Skip.
isInputPreviouslyBlurred.current = false;
};
return (
<CreatableSelect
isMulti
value={value}
onChange={setValue}
inputValue={inputValue}
onInputChange={setInputValue}
options={options}
onMenuClose={onMenuClose}
onBlur={onInputBlur}
/>
);
}
Live Demo
Related
Description:
I have a button in a form. When the button is clicked, the form is submitted to a backend. While the request is in flight, I'd like the button to show a loading signal instead of the usual text. When the request is done, I'd like the form to disappear.
I can do this with useState for the different button forms, but my issue is that just before the form disappears, the text shows again. This is a small issue but it looks unpolished.
Issue:
When the button is clicked, the loading animation appears as intended. When the request finishes, I can see the text re-appear for a split second before the dialog disappears. I don't want the text to reappear until I reopen the dialog.
What I tried:
I currently change the loading state back to false after I call (and wait on) closing the dialog. I tried NOT doing that, but this causes the loading button to be there when I reopen the dialog, which is worse.
To correct that, I tried setting the value of the loading state to false on startup, as follows:
useEffect(() => {
setLoading(false);
}, []);
this had no effect, which leads me to believe that the component only gets mounted once, and that when I close it it doesn't actually get unmounted.
I tried using the LoadingButton component from material-ui/lab, but for a variety of typescript/react reasons it was just throwing errors left and right. Since it's an experimental package I decided to stay away from it.
Code:
Here is my component:
export const NewClientDialog = (props: INewClientDialogProps) => {
// open is a boolean that's true when the dialog is open, close is a function that closes the form dialog
const {open, close} = props;
const [fn, setFn] = useState("");
const [ln, setLn] = useState("");
const [email, setEmail] = useState("");
const [pn, setPn] = useState("");
const [newUser, setNewUser] = useState<INewClientProps>();
const [loading, setLoading] = useState(false);
// this is not optimal, but not the point of this post. It sets the object from the form inputs for the network request
useEffect(() => {
const newClient: INewClientProps = {
firstName: fn,
lastName: ln,
email: email,
phoneNumber: pn
}
setNewUser(newClient)
}, [fn, ln, email, pn])
// this gets triggered when a button is clicked
const onAddClient = () => {
// set the loading animation
setLoading(true)
// this calls the network API and returns a promise
createNewClient(newUser)
// once the promise is completed, close the dialog
.then(() => close())
// then set the loading back to false
.then(() => setLoading(false))
}
// this the button, if loading is true then it is a circular loading icon otherwise it's text
const AddClientButton = () => {
const contents = loading ? ><CircularProgress /> : <div>Create new client</div>
return (
<Button onClick={() => onAddClient()}>
{contents}
</Button>
);
}
return (
<div>
<Dialog onClose={close} open={open}>
<TextField label="First name" onChange={(value) => setFn(value.target.value)}/>
<TextField label="Last name" onChange={(value) => setLn(value.target.value)}/>
<TextField label="Email address" onChange={(value) => setEmail(value.target.value)}/>
<TextField label="Phone number" onChange={(value) => setPn(value.target.value)}/>
<AddClientButton />
</Dialog>
</div>
)
}
Just after writing it, I had an epiphany:
useEffect(() => {
setLoading(false);
}, [open]);
this does exactly what I want it to do - when the dialog is open, set loading to false. I removed the setLoading(false) call in onAddClient.
I will leave this up, hopefully it helps anyone who wants to create dynamic buttons in Material UI.
I am trying to handle with read-only on my input component. So basically, I have an input component and as default, it comes with read-only. What I am trying to do is when it is clicked inside of the input field, read-only comes false and it is editable. But I want read-only true again, only when it is clicked to the outside of the input box.
So here is my component:
const InputComponent = () => {
const [disabled, setDisabled] = useState(true);
function handleClick() {
if(disabled == true) {
setDisabled(false);
}
else {
//TODO: click outside the set readonly
}
}
return (
<Form.Control type="number" readOnly={disabled} onClick={handleClick}/>
);
};
So my logic is quite simple when, disabled is true that means read-only is true so when it is clicked inside, disabled turns false and it is being editable. But I couldnt do the rest. So I dont know how to make disabled false again when it is clicked outside.
Thanks for your help. And I am open more idea.
You can use onBlur to do that:
<Form.Control onBlur={() => {setDisabled(true)}} />
I am new to react and writing test case in react. I have two radio buttons (IPV4 and IPV6). I want to test that a user can select another radio button or not. I don't know which event listener to apply on radio button, is it change event or click event?
I broke down my problem into two parts: 1) I am checking whether I can select or deselect one radio button or not 2) I can select another radio button or not. I failed in my 1 part only so please help me. Here is my code and test case.
import React from 'react'
import constants from 'LookingGlass/constants'
import { Label, RadioButton } from 'LookingGlass/common/components'
export const componentTestId = 'SelectorIPVersion'
export default function SelectorIPVersion(props = {}) {
const { checkedVersion, onChange, disabled } = props
const { choices, groupName, label } = constants.SelectorIPVersion
const _onChange = (e) => {
onChange && onChange(e.target.value)
}
let selectorChoices = choices.map((c) => {
return (
<RadioButton
key={c.value}
id={c.value}
name={groupName}
value={c.value}
label={c.label}
checked={c.value === checkedVersion}
onChange={_onChange}
disabled={disabled}
/>
)
})
return (
<div data-testid={componentTestId}>
<Label text={label} />
{selectorChoices}
</div>
)
}
Here is my test case:
Here i am trying select and deselect the radio button but this is giving me error so how can I check that user can select another option or not?
const renderComponent = (props) => render(<SelectorIPVersion {...props} />)
test('Verify that user can select another version', () => {
const { getByRole, debug } = renderComponent({ checkedVersion: 'ipv4' })
const radio = getByRole('radio', { name: 'IPv4' })
expect(radio).toBeChecked()
debug(radio)
fireEvent.click(radio)
expect(radio).not.toBeChecked()
})
Output of Error:
Received element is checked:
<input class="hidden" id="ipv4" name="ipVersion" readonly="" tabindex="0" type="radio" value="ipv4" checked=""/>
When I debug the IPV4 radio button this is the output:
● Console
console.log node_modules/#testing-library/react/dist/pure.js:94
<input
checked=""
class="hidden"
id="ipv4"
name="ipVersion"
readonly=""
tabindex="0"
type="radio"
value="ipv4"
/>
And when I don't pass checkedVersion: 'ipv4' as props radio button is not checked.
Where am I wrong. Is it right or wrong?
Radio buttons are meant to be used in groups, so that when clicking on one of them deselects the currently selected one. To test that a radio button is deselected simply select the other one.
test('Verify that user can select another version', () => {
const { getByRole } = renderComponent({ checkedVersion: 'ipv4' })
const ipv4Radio = getByRole('radio', { name: 'IPv4' })
const ipv6Radio = getByRole('radio', { name: 'IPv6' })
expect(ipv4Radio).toBeChecked()
fireEvent.click(ipv6Radio)
expect(ipv4Radio).not.toBeChecked()
expect(ipv6Radio).toBeChecked()
})
However, as a test this doesn't bring much value since you're just testing that radio buttons work as expected (we already know they do). You should focus on testing your own logic around these buttons instead.
I have an <input> element, and I want to show content (div) below the input if it has focus.
Also, on every click on the input, I want to hide/show the div.
The problem is that if the input is not focused, than clicking the input will trigger both onClick and onFocus events, so onFocusHandler will run first so the menu will appear but immidately after that onClickHandler will run and hide it.
This is the code (that doesn't work):
import React from 'react';
const MyComp = props => {
/*
...
state: focused, showContent (booleans)
...
*/
const onFocusHandler = () => {
showContent(true);
setFocused(true);
}
const onClickHandler = () => {
if (focused) {
showContent(false);
} else {
showContent(true);
}
}
return (
<>
<input
onFocus={onFocusHandler}
onClick={onClickHandler}
/>
{
showContent &&
<div>MyContent</div>
}
</>
);
};
export default MyComp;
How can I solve this issue?
This is a very odd desired behavior as an active toggle is rather opposed to an element being focused or not. At first I couldn't see any clear way to achieve the behavior you desire. But I thought it could be achieved.
Use the onMouseDown handler instead of the onClick handler.
Use onFocus handler to toggle on the extra content.
Use onBlur handler to toggle off the extra content.
Code:
const MyComp = (props) => {
const [showContent, setShowContent] = useState(false);
const onFocusHandler = () => {
setShowContent(true);
};
const onBlurHandler = () => {
setShowContent(false);
};
const onClickHandler = () => {
setShowContent((show) => !show);
};
return (
<>
<input
onBlur={onBlurHandler}
onFocus={onFocusHandler}
onMouseDown={onClickHandler}
/>
{showContent && <div>MyContent</div>}
</>
);
};
Note: It should be noted that onMouseDown isn't a true replacement to onClick. With a click event the mouse down and up have to occur in the same element, so if. user accidentally clicks into the input but then drags out and releases, the onClick event should not fire. This may be ok though since the focus event fires before the click event so the input may get focus and toggle the content on anyway. Maybe this quirk is acceptable for your use-case.
You should you onBlur event to setContent to false. There is no need to use the onClick handler for that.
I am unable to figure out how I could get pre-populated value from disabled input field. Since the field is disabled onChange won't apply here. Even with inputRef property of TextField, I am able to trigger a function called getNameInputValue and can see the values via console. But when I try to set the state to a function coming via props, I get null errors for value.
Here's the relevant code snippet:
const getNameInputValue = (id, nameValue) => {
console.log(id, nameValue.value); //works
props.getNameValues({ ...nameValues, [id]: nameValue.value }) //doesnt work
}
return (
<Box>
<Typography>NAME</Typography>
<TextField
id={`name-${index}`}
disabled
defaultValue={nameValueComingFromTheLoop}
variant="outlined"
inputRef={(inputValue) => getNameInputValue(`name-${index}`,inputValue)}
/>
</Box>
);
As requested getNameValues is defined in an outer component, here is its definition:
const [nameValue, setNameValue] = useState({});
const getNameValues= (receivedNameValuesObj) => {
setNameValue(receivedNameValuesObj);
};