1

I wrote the following code to create a text input element that dynamically changes its width as input length increases, but it also has a minimum length:

var input = document.getElementById("entry");
input.oninput = resizeInput;
resizeInput.call(input);

 function resizeInput() {
    var len = this.value.length, minLen = 30, applied = Math.max(len, minLen);
    this.style.width = applied + "ch";
}
<input id="entry" type="text">

But if you insert a successive string of characters (iii.../7/f), you would notice that the input field width suddenly starts increasing at a very rapid pace as soon as the input length crosses the minimum length minLen. (Also, as pointed out in the comments, the issue does not occur for ggg.../m or others. I don't know why.)

I tried using the min-width property but to no avail.

var input = document.getElementById("entry");
input.oninput = resizeInput;
resizeInput.call(input);

function resizeInput() {
    this.style.width = this.value.length + "ch";
}
input { min-width: 30px !important; }
<input id="entry" type="text">

but it doesn't work either. So, what else do I do?

Gaurang Tandon
  • 6,504
  • 11
  • 47
  • 84
  • Isn't that the desired output? If it dynamically changes to the size of it's content, and you're rapidly adding content, shouldn't it grow quickly? – DBS Jun 08 '18 at 13:24
  • @DBS Why would I want miles of empty space following my text? :( I don't want any spacing to the right or the left, so, no, it isn't the desired output. – Gaurang Tandon Jun 08 '18 at 13:26
  • Hmm, I'm not seeing any additional white space in your example – DBS Jun 08 '18 at 13:26
  • 1
    @DBS Try typing in `i` vs `m`. OP is measuring in characters, but the default font is not a monospace font. That is the main issue... – Mr. Polywhirl Jun 08 '18 at 13:29
  • @DBS Ah, this is interesting. The letter `g` doesn't cause any issue at all, rather `f`, `1`, `i`, etc. do. This is strange. – Gaurang Tandon Jun 08 '18 at 13:30

3 Answers3

3

The problem comes with the unit ch which takes the height as base. Since not all characters have the same height as width it might end up with a margin on the right (for instance by using i). https://css-tricks.com/the-lengths-of-css/

You could solve the font issue by either using a monospace font or a contentEditable element, since it kinda resizes itself.

p{
  border: 1px solid #000;
  display: inline-block;
  min-width: 150px
}
<p contentEditable = 'true'></p>

Depending on what you want you would have to change the css or script to prevent pasting html or linebreaks.

Another option to keep the integrity of the input element would be by using css ::after to set the width for you:

.inputText{
  display: inline-block;
}
.inputText > input[type=text]{
  font-size: 13px;
  font-family: arial;
  width: 100%
}

.inputText::after{
  background:red;
  border: 1px solid black;
  content: attr(data-content);
  display: block;
  font-family: arial;
  font-size: 13px;
  visibility: hidden
}
<div class = 'inputText' data-content = 'text'>
  <input type = 'text' placeholder = 'text1' oninput = 'parentNode.setAttribute("data-content", this.value)' />
</div>
<div class = 'inputText' data-content = 'text'>
  <input type = 'text' placeholder = 'text2' oninput = 'parentNode.setAttribute("data-content", this.value)' />
</div>

Update

Created a little plugin for it, which does not handle all cases yet the basic ones. Also, I replaced div with label since those are more suitable for input elements. Theoretically, it could be expanded.

;(function(ns){
  "use strict";

  //REM: Makes an input-element flexible to its width
  function _makeFlex(input){
    if(input && input.type){
      //REM: Wrapping the input-element with a label-element (makes the most sense on inputs)
      _wrapInputInLabel(input);

      //REM: Adding a listener to inputs, so that the content of othe ::after can be adjusted
      input.addEventListener('input', _onInput, false);
    }
  };

  function _onInput(){
    var tInput = this,
      tParent = tInput.parentNode;

    //REM: Just verifying.. better save than sorry :-)
    if(tInput.type && tParent.tagName === 'LABEL' && tParent.getAttribute('data-flex')){
      //REM: Here exceptions can be set for different input-types
      switch(tInput.type.toLowerCase()){
        case 'password':
          //REM: This one depends on the browser and/or OS
          //https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/password
          tParent.setAttribute('data-flex-content', tInput.value.replace(/./g, '•'))
          break
        default:
          tParent.setAttribute('data-flex-content', tInput.value)
      }
    }
  };

  //REM: Wraps the input in a label-element
  function _wrapInputInLabel(input){
    if(input){
      var tLabel = (input.parentNode && input.parentNode.tagName === 'LABEL') ? input.parentNode : input.parentNode.appendChild(document.createElement('label'));
      tLabel.setAttribute('data-flex', input.getAttribute('data-flex'));
      tLabel.appendChild(input);

      //REM: Copy the font-styles on the label - can be expanded with more
      tLabel.style.fontFamily = window.getComputedStyle(input, null).getPropertyValue('font-family');
      tLabel.style.fontSize = window.getComputedStyle(input, null).getPropertyValue('font-size');

      if(input.id){
        tLabel.setAttribute('for', input.id)
      }
    }
  };

  ns.Flex = {
    Init: function(){
      //REM: Loops through all the marked input elements
      for(let tL=document.querySelectorAll('input[data-flex]'), i=0, j=tL.length; i<j; i++){
        _makeFlex(tL[i])
      }
    }
  }
})(window.mynamespace=window.mynamespace || {});

;window.onload = function(){
  mynamespace.Flex.Init()
};
label[data-flex]{
  font-family: arial;
  font-size: 13px;
  display: inline-block;
  min-width: 30px;
  position: relative
}

label[data-flex]::after{
  background: red;
  border: 1px solid black;
  content: attr(data-flex-content);
  display: block;
  visibility: hidden
  /*REM: following styles are just for demo purposes */
  line-height: 20px;
  visibility: visible;
}

/*REM: Has a little slider on the right */
label[data-flex='number']::after{
  margin-right: 18px
}

label[data-flex] > input{
  font-size: 13px;
  font-family: arial;
  width: 100%
}
<input type = 'text' placeholder = 'text' data-flex = 'flex' id = 'inText' />
<input type = 'text' placeholder = 'crazy' data-flex = 'flex' id = 'inText2' style = 'font-size: 20px; font-family: comic-sans' />
<input type = 'password' placeholder = 'password' data-flex = 'flex' id = 'inPassword' />

<label>
  <input type = 'number' placeholder = 'number' data-flex = 'number' id = 'inNumber' />
</label>

Another update

Was researching and trying a bit further and ran into a plugin that tries to do something alike.

I extracted and changed the part for the input-elements a bit. For me in Chrome all input types work fine. IE11 requires a polyfill for scrollWidth on input-elements and Firefox has issues with the type number. Yet I guess from the idea using the clientWidth and scrollWidth this is - in theory - the best solution:

//REM: Resizes the Input-Element
function resizeInput(input){
  if(input.nodeName.toLowerCase() === 'input'){
    var tStyle = getComputedStyle(input),
        tOffset = 0;

    //REM: Input-Elements with no width are likely not visible
    if(input.getBoundingClientRect().width){
      //REM: Resetting the width for a correct scroll and client calculation
      input.style.width = '0';

      //REM: Calculting the offset
      switch(tStyle.boxSizing){
        case 'padding-box':
          tOffset = input.clientWidth;
          break
        case 'content-box':
          tOffset = parseFloat(tStyle.minWidth);
          break
        case 'border-box':
          tOffset = input.offsetWidth;
          break
      };

      //REM: Somehow IE11 does not seem to support scrollWidth properly
      //https://github.com/gregwhitworth/scrollWidthPolyfill
      var tWidth = Math.max(tOffset, input.scrollWidth - input.clientWidth);

      input.style.width = tWidth + "px";

      //REM: This is kind of a double-check to backtrack the width by setting an unlikely scrollLeft
      for(var i=0; i<10; i++){
        input.scrollLeft = 1000000;

        if(input.scrollLeft === 0){
          break;
        };

        tWidth += input.scrollLeft;
        input.style.width = tWidth + 'px'
      }
    }
    else{
      //REM: Input-Element is probably not visible.. what to do? >.<
      element.style.width = element.value.length + 'ch'
    }
  }
};
input{
  min-width: 30px
}
<input type = 'text' placeholder = 'text' oninput = 'resizeInput(this)' />
<input type = 'number' placeholder = '99' oninput = 'resizeInput(this)' />
<input type = 'password' placeholder = 'password' oninput = 'resizeInput(this)' />
Lain
  • 3,657
  • 1
  • 20
  • 27
  • Problem with this solution is that you can't take advantage of input's attributes like required or pattern and it also doesn't work for anything but text (think password, email, number). All these features would have to be added using extra JS. – user3210641 Jun 08 '18 at 14:11
  • Yes, that is correct. It also has problems with pasted HTML of course, unless you solve it in a script - like I wrote in my answer. – Lain Jun 08 '18 at 14:14
  • @user3210641: I found a solution which keeps the advantages of the `input`, yet I could not get it working with other input types yet - for instance password has other characters and hence a different length :-) – Lain Jun 08 '18 at 14:38
  • The second edit is interesting! Why do you have two inputs in it though? – Gaurang Tandon Jun 08 '18 at 15:01
  • @Gaurang Tandon: Just as an example. Was playing with different input types. It is kinda in progress, sadly no time at the moment :-) – Lain Jun 08 '18 at 15:02
