Unobtrusive Javascript: Expandable textareas

Using unobtrusive Javascript (see introduction), we can add behavior to textareas to make them automatically expand or contract as text is entered into them. Here is a script that accomplishes that:

autosize.js

Event.onReady(function() {
  $$('textarea').each(function(inputElement) {
    var textarea = inputElement;
    var initialHeight = textarea.getHeight();
    var currentHeight = -1;
    var currentTimer = false;
    var div = $div({id: textarea.id + '_hidden'});

    textarea.insert({'after': div});
    div.setStyle({'display'       : 'none',
                  'width'         : textarea.getWidth() ?
                                      (textarea.getWidth() + "px") :
                                      textarea.getStyle('width'),
                  'whiteSpace'    : 'pre-wrap',
                  'fontFamily'    : textarea.getStyle('fontFamily'),
                  'fontSize'      : textarea.getStyle('fontSize'),
                  'lineHeight'    : textarea.getStyle('lineHeight'),
                  'paddingTop'    : textarea.getStyle('paddingTop'),
                  'paddingLeft'   : textarea.getStyle('paddingLeft'),
                  'paddingRight'  : textarea.getStyle('paddingRight'),
                  'paddingBottom' : textarea.getStyle('paddingBottom'),
                  'marginTop'     : textarea.getStyle('marginTop'),
                  'marginLeft'    : textarea.getStyle('marginLeft'),
                  'marginRight'   : textarea.getStyle('marginRight'),
                  'marginBottom'  : textarea.getStyle('marginBottom'),
                  'borderTop'     : textarea.getStyle('borderTop'),
                  'borderLeft'    : textarea.getStyle('borderLeft'),
                  'borderRight'   : textarea.getStyle('borderRight'),
                  'borderBottom'  : textarea.getStyle('borderBottom')
                 });

    var timerHandler = function() {
      currentTimer = false;
      if(initialHeight == 0) {
        initialHeight = textarea.getHeight();
      }
      div.innerHTML = $F(textarea).replace(/&/g, '&')
                                  .replace(/</g, '<')
                                  .replace(/n/g, '<br />') +
                      '<br />z';
      var newHeight = Math.max(initialHeight, div.getHeight());
      if(newHeight != currentHeight && newHeight != 0) {
        textarea.setStyle({ 'height': newHeight + 'px' });
        currentHeight = newHeight;
      }
    }
    var eventHandler = function(ev) {
      if(!currentTimer) {
        setTimeout(timerHandler, 250);
      }
    }
    textarea.observe('change', eventHandler);
    textarea.observe('keyup', eventHandler);
    timerHandler();
  });
});

Here’s how you would use it. You don’t need to include any explicit Javascript or even styling within your document; the script automatically locates all textareas and adds the stretch behavior to them. Note that you need to include the Prototype and Low Pro Javascript libraries:

example.html

<script src="/js/prototype.js" type="text/javascript"></script>
<script src="/js/lowpro.js" type="text/javascript"></script>
<script src="/js/autosize.js" type="text/javascript"></script>
. . .
<textarea name="comment">blah blah . . .</textarea>

How it works

The script first searches for all textareas in the document, and then executes a function for each textarea to add the stretch behavior to the element. This function creates a hidden div element associated with the textarea, and copies much of the style information from the textarea to the div. Then, it associates a function with the onkeyup and onchange events for the textarea. This event handler function copies the textarea text into the hidden div, measures the size of the div, and adjusts the size of the textarea to fit the size of the div. This means that the textarea grows or shrinks (never smaller than its original size) based on the size of the text contained within it.

Additional notes

The onchange and onkeyup handlers don’t directly copy the text into the div and resize the textarea. I found that doing that immediately on every key press slowed typing down considerably. Instead, the event handlers set a timer to expire 1/4s after the textarea is changed, and this timer handler itself does the resizing. I do not notice any lags in typing responsiveness with this approach.

The timer handler remembers the last measured size of the div so that it doesn’t need to resize the textarea if the div hasn’t changed in size. There are also some places where we check to be sure that a measured height is not zero — I found that IE6 sometimes reports a height of zero even though the DOM has loaded at the point that these functions are called.

If you have any unobtrusive Javascript code that hides your textareas, you should make sure that the autosize.js code runs before your textareas are hidden, so that it can measure their size while they are still visible. We’ll consider something like this in a later post, where you can have some text on your page with an edit link that automatically reveals a textarea to edit the text’s content.

Limitations

For unknown reasons, IE6 doesn’t seem to correctly report the fontFamily for an unstyled textarea. In cases where the textarea is clearly a monospace font, IE6 will report the body’s fontFamily (e.g., ‘arial’) instead of ‘monospace’ when retrieving the textarea’s fontFamily style. The result of this is that the div’s styling doesn’t match the textarea’s styling, and so the textarea will not necessarily be sized properly if it holds a lot of text. The workaround for this problem is to explicitly style your textareas using ‘font-family: monospace’; IE6 correctly reports the fontFamily in this case.

As indicated above, in some cases IE6 incorrectly reports a height of 0 for the textarea or div. The result of this is that in IE6 the textarea’s initial size may not stretch to fit its contents, but as soon as a character is typed into it it will expand to the correct size. I’m not aware of any universal workarounds for this. However, if you have textareas that are not auto-hidden on page load then you may be able to modify the code above so that instead of calling timerHandler() directly at DOM load time, it is scheduled to be called by a timer shortly thereafter.