Thursday, September 17, 2009

Ruby/Rails and Javascript Word Count Validation - Server and Client-side Validation

I based the following code on Mark Swardstrom's Character count a textarea in Ruby on Rails solution, adding in the validates_length_of example in the Rails API docs and the scan method of the Prototype API (prototype is available automatically in Rails). The following methods are an example of both server-side and client-side word count validation. This was useful in a Rails app that we have where students have to enter answers for several essay questions in a form. Despite the number of observers checking each tenth of a second, it behaves reasonably well even with several questions on a page checking up to 500 words. It got drudging slow at typing at ~17000 words. Firefox v3.5.3 displayed an error about a unresponsive script at ~23000 words on a Mac Pro dual-core Intel Xeon (2.66 GHz) with 2GB memory. However, if the user chooses to continue it will continue to work. Also, that can be remedied by adding a character count check prior to the Prototype scan check.

Here is a screenshot of the example doing client-side validation (warnings, really). This is done dynamically (with javascript, but all client-side- no Ajax) so that the user gets immediate feedback as they type or copy/paste:

Thanks to the Lorem Ipsum generator for help generating the nice Latin text.

Here is an example of the server-side check based on the validates_length_of example in the Rails API docs. You put this code in your model, which we'll call MyModel:

  SUMMARY_MAX_WORDS = 17
  validates_length_of :summary, :maximum => SUMMARY_MAX_WORDS, :too_long=> "must not be longer than {{count}} words", :tokenizer => lambda{ |s| s.scan(/\w+/) }
  SHORT_STORY_MAX_WORDS = 500
  validates_length_of :short_story, :maximum => SHORT_STORY_MAX_WORDS, :too_long=> "must not be longer than {{count}} words", :tokenizer => lambda{ |s| s.scan(/\w+/) }

Here is the code to put into the helper (or ApplicationHelper if you want available everywhere in the app):

  def warn_if_word_count_exceeded(field_id, update_id, maximum_number_of_words, options = {})
    function = "var words = []; $F('#{field_id}').scan(/\\w+/, function(match){ words.push(match[0]) }); if (words.length > #{maximum_number_of_words}) { $('#{update_id}').innerHTML = 'Your entry contains ' + words.length + ' words. Please limit your answer to #{maximum_number_of_words} words.' } else { $('#{update_id}').innerHTML = '' };"
    out = javascript_tag(function)
    out += observe_field(field_id, options.merge(:function => function))
  end

Here is some example code for the view (note: the 0.10 means it is checking every tenth of a second):

<h3><%= title "Short Story Application" %></h3>

<% form_for :some_application, @some_application, :url => {:action => :update}, :html => {:id=>'page_form'} do |f| %>
  1. Provide a summary of your short story. (<%= MyModel::SUMMARY_MAX_WORDS %> words maximum)<br/>
  <%= f.text_area :summary, :rows => 20, :cols => 67 %>
  <span class="word_count_warn" id='some_application_summary_word_count_id'></span>
  <%= warn_if_word_count_exceeded('some_application_summary', 'some_application_summary_word_count_id', MyModel::SUMMARY_MAX_WORDS, :frequency => 0.10) %>
<br/><br/>
  2. Write a short story. (<%= MyModel::SHORT_STORY_MAX_WORDS %> words maximum)<br/>
  <%= f.text_area :short_story, :rows => 20, :cols => 67 %>
  <span class="word_count_warn" id='some_application_short_story_word_count_id'></span>
  <%= warn_if_word_count_exceeded('some_application_short_story', 'some_application_short_story_word_count_id', MyModel::SHORT_STORY_MAX_WORDS, :frequency => 0.10) %>
<br/><br/>
  <div id="actions">
    <%= submit_tag "submit application" %>
  </div>
<% end %>

(If you wanted to, you could change the javascript and the calling code such that it would disable the submit button if there are too many words, but for this example, it is just warning the user, that way if the javascript gets hung up and the user stops it, it won't leave the submit button disabled.)

Here is the style for the CSS:

.word_count_warn {
  display: block;
  color: red;
}

2 comments:

Gary S. Weaver said...

Updated the example to indicate that constants should be used in the model to indicate the max number of words for each answer.

Admiral Adney said...

I was exploring for ror development and alighted up on your send and i ought declare thanks for dividing such practical information.