5

In javascript input has change event that in case of type="number" occures when either the input looses its focus or user uses step up or step down functionality, but not on the normal typing.

document.querySelector("input").addEventListener("change", e => {
  console.log(e.type, e.target.value)
})
<input type="number" step="0.01"> <button>&nbsp;</button>

But when I'm using react, it replaces change event by input event which occurs on typing too:

function App() {
  const [val, setVal] = React.useState("")
  
  function onChange(e) {
    console.log(e.type, e.nativeEvent.type, e.target.value)
    setVal(e.target.value)
  }
  
  return <input type="number" step="0.01" value={val} onChange={onChange} />
}

ReactDOM.render(<App />, document.getElementById("app"))
<script crossorigin src="//unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="//unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<span id="app"></span> <button>&nbsp;</button>

I need to handle normal change event that does not occur on typing.
How can I do that in react?

PS: Button in the snippects is a place where you can tab to from the input.

Yes, the "when typing as it causes problems with editing value" part does help a bit, though it seems a code smell to me, maybe an XY problem. Can you share how typing in the input causes problems when using onChange in the normal React way?

I didn't want to fully refuse using it, I just want to use both.

Here is an example with a problem:

  • You can't use backspace in decimal part (except yeu entered more then 2 decimal digits)
  • If using delete in decimal part, zero is added and cursor jumps to the end
  • If current value is number is like 1.<cursor>20 and you type a digit, cursor jumps to the end
  • If current value is like <cursor>1000.00 an you press delete, the number becomes 0.00
  • If you try to write in an empty input, after pressing the first digit it becomes like 7.00<cursor> and it's very difficult to enter something like 7.23
  • I'ts difficult to erase all value from the input as it becomes 0.00 until you select it all
  • I think, there's much more issues

function App() {
  const [val, setVal] = React.useState("")
  
  function onChange(e) {
    console.log(e.type, e.nativeEvent.type, e.target.value)

    setVal(e.target.value == +e.target.valueAsNumber.toFixed(2)
      ? e.target.valueAsNumber.toFixed(2)
      : e.target.value
    )
  }
  
  return <input type="number" step="0.01" value={val} onChange={onChange} />
}

ReactDOM.render(<App />, document.getElementById("app"))
<script crossorigin src="//unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="//unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<span id="app"></span> <button>&nbsp;</button>

If I try to use onBlur fpr formatting, it becomes better, but:

  • The number is not formatted when you click step up or step down buttons or press up or down on the keybord with the same effect:

function App() {
  const [val, setVal] = React.useState("")
  
  function onChange(e) {
    console.log(e.type, e.nativeEvent.type, e.target.value)
    setVal(e.target.value)
  }
  
  function onBlur(e) {
    console.log(e.type, e.nativeEvent.type, e.target.value)
    if (e.target.value && e.target.value == +e.target.valueAsNumber.toFixed(2)) {
      setVal(e.target.valueAsNumber.toFixed(2))
    }
  }
  
  return <input type="number" step="0.01" value={val} onChange={onChange} onBlur={onBlur} />
}

ReactDOM.render(<App />, document.getElementById("app"))
<script crossorigin src="//unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="//unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<span id="app"></span> <button>&nbsp;</button>

So I want to use change that occures both on blur and steps.

