Tuesday, July 21, 2009

Gmail for Mobile HTML5 Series: Autogrowing Textareas

On April 7th, Google launched a new version of Gmail for mobile for iPhone and Android-powered devices. We shared the behind-the-scenes story through this blog and decided to share more of what we've learned in a brief series of follow-up blog posts. This week I'll talk about autogrowing textareas for entering large amounts of text.

When composing a long message in a web app, regardless of whether it's on a desktop or a mobile device, you really want to see as much of your draft as possible and make use of all the available screen space.
One of my biggest gripes are fixed-size textareas that restrict me to only a couple lines of visible text when my screen is actually many times larger than the size of the textarea.

In today's blog post, I'll share a JavaScript solution for textareas that automatically grow (vertically) to the size of the content. They make composing long messages much easier and, for all those iPhone users out there, takes away the need to scroll with the dreaded magnifying glass! We're working on getting this into Gmail for mobile but here it is now as a teaser of things to come.





Measuring the height of the content

The first step is to detect when the content has changed. Some solutions on the net recommend using a timer (see our previous post to find out more about timers) to check if content has changed. However, that approach is not ideal on a mobile device, where both battery life and processor power are limited.

Instead, we will listen for key-up events from the browser. This guarantees that we only measure the textarea when the content has actually changed.

<textarea id="growingTextarea" onkeyup="grow();"></textarea>

The second step is to actually measure the height of the content. There are solutions on the net that recommend keeping a copy of the content in a div and measuring the div to get the height; however, due to memory and processor limitations on a mobile device, those solutions don't scale well when the content gets large (and it's also hard to replicate textarea line wrapping behavior exactly in a div).

Therefore we will make use of the scrollHeight and clientHeight properties. For our purposes, scrollHeight is the height of all the content while clientHeight is the height of the content that's visible in the textarea (for more precise definitions, see scrollHeight and clientHeight)
function grow() {
var textarea = document.getElementById('growingTextarea');
var newHeight = textarea.scrollHeight;
var currentHeight = textarea.clientHeight;

}
One limitation of using scrollHeight and clientHeight is that we aren't able to shrink the textarea when content is deleted. When all the content of a textarea is visible, the scrollHeight is equal to the clientHeight. Therefore we aren't able to detect that our textarea is actually larger than the minimum size required to fit all the content (please do leave a comment if you think of a solution that doesn't require re-rendering the page).

Growing the textarea

To grow the text area, we modify the height CSS property:
if (newHeight > currentHeight) {
textarea.style.height = newHeight + 5 * TEXTAREA_LINE_HEIGHT + 'px';
}
Notice how we only change the height if newHeight > currentHeight. Depending on the browser, changing the height (even if it's to the same value) will cause the page to re-render. On a mobile device, we want to try our best to minimize the number of operations.

Also, we grow the textarea by five lines every time we grow. From a UI perspective, this reduces the amount of jitter when composing a message but, from a performance perspective, this reduces the number of times we need to re-render the page.

(Quick note for developers implementing this for a browser with scrollbars: you might want to modify the CSS overflow property to preventing the scrollbar from appearing and disappearing as you grow your textarea)

The complete solution

Growing textareas are easy to implement and they make composing long messages infinitely more usable. So go out there add it to all your web apps!
<script>
// Value of the line-height CSS property for the textarea.
var TEXTAREA_LINE_HEIGHT = 13;

function grow() {
var textarea = document.getElementById('growingTextarea');
var newHeight = textarea.scrollHeight;
var currentHeight = textarea.clientHeight;

if (newHeight > currentHeight) {
textarea.style.height = newHeight + 5 * TEXTAREA_LINE_HEIGHT + 'px';
}
}
</script>
<textarea id="growingTextarea"
onkeyup="grow();">
</textarea>

Previous posts from Gmail for Mobile HTML5 Series
Using timers effectively

6 comments:

  1. There is a way of getting the height of the text when it is smaller than the textarea: setting the height of the textarea to 1 line temporary.

    The problem with this is that it requires updating the height of the area on every single key stroke. here is a Mootools version of the code. I have used a div container around the textarea to "buffer" the height changes, so the page layout does not get affected when the textarea height is set to one line:

    theTextarea.addEvent('keyup', function() {
    this.getParent().setStyle('height', this.clientHeight);
    if (this.scrollHeight <= this.clientHeight) {
    this.setStyle('height', TEXTAREA_LINE_HEIGHT);
    }
    this.setStyle('height', this.scrollHeight + 5*TEXTAREA_LINE_HEIGHT);
    this.getParent().setStyle('height', this.clientHeight);
    });

    ReplyDelete
  2. Here is a solution I'm using to grow and shrink textarea when typing. It's little bit CSS and little bit Javascript.
    The idea is to use container div wich will groove and shrink depending on it's content height. To do that I'm using another div elemnt hidden under the textbox and seting textbox height to 100%. It works great on iPhone.

    Here is my solution with CSS and JS:


    <style>
    #growArea {
    width: 100%; height: 100%; position: absolute; top: 0px;
    }

    #growArea, #textDiv {
    font-family: Arial; font-size: 12px; line-height: 15px;
    }
    #textDiv {
    position: relative; top: 0px; white-space: pre-wrap;
    }
    #textareaContainer {
    position: relative; width: 200px; min-height: 100px; padding-bottom: 75px;
    }
    </style>
    <script>
    function resize() {
    document.getElementById('textDiv').innerHTML = document.getElementById('growArea').value;
    }
    </script>
    <div id="textareaContainer"><div id="textDiv"></div>
    <textarea id="growArea" onkeyup="resize();"></textarea>
    </div>

    ReplyDelete
  3. Works great for me! Here's how I wired it up to all textareas on the page via jQuery:

    var TEXTAREA_LINE_HEIGHT = 13;

    function grow(textarea) {
    var newHeight = textarea.scrollHeight;
    var currentHeight = textarea.clientHeight;
    if (newHeight > currentHeight) {
    textarea.style.height = newHeight + 5 * TEXTAREA_LINE_HEIGHT + 'px';
    }
    }

    $('textarea').keyup(function(e){
    grow(e.srcElement);
    });

    ReplyDelete
  4. Since pasting doesn't generate a key-up, you might want to add grow() as a change handler, as well:

    <textarea id="growingTextarea"
    onkeyup="grow();"
    onchange="grow();">
    </textarea>

    ReplyDelete
  5. Could you please explain to me what TEXTAREA_LINE_HEIGHT variable does? It does not seem to affect anything but number of lines to shift down. But how does it work?

    ReplyDelete
  6. How about shrinking as "shrink to 1 line temporarily and then regrow as necessary", but performed as little as possible? One could, for example, count the number of newlines and only shrink if that count decreases.

    ReplyDelete