2

Although @Lain's answer is the simplest solution it has multiple pitfalls - if you don't want to support multiline input you have listen for line breaks as well as pasting of text and it also doesn't support input's native attributes like type, required, pattern etc.

What you can do instead is create a container for your input and hide 'helper' element under your input field. Then you update your input element's width based on the width of your helper element.

EDIT: As mentioned by @Mr.Polywhirl my previous solution wouldn't work if you directly changed input's font-family or font-size. Below is updated solution which solves this issue:

// Save reference of the elements
const input = document.getElementById('input-field');
const helper = document.getElementById('input-text');

// Credit : https://stackoverflow.com/questions/7444451/how-to-get-the-actual-rendered-font-when-its-not-defined-in-css/7444724#7444724
const css = (element, property) => window.getComputedStyle(element, null).getPropertyValue(property);

// Listen to changes on input element
input.addEventListener('input', function(event) {
  // Save input current value
  const value = event.target.value;

  // Get current font styles of the input and set them to helper 
  helper.style.font = css(input, 'font');

  // Update helper text according to the input value
  helper.innerText = value;

  // Update input width to match helper width
  input.style.width = css(helper, 'width');
});
/* Necessary for helper position */
.input-container {
  position:relative;
}

.input-field {
  /* Set initial width */
  width: 5rem;

  /* Set minial width */
  min-width: 5rem;
}