Qwertiy
  • 19,681
  • 15
  • 61
  • 128
  • `onChange` just works differently in React than it does in vanilla JS. You could use the `onBlur` event and grab the input's value via a React ref. Is there a compelling reason not to just use the `onChange` handler though? You might find this [post](https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput) helpful. – Drew Reese Jan 28 '22 at 23:08
  • @DrewReese, `onBlur` doesn't occure on step up on step down. FF eve allows clicking them without moving a focus from another control. – Qwertiy Jan 28 '22 at 23:40
  • Admittedly 99.9% of my JS experience is in React, so React's `onChange` is the "normal" behavior. Perhaps if you describe what you are trying to accomplish, or explicitly what you want the behavior to be, it might be more clear what you are wanting to do in React. Is there an issue/problem with just using `onChange` handler? – Drew Reese Jan 28 '22 at 23:57
  • @DrewReese, seems like I've described exact enough: function should be called on blur or up/down and not when typing. If you want to know the function, that's it: `if (e.target.value && !isNaN(e.target.valueAsNumber)) e.target.value = e.target.valueAsNumber .toFixed(2)`. Don't need to call it when typing as it causes problems with editing value. Does it help you? – Qwertiy Jan 29 '22 at 00:11
  • Yes, the "when typing as it causes problems with editing value" part does help a bit, though it seems a code smell to me, maybe an XY problem. Can you share how typing in the input causes problems when using `onChange` in the normal React way? – Drew Reese Jan 29 '22 at 01:21
  • @DrewReese, ok? added full example. – Qwertiy Jan 29 '22 at 10:36

2 Answers2

1

Here's a way to intercept all the input types of interest, link to codesandbox -> https://codesandbox.io/s/summer-resonance-58n15?file=/src/App.js:

function App() {
  const [val, setVal] = React.useState("");

  function onChange(e) {
    if (e.nativeEvent.inputType === "insertText") {
      console.log("input");
      // Warning: Using undefined would also comprehend other events
      // that may be not specified right now in the documentation, 
      // but could be emitted, hence causing bugs.
      // Maybe a possible future improvement could be to specify
      // this particular inputType.
    } else if (e.nativeEvent.inputType === undefined) {
      if (e.target.value > val) {
        console.log("step up");
      } else {
        console.log("step down");
      }
    } else if (e.nativeEvent.inputType === "deleteContent") {
      console.log("deleting");
    }
    setVal(e.target.value);
  }

  return (
    <React.Fragment>
      <input type="number" step="0.1" value={val} onChange={onChange} />{" "}
      <button>&nbsp;</button>
    </React.Fragment>
  );
}
  • Very interesting way. – Qwertiy Jan 28 '22 at 23:40
  • Not sure if it's ok to rely on `inputType === undefined` when [the spec](https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes) says _"Other specifications may expand on this definition."_ – Qwertiy Jan 28 '22 at 23:49
  • Yes it's kind of funny that this specific inputType type doesn't exists. And of course it is not ok, this is an hacky solution, and I was about to write a comment on the ``` undefined ``` line about it, but StackOverflow went in maintenance mode. Using undefined would also comprehend other events that may be not specified right now in the documentation, but could be emitted, hence causing bugs. But good that maybe we found a possible future improvement of the specification. – Antonio Della Fortuna Jan 29 '22 at 11:21
1

You can simply create a ref of your input and create your eventListener as you do in Javascript :

import React from "react";
import { render } from "react-dom";

const App = () => {
  const inputRef = React.useRef();

  React.useEffect(() => {
    inputRef.current.addEventListener('change', (e) => console.log(e.target.value))
  })

  return (
    <div>
      <input ref={inputRef} type="number" />
    </div>
  );
};

render(<App />, document.getElementById("root"));
Quentin Grisel
  • 4,794
  • 1
  • 10
  • 15
  • 3
    You should probably also remove the event listener when the component is unmounted. – ksav Jan 28 '22 at 23:20
  • Listener should be removed and should not be updated on each render. And about the idea yes, that's possible, but I expected some better way. – Qwertiy Jan 28 '22 at 23:46
  • @Qwertiy Well I know by experience that managing events as antonio did in its answer can lead to weird behavior, there is always parameters we don't take into account, like a different keyboard to give one of many. I still prefer an already existing and secure solution than something I have no entire control over. Now, Antonio's answer works well and should be considered, it just depend on what's the purpose of your app. – Quentin Grisel Jan 29 '22 at 02:17
  • This solution will not work if I want to render my component in a controlled or a uncontrolled mode. – Manoj Kumar Nagaraj Nov 30 '22 at 04:58