May 22, 2011

Rails, REST, and .js (TL;DR: We’re doing it wrong)

Filed under: ResearchAndDevelopment — Ryan Wilcox @ 10:32 pm

In my last blog article I talked about Returning HTML content from AJAX requests. However, I’ve been struggling with a question since then:

I asserted (implicitly) that the following (and canonical!) pattern for doing AJAX replacement is wrong. I felt this strongly, but why is it wrong?

First, the current pattern

Normal Rails pattern for HTML replacement is to make an AJAX call to the .js format, in which we return something like the following back to the browser:


<%# show.js.erb %>
var str = '<div id="<%= dom_id my_object -%>" >
Name : <%= my_object.first_name -%>
</div>';
$("#destination_div").replace(str);

To this I say Ugh.

Why Ugh? Why is it wrong to return this in the .js format?

Why? The DOM transform operation above isn’t the representation of the object

Let’s review what Rails actions do in the respond_to block

  • format.html returns a HTML representation of the object (for display on screen) — aka: it renders the view
  • format.json returns a JSON representation of the object
  • format.xml returns an XML representation of the object

These are all pretty normal in the Rails world. The last two take the object and serialize it (using .to_json or render :xml => object. Straight from rails generate scaffold

You might imagine (from DHH’s RailsConf keynote introducing Rails 2) that other formats existed, with mostly the same result:

  • format.icl might return the iCal representation of the object
  • format.csv might return a CSV representation of the object

But in the canonical Rails show.js.erb file at the beginning of this blog post, the .js is not returning the Javascript representation of the object.

Can I do this with your object?


$.ajax(
...
success: function (data, textStatus, xhr) {
var obj = eval(data);
alert(obj.first_name); /* attribute, like JSON */
alert(obj.api_function() ); /* and here's a function call! */

Answer: No, because your .js “representation” is some DOM injection of an unparsable HTML string.

A pattern for returning an actual Javascript Object

If you return an actual Javascript representation of the object, you have the same data attributes available to you on the JS side as you do on the Ruby side.

For example, if your .js format rendered a .js.erb template like this:

Yes, this smacks on the If you’re using to_json, you’re doing it wrong article by the Gomiso guys. I absolutely agree with their core idea of maybe you should be using views to construct your json objects!

Notice here, we are returning a Javascript object, with data and methods that encapsulate functionality … which is exactly what objects are.

I also know that you use a Ruby object in different contexts than you do a Javascript object. In Ruby I want to perform my business logic – computing priority of due dates, in the case of my Todo object here. In Javascript I care about different operations – perhaps mostly related to AJAX and DOM manipulation.

For example, I’ve always struggled with getting my Rails routes into my unobtrusive Javascript. In Ruby (or .erb code) I use routes and path helpers like I should… and on the Javascript side I hardcode the routes. Heaven help me if I ever have to change the routes – I’ve solved only half the problem!

Since I implemented a routes method in my JS object above, the route will always be correct – because I’m using the normal Rails routes helpers to generate it!

In Rails 3.0.x, it’s non-obvious to keep the Javascript representation of an object in show.js.erb. This location is certainly it’s not where I would look for the source code to the object! Rails 3.1, I believe, will save this day. Imagine a Rails 3.1 app that uses the assert pipeline to store the Javascript representation in the following hierarchy:


app/
assets/
javascripts/
models/
todo.js.erb

Much better!

To Review

  1. The normal Rails pattern of returning DOM manipulation functions as the Javascript representation of the object is not RESTful
  2. … and inconsistent, because every other format (.json, .xml) returns the representation of the object
  3. We should return actual Javascript objects – data and methods to act on that data

So, how do we do AJAX replacement then?

If we can no longer use the .js format to return a DOM manipulation function, how do we return chunks of HTML to the browser (for a standard “AJAX request and replace” type functionality)?

Answer: Request a partial HTML representation of your object

My Returning HTML content from AJAX requests previous article

covers the how very well, but there are some other advantages:

  • We’re returning proper representations for proper formats
  • The Javascript that originally makes this AJAX call knows more about the structure of the DOM – and where it wants the result – than a show.js.erb that lives on the other conceptional side of the app

May 12, 2011

Returning HTML content from AJAX requests – a pattern for Rails 3

Filed under: General Information,ResearchAndDevelopment — Ryan Wilcox @ 5:25 pm

The problem: semantic formats for returned HTML for AJAX

In a previous Rails 2 project, we decided that Rails apps return 3+ kinds of content:

  • A complete HTML page, for user viewing
  • JSON (for JS/web API viewing)
  • A partial HTML page, for jQuery DOM swapping/

However, it was hard to know when to return a full HTML page, and when to :layout => false

So we invented a semantic format

Huh?

Rails actions typically go something like this

# from todo.rb

def show
@object = Todo.find(params[:id])
respond_to do |format|
format.html { render "show" } # being explicit here, render not really required
format.json {@object.to_json}
end
end

So, if the format.html gets called, how do you know if you should show the whole page, or just some partial page update (for example, to update the Todo item on the screen for some AJAX effect or another?

The solution

The solution was – for the Rails 2 project – to use a custom MIME type to identify when we wanted a snippet of HTML. Our AJAX requests looked like:


$.ajax({
url: "/todos/" + id,
beforeSend: function(xhr){ xhr.setRequestHeader("Accept", "text/html-partial") },
success: function(){....}

That Accept parameter set up the MIME type, and we added it to our config/initializers/mime_types.rb file and we were ready to rock

It’s not so simple in Rails 3

Setting up the MIME type

Rails, until about October 2010, didn’t respect MIME types that well. I believe that it would take this MIME type and return text/html

Rails 3 takes the MIME type, and returns it. So even if you got the rest of the example up, your browser would complain because it doesn’t know what to do with a text/html-partial MIME type.

So, instead of a MIME type, it’s best to use a format parameter, for Rails 3

Set up your config/initializers/mime_types.rb file to contain:


Mime::Type.register_alias "text/html", :partial

This is essentially just an alias for another MIME type (text/html).

So, we’re going to use a format

Because we don’t want the user to see “Unknown MIME type, save or open?” in their browsers, we are going to cheat a little bit and specify our format via an extension.

Our Javascript code should now look like


$.ajax({
url: "/todos/" + id + ".partial",
success: function(){....}

Your actions, knowing about full page HTML, and AJAX HTML

Now you need to set up your actions

# from todo.rb

def show
@object = Todo.find(params[:id])
respond_to do |format|
format.html { render "show" } # being explicit here, render not really required
format.partial { render "show", :layout => false }
format.json {@object.to_json}
end
end

Except we have a problem. If you actually try this out, you will get a Rails Missing Template error

Rails 3 takes the MIME type into consideration when constructing the template path/name to render.

format.html { render "show" }

— Rails looks for todo/show.html

format.partial { render "show", :layout => false }

— Rails looks for todo/show.partial

Now, ideally we want to share as much HTML as possible, so we have a problem

In my use case today, I was actually rendering a Rails partial – a partial that was also used in non Ajax situations. So renaming my file to be _something.partial.erb just wasn’t going to cut it

Actually, this behavior does introduce an interesting side effect: being able to further isolate snippets returned for AJAX vs full page reload requests, if the situation demands it.

But my situation demanded sharing. I’m sure separating things out will work for some requests, and it’s a handy thing to have… but most of the time I want Rails to read from the blah.html.erb file.

Setting up a Rails 3 ActionView::FileSystemResolver

So I decided to go monkey patching Rails, to provide the support I wanted.

Before I banged my head on my desk too hard, I found the following article on using FileSystemResolver for some other things:

Implementing A Rails 3 View Resolver

My resolver is very similar to that one:

Conclusion

  • We have a way to separate full page HTML requests from AJAX requests that only want a specific section of a page
  • We use the less magical extensions to provide formatting, instead of MIME types
  • We can provide AJAX specific templates if we want (blah.partial.erb
  • We fall back to .html if nothing specific exists, because we really want to share HTML code between a full page redraw and an AJAX redraw

May 6, 2011

Subcontractors Wanted

Filed under: General Information — Ryan Wilcox @ 8:49 pm

Sometimes I find myself with more work than I can handle, or internal work I want done but can’t do myself.

Today, I decided to fix that problem

I’ve created a subcontractor survey on my website. If you do consulting work (mostly around the web sphere: Ruby on Rails, Django, front end design work using CSS frameworks and Javascript.)

How It Works

When I get a new project in I’ll evaluate if I have to pull in a subcontractor, and then look at my list of subcontractors for possible matches.

If I think your skillset and availability match with the needs of the project, I’ll send you an email asking about your availability. Evaluate carefully if you have time, or if it’s not going to work out, and respond.

If things work out, I’ll collaborate with you to pull together a quote for the project, or for a iteration worth of work.

Why

I want to make sure I serve my existing clients my giving them work done to the best of my ability. I also (right now) have more things I’d like to do than I can actually get done.

Bidding things out on elance.com (or similar sites) piecemeal doesn’t suit me – I want to make sure I have high quality code going out of my company.

I need a list of subcontractors I can trust, thus this list

Why wait?

The survey takes 5 minutes – complete the subcontractor survey!