10

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?

cabralpinto
  • 1,814
  • 3
  • 13
  • 32

4 Answers4

21

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.

cabralpinto
  • 1,814
  • 3
  • 13
  • 32
7

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

faerin
  • 1,915
  • 17
  • 31
  • Check the solution I just posted (works with proportional fonts!) – cabralpinto Nov 26 '20 at 14:32
  • That looks very similar to my first approach. The thing is that the reference was one click away affecting the input in a way that all characters were not 100% visible. I also had another test were the input would grow too long. Could you put it in a runable snippet? I was also not fond of positioning the span with absolute (which I also did)... maybe a portal is a better solution instead? But, if it works for you - that is great – faerin Nov 26 '20 at 19:08
2

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.

  • It's not dynamic – Jamal Aug 18 '22 at 15:14
  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Albert Logic Einstein Aug 19 '22 at 02:28
1

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

Simon
  • 209
  • 4
  • 12