skip to Main Content

For example, if I input 123456789 and leave focus the input will change to *****6789

I have a working example but I am unsure if this is without potential bugs. Is there a better way?

I came to this through trial and error

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [hovered, setHovered] = useState(false);
  const [testInput, setTestInput] = useState("");
  const showLastFourDigits = (value) => {
    if (value.length == 9) {
      const result = "*".repeat(value.length - 4) + value.slice(-4);
      return result;
    } else {
      return value;
    }
  };

  const onSubmit = (e) => {
    e.preventDefault();
    console.log(testInput);
  };

  return (
    <form className="App">
      <input
        value={!hovered ? showLastFourDigits(testInput) : testInput}
        onChange={(e) => setTestInput(e.target.value)}
        onFocus={() => setHovered(true)}
        onBlur={() => setHovered(false)}
        maxLength={9}
      />
      <button onClick={onSubmit}>Submit</button>
      <p>testInput:{testInput}</p>
    </form>
  );
}

Code sandbox

As you could see. If I hover it, it shows the full input. If I unhover it, it becomes *****6789. For reason I’m not sure why but onChange doesn’t run if I do it this way. Is this secure? I’m concerned whether if i click onsubmit it might show *****6789 instead of 123456789. Is this the recommended way to do this?

3

Answers


  1. It sounds like you want to mask and unmask in response to two criteria: element focus and hover state… and it appears as though you already figured out the focus and blur logic. Great!

    JS doesn’t have a "hover" event, but a close approximation can be implemented using the events mouseenter and mouseleave. The logic for mouseenter and mouseleave is similar to focus and blur respectively, except that they should only operate if the element is not focused.

    Whether the element has focus can be determined in JS by comparing the element to document.activeElement. Here’s a minimal, reproducible example:

    <style>body { font-family: sans-serif; }</style>
    <div id="root"></div><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js"></script><script src="https://cdn.jsdelivr.net/npm/@babel/[email protected]/babel.min.js"></script>
    <script type="text/babel" data-type="module" data-presets="env,react">
    
    // This Stack Overflow snippet demo uses UMD modules
    const { StrictMode, useRef, useState } = React;
    
    function maskAllExceptFinal(str, options = {}) {
      const { maskToken = "*", revealCount = 4 } = options;
      return str.length <= revealCount
        ? str
        : `${maskToken.repeat(str.length - revealCount)}${str.slice(-4)}`;
    }
    
    function App() {
      const [masked, setMasked] = useState(true);
      const [testInput, setTestInput] = useState("");
      const ref = useRef(null);
    
      const handleSubmit = (ev) => {
        ev.preventDefault();
        console.log(testInput);
      };
    
      const handleMask = (ev) => {
        const isFocused = document.activeElement === ref.current;
        switch (ev.type) {
          case "blur": {
            setMasked(true);
            break;
          }
          case "focus": {
            setMasked(false);
            break;
          }
          case "mouseenter": {
            if (!isFocused) setMasked(false);
            break;
          }
          case "mouseleave": {
            if (!isFocused) setMasked(true);
            break;
          }
        }
      };
    
      return (
        <form>
          <input
            onBlur={handleMask}
            onChange={(ev) => setTestInput(ev.target.value)}
            onFocus={handleMask}
            onMouseEnter={handleMask}
            onMouseLeave={handleMask}
            maxLength={9}
            ref={ref}
            value={masked ? maskAllExceptFinal(testInput) : testInput}
          />
          <button onClick={handleSubmit}>Submit</button>
          <p>testInput: {testInput}</p>
        </form>
      );
    }
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <StrictMode>
        <App />
      </StrictMode>,
    );
    
    </script>
    Login or Signup to reply.
  2. This is alright as long as you aren’t using <form onSubmit= {function}> to process the input, in which case the masked value (*****6789) will passed as the input value.

    Login or Signup to reply.
  3. I honestly would not recommend messing with the actual value just to achieve a purely visual effect. I did this in the past and ran into several problems, that weren’t apparent at first. Some things I recall:

    • clicking into the input would not set the caret position correctly in some browsers (because the input value changed the very moment when clicked and thereby invalidating the selection position)
    • The modified value would find its way into the further processing chain ending up being sent to the backend
    • validation would break because an input that only allows numeric input can’t contain asterisks ***1233

    And there is a wonderful alternative without the trouble:
    Do the switcheroo purely with CSS. Place two different elements into the DOM:

    • one is the input which contains the actual value (free from tampering)
    • the other is an element that display the masked value (this can be an input with readonly, or a div, or anything that allows you to get the visual effect you are aiming for)
    • you use :focus and :hover (and what else you want to include) to control the visibility of the two elements
    • css property pointer-events allows fine control over which element receives the hover events.

    Here is an example, just to illustrate the idea:

    <div className="container">
      <input className="actual-value" value={value} onChange={...} />
      <input className="masked-display" value={maskAllExceptFinal(value)} readonly />
    </div>
    
    .container {
      position: relative;
    }
    
    .container:hover {
      & .masked-display {
        display: none;
      }
    }
    
    .actual-value {
      // ...
    }
    
    .masked-display {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search