Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: how to make an input only as wide as the amount of text provided?

Simple enough question: I am trying to create inputs that are as large as the text supplied to them.

Sandbox: https://codesandbox.io/s/long-snowflake-6u13n?file=/src/Test.jsx

My design intention is to generate inputs dynamically and then allow the user to have styles specific to each input that visually help break up each sentence based on outside events. But before I can move forward, it's really important that my input container is only as large as the text within.

why not use a textarea? -- I have data that is particular to each sentence that I want to create unique styles for.

Any thoughts?

like image 757
kevin Avatar asked Sep 27 '20 20:09

kevin


People also ask

How do I make input width fit content?

Use the span. text to fit width of text, and let the input have same size with it by position: absolute to the container.

How do you limit the input field in React?

Use the maxLength prop to set a character limit on an input field in React, e.g. <input maxLength={5} /> . The maxLength attribute defines the maximum number of characters the user can enter into an input field or a textarea element.

How do you use React to set the value of an input?

To get the value of an input on button click in React: Declare a state variable that tracks the value of the input field. Add an onClick prop to a button element. When the button is clicked, update the state variable.

How can an input be controlled in React?

Controlled Components We can combine the two by making the React state be the “single source of truth”. Then the React component that renders a form also controls what happens in that form on subsequent user input. An input form element whose value is controlled by React in this way is called a “controlled component”.


2 Answers

Here is an approach from plain HTML/CSS and a working snippet , hidding the value typed inside a span behind the input set in an absolute position. CSS can make both span and input matching the same lenght/width. Stretching/collapsing a parent (label) will finish the job.

In the courtesy of @silvenon you may also find a react sample below the snippet

var val = document.querySelector('#test');
let tpl = document.querySelector('#tpl');
let text = val.value;
 tpl.textContent= text;

val.addEventListener("input", function () {// onchange ...
  let text= val.value;
  //console.log(text);
  tpl.textContent= text;
  });
label {
  display: inline-block;
  position: relative;
  min-width: 2em;
  min-height: 1.4em;
}

#tpl {
  white-space: pre;
  /* max-width : could be wised to set a maximum width and overflow:hidden; */
}

#test {
  font-family: inherit;
  font-size: inherit;
  position: absolute;
  vertical-align: top;
  top: 0;
  left: 0;
  width: 100%;
  background: white;
}
<label><span id="tpl"></span><input id='test' value="Some test to try" ></label>

In the courtesy of @silvenon, you may find a react sample of that code.

const SentenceInput = styled.input`
  padding: 0;
  margin: 0;
  border: none;
  border: 1px solid black;
  /* added styles */
  font-family: inherit;
  font-size: inherit;
  position: absolute;
  vertical-align: top;
  top: 0;
  left: 0;
  width: 100%;
  background: white;
`

const Label = styled.label`
  display: inline-block;
  position: relative;
  min-width: 2em;
  min-height: 1.4em;
`

const Template = styled.span`
  white-space: pre;
  /* max-width : could be wised to set a maximum width and overflow:hidden; */
`

const Sentence = ({ initialValue }) => {
  const [value, setValue] = React.useState(initialValue)
  return (
    <Label>
      <Template>{value}</Template>
      <SentenceInput
        type="text"
        value={value}
        onChange={(event) => {
          setValue(event.target.value)
        }}
      />
    </Label>
  )
}
like image 194
G-Cyrillus Avatar answered Oct 30 '22 08:10

G-Cyrillus


Using ch unit would work if the typeface was monospace, otherwise character width varies. I would approach this problem by rendering an inline element holding the same text, measuring it and hiding it instantly every time the input field value changes.

To do this it's best to create a separate component for rendering sentences, let's call it Sentence:

const Test = () => {
  return (
    <div className="App">
      {value.map(({ sentence }, i) => {
        return (
          <Sentence
            initialValue={sentence}
            key={i}
          />
        );
      })}
    </div>
  );
};

Test would pass the initial value, then Sentence will continue maintaining its own state:

const Sentence = ({ initialValue }) => {
  const [value, setValue] = React.useState(initialValue)

  return (
    <SentenceInput
      type="text"
      value={value}
      onChange={(event) => {
        setValue(event.target.value)
      }}
    />
  )
}

Next, I'd add a span element that will serve as a measurer element, where the text should be styled the same way as in input elements, so the measurements turn out accurate. In your example in Chrome that would mean setting the font size to 13.3333px.

Now for the trickiest part, we need to combine useEffect and useLayoutEffect; useEffect will make the measurer visible, then useLayoutEffect will measure it and hide it

This is the result:

const Sentence = ({ initialValue }) => {
  const [value, setValue] = React.useState(initialValue)
  const [visible, setVisible] = React.useState(false)
  const [width, setWidth] = React.useState('auto')
  const measurer = React.useRef(null)

  React.useEffect(() => {
    setVisible(true)
  }, [value])

  React.useLayoutEffect(() => {
    if (visible) {
      const rect = measurer.current.getBoundingClientRect()
      setWidth(rect.width)
      setVisible(false)
    }
  }, [visible])

  return (
    <>
      <span
        ref={measurer}
        style={{ fontSize: '13.3333px' }}
      >
        {visible && value}
      </span>
      <SentenceInput
        type="text"
        value={value}
        style={{ width: width + 1 }}
        onChange={(event) => {
          setValue(event.target.value)
        }}
      />
    </>
  )
}

I added 1px to the computed width because it seems to remove a small horizontal scroll in the input fields.

This is for you to tweak further the way you want, for example how it should behave when it reaches the viewport width.

like image 36
silvenon Avatar answered Oct 30 '22 08:10

silvenon