Creating JavaScript widgets in Rails

The proverbial widget economy is running at full steam, and in order to keep up with the times I decided to create some custom JavaScript widgets for one of my Rails projects. In the end, the process came out to be remarkably simple; you should be up and running in no time as well.

Pick an architecture

The choice of how to build the widget comes down to a relatively simple factor: it all depends on the complexity of the HTML layout. If you're just trying to inject a few lines of text and don't have to worry about complex CSS structures, then you can use JavaScript to directly write into the DOM of the recipient document. Otherwise, if you need more flexibility and control (think digg buttons), then you can use the same process to create an iframe, and which will in turn load the fully styled widget in the background.

The catch is, having an iframe is great, but it results in two requests to the server: one to fetch the JavaScript file, and second to load the content of the iframe itself. It's a matter of fine balance, and you should think twice about which method you want to adopt.

Widget backend in Rails

For our purposes, we will replicate del.icio.us's widget for sharing bookmarks in the code below. Assuming we already have a Bookmark model defined, which captures the relationship between some resource and a user, we will create a separate Widget controller to handle the task at hand:

class WidgetController < ApplicationController
  layout nil
  session :off

  before_filter :validate_api_key, :only => [:user_bookmarks]

  def user_bookmarks
    # Find bookmarks assigned to owner of the API key
    @bookmarks = Bookmark.find_by_user_id(@key.user_id, :order => 'created_at desc', :limit => 10)
  end

protected
  def validate_api_key
    # Assuming we defined API_KEY_REGEX elsewhere
    return render(:text => 'Invalid API key.') unless params[:api_key] =~ API_KEY_REGEX

    # You may want to validate the key against your database and/or log the request
    return render(:text => 'Invalid API key.') if not @key = Key.find_by_hash(params[:api_key])
  end
end

For performance reasons, we want to disable sessions in this controller, and omit all layouts in the rendering stage. Since the user is not physically visiting our site when they load the widget, both of these features are unnecessary. Next, as you have already noticed, we provided a facility for verifying an API key. It's not necessary in most cases, and adds extra overhead, but it's there to illustrate a point: it's very easy to do!

Next on the agenda, we need to create and render a JavaScript function which will be executed when the user loads a page with our widget:

(function(){
document.write('<div class="customCSSclass" id="appName-widget">');

<% @bookmarks.each do |bookmark| -%>
    document.write('<a href="<%= bookmark.uri %>"><%=h bookmark.title %></a><br />')
<% end -%>

document.write('</div>') })()

Nice and simple, we're writing directly to the DOM of the recipient document. Finally, we create a route for our new Widget controller, and we're almost there:

map.connect '/widget/:action/:api_key', :controller => 'widget', :api_key => /.*/

Sharing & embedding the widget

In order to embed the widget on their site, the user has to copy and paste a JavaScript snippet into the source code of the recipient page. Hence, we create a JavaScript wrapper and point the source of the script towards our new Widget controller:

<script type="text/javascript" src="http://yoursite.com/widget/user_bookmarks/A4923D2342JF"></script>
<style type="text/css">
  .customCSSclass a{text-decoration:none;}
</style>
rails_widget.zip - Source files

When the user loads a page an automatic call is made to our Widget controller, which in turn returns a dynamically generated JavaScript function which embeds the bookmarks on the page - simple as that. As a bonus, we also wrapped our links inside a div with a custom CSS class for further tweaking and modification. Believe it or not, that's it!


Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate at Google, where his focus is on making the web fast and driving adoption of performance best practices at Google and beyond.