cabralpinto
cabralpinto

Reputation: 2098

Auto-scaling input to width of value in React

I want to have an input whose width adapts to fit its content.

I'm trying to implement this answer to a similar question, but using React:

import React, { useState } from 'react';

export default () => {
  const [content, setContent] = useState('');
  const [width, setWidth] = useState(0);

  const changeHandler = evt => {
    setContent(evt.target.value);
  };

  return (
    <wrapper>
      <span id="hide">{content}</span>
      <input type="text" autoFocus style={{ width }} onChange={changeHandler} />
    </wrapper>
  );
};

The problem is I don't know how to then query the width of the span, in order to then change the width of the input (using setWidth).

How can I achieve this?

Upvotes: 16

Views: 30886

Answers (5)

Bersenev Yegor
Bersenev Yegor

Reputation: 1

Another option without using extra hidding element:

const GrowingInput = () => {
  const inputRef = React.useRef(null);
  
  const handleChange = () => {
    inputRef.current.style.width = "0";
    inputRef.current.style.width = `${inputRef.current.scrollWidth}px`;
  };
 
  return <input ref={inputRef} style={{width: 0}} autoFocus onChange={handleChange} />
};

const App = () => <p>Lorem ipsum {<GrowingInput />} egestas arcu. </p>;

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Upvotes: 0

cabralpinto
cabralpinto

Reputation: 2098

After a lot of fiddling around, I found a solution!

import React, { useState, useRef, useEffect } from 'react';

export default () => {
  const [content, setContent] = useState('');
  const [width, setWidth] = useState(0);
  const span = useRef();

  useEffect(() => {
    setWidth(span.current.offsetWidth);
  }, [content]);

  const changeHandler = evt => {
    setContent(evt.target.value);
  };

  return (
    <wrapper>
      <span id="hide" ref={span}>{content}</span>
      <input type="text" style={{ width }} autoFocus onChange={changeHandler} />
    </wrapper>
  );
};

To get a reference to the #hide span I employ useRef. Then, the width state variable can be updated via the function defined inside useEffect, which gets called everytime content changes.

I also had to switch the display: none in the css of #hide for position: absolute and opacity: 0, as otherwise targetRef.current.offsetWidth would always evaluate to 0.

Here's a working demo.

Upvotes: 30

Simon
Simon

Reputation: 429

Here is the simplest solution I've found.

You create a function that you'll use on change

   const handleChangeAndSize = (ev: ChangeEvent<HTMLInputElement>) => {
      const target = ev.target;
      target.style.width = '60px';
      target.style.width = `${target.scrollWidth}px`;

      handleChange(ev);
   };

Then you use it as a regular function in your component

<input type='text' onChange={handleChangeAndSize}/>

The style.width = 60px will allow to resize the input when shrinking, and the target.scrollWidth will watch the 'scrollable width' on x axis and set it as width.

Nb: credit to this guy: https://www.youtube.com/watch?v=87wfMZ56egU

Upvotes: 3

Moumita Das
Moumita Das

Reputation: 43

Found out a trick using Refs in react.

style={{ width: inputRef.current ? inputRef.current.value.length + 'ch' : 'auto' }}

And set the ref={inputRef} for the element. Do remember to set the min-width for the input in your CSS.

Upvotes: 2

faerin
faerin

Reputation: 1925

Well, this was interesting enough! I tried a few different ideas that I had, but none of them worked perfectly - especially not if they were to be written in a somewhat respectable code.

I found this post however and decided to try that out. https://stackoverflow.com/a/43488899/3293843

I am sure there are flaws with it, one for example is that it does act funny unless I use a monospaced font. But maybe there are some css tricks to get around that?

// Normally I'd go for ES6 imports, but to make it run as a StackOverflow snippet I had to do it this way
const { useState, useRef } = React;

const GrowingInput = () => {
  const [width, setWidth] = useState(0);
  
  const changeHandler = evt => {
    setWidth(evt.target.value.length);
  };
 
  return (
    <input style={{ width: width +'ch'}} type="text" autoFocus onChange={changeHandler} />
  )
};

const App = () => {
  return (
    <p>Lorem ipsum {<GrowingInput />} egestas arcu.</p>
  );
};

// Render it
ReactDOM.render(<App />, document.getElementById("react"));
input {
  font-family: Courier;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Have you considered using a contenteditable instead?

https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content

Upvotes: 8

Related Questions