Robot Has No Heart

Xavier Shay blogs here

A robot that does not have a heart

Unobtrusive live comment preview with jQuery

Live preview is shiny. First get your self a URL that renders a comment. In rails maybe something like the following.

1
2
3
4
5
6
7
8
9
def new
  @comment = Comment.build_for_preview(params[:comment])

  respond_to do |format|
    format.js do
      render :partial => 'comment.html.erb'
    end
  end
end

Now you should have a form or div with an ID something like “new_comment”. Just drop in the following JS (you may need to customize the submit_url).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$(function() { // onload
  var comment_form = $('#new_comment')
  var input_elements = comment_form.find(':text, textarea')
  var submit_url = '/comments/new'  
  
  var fetch_comment_preview = function() {
    jQuery.ajax({
      data: comment_form.serialize(),
      url:  submit_url,
      timeout: 2000,
      error: function() {
        console.log("Failed to submit");
      },
      success: function(r) { 
        if ($('#comment-preview').length == 0) {
          comment_form.after('<h2>Your comment will look like this:</h2><div id="comment-preview"></div>')
        }
        $('#comment-preview').html(r)
      }
    })
  }

  input_elements.keyup(function () {
    fetch_comment_preview.only_every(1000);
  })
  if (input_elements.any(function() { return $(this).val().length > 0 }))
    fetch_comment_preview();
})

The only_every function is they key to this piece – it ensures that an AJAX request will be sent at most only once a second so you don’t overload your server or your client’s connection.

Obviously you’ll need jQuery, less obviously you’ll also need these support functions

1
2
3
4
5
6
7
8
9
10
11
12
13
// Based on http://www.germanforblack.com/javascript-sleeping-keypress-delays-and-bashing-bad-articles
Function.prototype.only_every = function (millisecond_delay) {
  if (!window.only_every_func)
  {
    var function_object = this;
    window.only_every_func = setTimeout(function() { function_object(); window.only_every_func = null}, millisecond_delay);
   }
};

// jQuery extensions
jQuery.prototype.any = function(callback) { 
  return (this.filter(callback).length > 0)
}

Viola, now you’re shimmering in awesomeness. Demo up soon, but it’s similar to what you see on this blog (though this blog is done with inline prototype).

DOM Quirks

Unobtrusive javascript is undoubtably the nicest way to add Javascript behaviours to a web page. It keeps the HTML clean and (hopefully) ensures it will degrade properly in older browsers. That said, the methods you generally use for this type of design (see Unobtrusive Javascript for an excellent introduction) contain a number of quirks you should be aware, of which this article addresses a few. In particular, unexpected or non-obvious behaviour in createElement, appendChild, and getElementsByTagName.

Table of Contents

  1. Creating Elements
  2. Appending Elements
  3. Finding Elements
  4. Conclusion

Creating Elements

The createElement function allows the dynamic creation of HTML elements. It takes one parameter: the type of element to create. It is used in conjunction with setAttribute to modify the attributes of a new element. Elements created in this way will not actually be displayed in the document until added with appendChild, insertBefore or replaceChild. The following code creates an image (but does not display it):

1
2
element = document.createElement("img");
element.setAttribute("src", "img1.jpg");

While support for this is good in the major browsers, there is a small quirk in IE that can cause some pain when creating forms. To quote MSDN:

Attributes can be included with the sTag as long as the entire string is valid HTML. You should do this if you wish to include the NAME attribute at run time on objects created with the createElement method.

What this means is that in IE, you can do the following (which is equivalent to the above snippet of code):

1
2
str = '<img src="img1.jpg" />';
element = document.createElement(str);

While IE supports the first method shown for most attributes, if you want to set the “name” attribute of an element you must use the second method. This is a problem since Mozilla will throw an exception on the latter. Thankfully, we can use exception handling for an easy workaround:

1
2
3
4
5
6
7
8
try {
  str = "<input name='aradiobutton' type='radio' />"
  element = document.createElement(str);
} catch (e) {
  element = document.createElement("input");
  element.setAttribute("name", "aradiobutton");
  element.setAttribute("type", "radio");
}

Appending Elements

Using appendChild (or replaceChild) is the “correct” way to add content to a DOM, rather than the more popular innerHTML property.

When using this function to add rows to a table, you should add the rows to a tbody or equivalent tag inside the table, not the table tag itself. Mozilla and Opera will pick up the new rows if you add them directly to the table tag, whereas IE will not.

Finding Elements

You can get a collection of all tags of a specific type using the getElementsByTagName function. Not only is this handy for standard unobtrusive javascript behaviours, you can also use it to do cool things like automatically process all elements in a form.

1
2
3
4
5
6
7
8
function showData(form) {
  inputs = form.getElementsByTagName("input");
  buffer = "";
  for (i = 0; i < inputs.length; i++)
    buffer += inputs[i].name + "=" + inputs[i].value + "\n";

  alert(buffer);
}

Although it may appear to act like an array, it is very important to remember that the returned object is actually an HTMLCollection. It does not support any array-like functions (concat, splice, etc…) bar those presented above. This is because the HTMLCollection is a live representation of the page’s HTML, and such functions would interfere.

1
2
3
4
5
// Assume an empty document
images = document.getElementsByTagName("img");  
// images.length = 0
addImgElementToDocument(); // function implemented elsewhere 
// images.length = 1;

This can be an annoyance when we know that the HTML structure will not be changing, and is easily worked around:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function collectionToArray(col) {
  a = new Array();
  for (i = 0; i < col.length; i++)
    a[a.length] = col[i];
  return a;
}

function showData(form) {
  elems = form.getElementsByTagName("input");
  inputs = collectionToArray(elems);
  elems =  form.getElementsByTagName("select");
  inputs = inputs.concat(collectionToArray(elems));
  buffer = "";
  for (i = 0; i < inputs.length; i++)
    buffer += inputs[i].name + "=" + inputs[i].value + "\n";
        
  alert(buffer);
}

It would be nice if the collectionToArray function above could be added to HTMLCollection’s prototype, however for some reason it is read-only.

Conclusion

These quirks may be minor and their solutions trivial, but it helps to be aware of them when coding any sort of unobtrusive javascript as it can reduce the amount of time you spend debugging seemingly illogical behaviour.

A pretty flower Another pretty flower