/* Hide helper */
.input-text {
  position: absolute;
  top: 0;
  left: 0;
  z-index: -999;
}
<div class="input-container">
  <input type="text" class="input-field" id="input-field" />
  <span class="input-text" id="input-text"></span>
</div>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
user3210641
  • 1,565
  • 1
  • 10
  • 14
  • This seems similar to syntax-highlighting code editors. Where they have a backing text area, but render styled divs in top. The only problem is if you change the font style of the input, you need to apply the same style to the helper span/div. It's a neat little trick. – Mr. Polywhirl Jun 08 '18 at 13:55
  • Hm good point ! I didn't think of that ... Edited my answer so it is independent on the container's font-size and font-family, but rather computes current font styles from the input element. – user3210641 Jun 08 '18 at 14:04
  • Maybe it is my browser (Chrome) yet it does not work for me. The `input` does not extend. – Lain Jun 08 '18 at 14:35
  • The input doesn't extend for me too. – Gaurang Tandon Jun 08 '18 at 14:38
  • 1
    in which way does your solution support different input types better than the other two solutions? for me just text works with this one aswell. – JavaScript Jun 09 '18 at 07:00
1

You can dynamically grab the computed font and then measure the text using HTML5 canvas.

You are trying to measure in characters, but the font is not always a monospace font. You can test this by typing in i vs m in your example. You need to measure the entire text and consider the width of each individual character.

var input = document.getElementById('entry'); // Grab the element
input.oninput = resizeInput;                  // Add listener
resizeInput.call(input);                      // Trigger change,

function resizeInput() {
  var fontFamily = css(this, 'font-family');
  var fontSize   = css(this, 'font-size');
  var fontStyle  = css(this, 'font-style');

  this.style.width = getTextWidth(this.value, fontSize + ' ' + fontFamily) + 'px';
}

// https://stackoverflow.com/a/7444724/1762224
function css(element, property) {
  return window.getComputedStyle(element, null).getPropertyValue(property);
}

// https://code.i-harness.com/en/q/1cde1
function getTextWidth(text, font) {
  // re-use canvas object for better performance
  var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
  var context = canvas.getContext('2d');
  context.font = font;
  var metrics = context.measureText(text);
  return metrics.width;
}
input {
  min-width: 30px !important;
}
<input id="entry" type="text">
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
  • But how do you measure the text in relation to the font family and size? Is there another way to call `context.measureText(text)` without using canvas? – Mr. Polywhirl Jun 08 '18 at 13:47
  • cool solution. but i join user3210641, the other solution looks more simple without any calculations needed. – JavaScript Jun 08 '18 at 13:48
  • 1
    But then you are destroying the integrity of the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) field. You can no longer utilized built in functionality, it is just a paragraph element... – Mr. Polywhirl Jun 08 '18 at 13:50
  • Check my answer below. – user3210641 Jun 08 '18 at 13:52
  • you are right with the integrity of course. i just like the simplicity of the other solution. just my personal taste. – JavaScript Jun 08 '18 at 13:54