I'm working on a form that initially only shows one input field and when it is focused, it shows other inputs and the submit button.
I also want to hide all those extra fields if the form loses focus while they are empty. And this is the part that I'm not being able to implement.
This is my code: I use a controlled form and a state to handle focus.
const FoldableForm = () => {
const [formState, setFormState] = useState(defaultFormState);
const [hasFocus, setFocus] = useState(false);
const handleOnBlur = () => {
if (!formState.message.trim() && !formState.other_input.trim()) {
setFocus(false);
}
};
return (
<form
onFocus={() => setFocus(true)}
onBlur={handleOnBlur}
>
<textarea
name="message"
onChange={(e) => setFormState({ ...formState, message: e.target.value })}
/>
{hasFocus && (
<>
<input
type="text" name="other_input"
onChange={(e) => setFormState({ ...formState, message: e.target.other_input })}
/>
<button type="button">Post comment</button>
</>
)}
</form>
);
}
Currently, if I type something in the text area, setFocus(false)
is never invoked, so it works as intended.
Otherwise, if I leave it empty and click on the other input field, the handleOnBlur
function is called, it sets focus to false, so the form is 'minimized'.
This is expected because the blur event (from the textarea) is triggered before the focus event (from the new input field). So I tried to use setTimeout to check, after a fraction of a second if the focus event had already occurred.
To do so, I used a second state (shouldShow) that is updated in a setTimeout inside the handleOnBlue function.
setTimeout(() => {
if(!hasFocus) {
setShouldShow(false); // this should cause the form to minimize
}
}, 100);
However, according to the react lifecycle, the value of hasFocus that is passed to the setTimeout function is at the invocation time, not at execution. So setTimeout here is useless.
I also tried to use references, but I couldn't make it work.
CodePudding user response:
This behavior is because of closures in JavaScript. The value of hasFocus
is not the value of the variable at the moment your callback inside setTimeout
is executed. It's the value when the onBlur
callback is executed.
One solution would be to use functional updates.
Define a state which holds both hasFocus
and shouldShow
inside:
const [state, setState] = useState({ hasFocus: false, shouldShow: false });
When you try to access the previous state using functional updates, you get the most recent value:
setTimeout(() => {
setState((state) => {
if (!state.hasFocus) {
return { ...state, shouldShow: false };
}
return state;
});
}, 100);
Another solution would be to debounce a function which sets the hasFocus
state to false, which imo is way better.
CodePudding user response:
In your case i think that the usage of the shouldShow
state is redundant and you can also avoid using a timeout which may lead to bugs.
You can take advantage of the FocusEvent.relatedTarget attribute and prevent hiding the extra fields when blur from an input and focus to another happens simultaneously.
The handleOnBlur
function should look like this:
const handleOnBlur = (e) => {
if (e.relatedTarget && e.relatedTarget.name === "other_input") return;
if (!formState.message.trim() && !formState.other_input.trim()) {
setFocus(false);
}
};
You can find a working example in this code sandbox.
The problem with this approach is that if you have multiple fields appearing you need to check if any of those is focused like below:
["other_input", "another_input"].includes(e.relatedTarget.name)