Rails pattern: trim spaces on input

Problem: Your Rails application accepts user input for a number of models. For many or most of these fields, leading and trailing spaces are a significant inconvenience — they cause problems for your validators (email address, phone number, etc.) and they cause normalization and uniqueness problems in your database.

Solution: Just as the Rails ActiveRecord class uses methods like belongs_to and validates_format_of to define model relationships and behaviors, create a new class method to express trimming behavior. There are a number of ways to do this; I will present one possibility that I have used in my own code. I created a file lib/trimmer.rb with the following contents:

module Trimmer
  # Make a class method available to define space-trimming behavior.
  def self.included base
    base.extend(ClassMethods)
  end

  module ClassMethods
    # Register a before-validation handler for the given fields to
    # trim leading and trailing spaces.
    def trimmed_fields *field_list
      before_validation do |model|
        field_list.each do |n|
          model[n] = model[n].strip if model[n].respond_to?('strip')
        end
      end
    end
  end
end

Then I write the following in my models:

require 'trimmer'
class MyModel < ActiveRecord::Base
  include Trimmer
  . . .
  trimmed_fields :first_name, :last_name, :email, :phone
  . . .
end

While this makes the behavior available to particular models explicitly, you may prefer to make this behavior available to all of your models implicitly. In that case, you can extend the ActiveRecord::Base class behavior by adding the following to config/environment.rb:

require 'trimmer'
class ActiveRecord::Base
  include Trimmer
end

If you do this, the trimmed_fields class method will be available to all of your models.

Unobtrusive Javascript: Self-labeling text inputs

Some web sites have self-labeling text input boxes; for example, see the text box at the top right of the page on memberhub. When these self-labeling form fields are empty, they contain helpful text that labels or further explains their purpose, such as “Search” or “Enter your favorite color.” As soon as you click on these fields, the help text vanishes and you can type in a value.

Using unobtrusive Javascript (see introduction), we can add behavior to a text input element to automatically label it with this sort of help text contained within the element. We will take advantage of the title attribute of input elements to do this. The title element is already widely used in many browsers to provide help text in a tool-tip when you hover over an element with your mouse, and we will steal this title text from any input text field to use for self-labeling purposes. Here is a script that accomplishes that:

autolabel.js

Event.onReady(function() {
  $$('input[type="text"][title]').each(function(inputElement) {
    var e = inputElement;
    var color = e.getStyle('color');
    var fontStyle = e.getStyle('fontStyle');

    if(e.value == e.title) {            // FF reload behavior.
      e.value = '';
    }

    var blank = !$F(e);

    var blurHandler = function(ev) {
      blank = !$F(e);
      if(blank) {
        e.setStyle({ 'color'     : 'darkgray',
                     'fontStyle' : 'italic' });
        e.value = e.title;
      }
    }
    e.observe('focus', function(ev) {
      if(blank) {
        if($F(e) == e.title) {
          e.value = '';
        }
        e.setStyle({ 'color'     : color,
                     'fontStyle' : fontStyle });
      }
    });
    e.observe('blur', blurHandler);
    blurHandler(null);

    Event.observe(e.form, 'submit', function(ev) {
      if(blank) {
        e.value = '';
      }
    });
  });
});

Here’s how to use it. 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/autolabel.js" type="text/javascript"></script>
. . .
<input type="text" name="color" title="Enter your favorite color" />

How it works

The script first searches for all input elements in the document that are of type “text” and also have the title attribute. For each of these, it executes a function to add the self-labeling behavior to the element.  This function does a number of things:

  1. When you refresh a page in Firefox, as long as you don’t do a full reload, Firefox preserves whatever values were previously in form fields.  The function tests if the current value of the form field is equal to the title attribute (meaning that someone refreshed the page while the self-labeling description was present in the field, and if so, the function clears the form field.
  2. For all other purposes, the function uses the variable blank to track whether the field is blank and should have the label inserted.  We do this rather than comparing the field’s content to the title tag, in case the user actually types in the value of the title tag (for something like “Search”).
  3. The function adds a handler for the focus event (cursor enters field).  If the field is blank, it clears the label text so the user can enter their own text, and restores the field’s color and font style to their original values.  Otherwise, the field contains user text so it is left unchanged.
  4. The function adds a handler for the blur event (cursor leaves field).  If the field is blank, it remembers the fact that it is blank, sets the style of the field so that the text is italic and dark gray (you may modify this as you wish), and then inserts the title text.
  5. The function adds a handler for the submit event on the field’s form.  If the form is submitted and the field is blank, it will be cleared so that the correct value is submitted for the form contents.

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.