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.

8 thoughts on “Unobtrusive Javascript: Expandable textareas

  1. Hi Scott

    Well, not sure how were you able to test this on IE6, there seems to be some problems with it. Tried with different versions of Prototype.

    First I needed to change

    var textarea = inputElement;

    into :

    var textarea = $(inputElement);

    as IE doesn’t allow us to touch element prototypes, so element variables can’t automatically have ‘Prototype Library’ specific methods. So it gives ‘variable or method doesn’t exist’

    Then I needed to change

    var div = $div({id: textarea.id + '_hidden'});

    into :

    var div = $div();

    Dunno why, but IE doesn’t like ‘id’ parameter somehow, and the function returns ‘undefined’

    Then one last thing, which made me think this is not always a good solution for IE, I needed to change

    'whiteSpace' : 'pre-wrap'

    to :

    'whiteSpace' : 'pre'

    as IE doesn’t support ‘pre-wrap’ value, and resets the text style in the ‘div’ to something close to the default style, which makes the script read wrong values. But with no wrapping, this won’t work for every design.

    Of course everything could be converted to non-pre style but in my case “pre-wrap” is the reason I’m messing with all this textarea hacks.

    So I finally returned to a different approach; measuring the content and resizing the textarea, after giving all my energy to the shadow div technique.

    Wanted to note these down, it may be helpful for people with applicable designs. You may want to check common.js on facebook.

    Onur

  2. Onur, thanks for writing!

    In my testing I used Prototype 1.6.0.2, IE 6.0.2900.2180.xpsp2_gdr.070227-2254. My Content-Type was set to application/xhtml+xml (at least, assuming that IE6 is advertising this in its Accept header) and my document is valid XHTML. I didn’t get any JS errors in this case.

    How are you measuring the textarea internal content size? Are you using the scrollHeight attribute on the textarea? Have you found that to be reliable across browsers? If so, that seems like a much better approach.

    Thanks!

  3. Aah, yes, I intended to set currentTimer = true just before calling setTimeout. The intent was to never have more than one outstanding scheduled timer call. Thanks!

  4. Strange, then it’s probably the content type. Same IE version. I’ve tried it on html 4.01, though I was targeting xhtml 1.0 for the production. If that’s the reason, it’s stange to see the doctype changes the IE’s ‘do not touch my element prototypes!’ style =)

    Working on this for days now, mostly on compressing / trimming prototype, deciding the code quote style to use, making some helper scripts. Still not sure which quote options to use, anyway.

    Right, I’m using the scrollHeight attribute. I was trying to stay away from that approach and use shadow divs instead. But finally scrollHeight appears to be a better option in my case. It works perfect after taking care of IE and FX specific behaviours in my case.

    However I don’t think it will be the same in your case, auto grow while you type. I use it during page load to fix block sizes. I keep them readonly, but I saw that when I type anything in a textarea (non readonly) on IE, it’s size gets changed (probably trying to prevent word-wraps). And when it’s readonly, the size is wrongly reported, so I needed to switch readonly mode off during calculation and then back on. This was on xhtml 1.0. You can try it anyway, didn’t try the last version on a test env. it may change on CSS definitons.

    My intention was to use it for a wrapped pre for code samples. It works, but I guess I’ll use HTML converted samples and horizontal scrolls now. You can see the code sample below this page :

    http://blog.onursafak.com/2008/07/hide-email-links-from-spam-bots-works.html

    It’s a textarea, you can see the resize script in blog.js. To keep one more line with this approach, I would get the font size, line spacing and line height during initialization, and add them to the reported size, or just add a row during init, and get the height difference.

    Onur

    Onur

Leave a comment