JavaScript replace only executes on complete match of the regex when using groups

Viewed 70

I'm trying to create an input mask with the following pattern: "4.01.05.01-6". In the onchange method I execute a function that uses the replace method:

value = value
.replace(/\D/g, '')
.replace(
/^(\d{1})(\d{2})(\d{2})(\d{2})(\d{1})/,
'$1.$2.$3.$4-$5'
)

The problem is that I only receive the masked input value after the regex is fully matched, when I have the 8 digits. That's not what I want. I'd like to type in the input and the mask will be working while I'm typing, for example, if I just typed "1234" I'd like to have the value "1.23.4"

OBS: I don't the following info matters, but I'm using React with a controlled Input, this variable called "value" is used to make the setState.

2 Answers

Make all parts of the regex but the first one optional using an optional non-capturing group. Then, you can check if each optional group matched in a callback function used as the replacement argument, and build the replacement string dynamically.

See the JavaScript demo:

function App() {
  const [Version, setVersion] = React.useState('')
  function maskVersion({ target: { value } }) {
    const re  = /^(\d)(?:(\d{1,2})(?:(\d{1,2})(?:(\d{1,2})(\d)?)?)?)?[\w\W]*$/
    setVersion(value.replace(/\D/g,'').replace(re, (_,a,b,c,d,e) =>  
       a + 
        ( b ? `.${b}` : "") + 
         ( c ? `.${c}` : "") + 
          ( d ? `.${d}` : "") + 
           ( e ? `-${e}` : "") ))
  }
  return  <div>
      Text: <input type='text' onChange={maskVersion} value={Version} />
  </div>
}

ReactDOM.render(<App/>, document.querySelector('#root'))
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

The regex - see its online demo - matches:

  • ^ - start of string
  • (\d) - Capturing group 1 (a): one digit
  • (?:(\d{1,2})(?:(\d{1,2})(?:(\d{1,2})(\d)?)?)?)? - an optional non-capturing group matching
    • (\d{1,2}) - Capturing group 2 (b): one or two digits
    • (?:(\d{1,2})(?:(\d{1,2})(\d)?)?)? - an optional non-capturing group matching
      • (\d{1,2}) - Capturing group 3 (c): one or two digits
      • (?:(\d{1,2})(\d)?)? - an optional non-capturing group matching
        • (\d{1,2}) - Capturing group 4 (d): one or two digits
        • (\d)? - an optional capturing group 5 (e): on digit
  • [\w\W]* - any zero or more chars (to truncate the input string if more chars are typed)
  • $ - end of string.

The replacement is built using the ternary operator, if b is matched, a dot plus b value is appended, if c or d is matched, the same happens, and if there is e group match, a - and the e group value is added.

I can't think of any way doing it with regex only. I think you should make cases with the length of the input so when the input is, lets say, 3 digits long, then run the replace for these digits only like this

if(value.length >= 2 && value.length <= 3){
value.replace(
/^(\d{1})(\d{1,2})/,
'$1.$2'
)
}

Do the same for cases of digit length 4 - 5 etc.

So the whole code could be like this

if(value.length >= 2 && value.length <= 3){
value.replace(
/^(\d{1})(\d{1,2})/,
'$1.$2'
)
}else if(value.length >= 4 && value.length <= 5){
value.replace(
/^(\d{1})(\d{2})(\d{1,2})/,
'$1.$2.$3'
)
}else if(value.length >= 6 && value.length <= 7){
value.replace(
/^(\d{1})(\d{2})(\d{2})(\d{1,2})/,
'$1.$2.$3.$4'
)
}else if(value.length == 8){
value.replace(
/^(\d{1})(\d{2})(\d{2})(\d{2})(\d{1})/,
'$1.$2.$3.$4-$5'
)
}

Maybe i have some errors in the above code but i think you got the idea

Related