Space Vatican

Ramblings of a curious coder

Conditional RJS Explained

RJS is one of those nifty little things that catches your eye when you first start off using Rails. You very quickly get to add ajax and whizzy effects without having to peer into the somewhat messy world of javascript. Some would argue that it’s a bit of a crutch and you’re better off casting it away and just writing the javascript yourself. To an extent I agree, but I do think it has its place (and also its shortcomings). One of the problems with RJS is that it creates a convenient illusion and it can occasionally catch you out unless you understand that illusion and its limitations.

A question I frequently see is along the lines of ‘I want my rjs to do x only if y is not already on the page’. For example if some element is on the page you want to highlight it, if not you want to create it. People often come up with something like this as their first attempt:

1
2
3
4
5
  if page['some_div'].visible
    page['some_div'].replace :partial => 'some_partial'
  else
    page.insert_html :after, 'something_else', :partial => 'header'
  end

It’s important to understand that this cannot possibly ever work, not in a month of mondays or without destroying the fabric of space time. Until you understand this (and I’ll try to explain) you haven’t understood rjs and you will be confused. More generally if the conditions you are interested are client side (is this div there, is x in the text box etc…) then they can’t be handled by a plain old if in your rjs.

RJS in 5 minutes

RJS allows you to write ruby instead of javascript. Unfortunately browsers don’t support ruby as a scripting language, and so that ruby is transformed into javascript. The way it works (simplifying horribly) is that an instance of JavascriptGenerator (the page object you use in a a rjs file or a render :update block) responds to methods by outputting the appropriate javascript (or another proxy object).

For example page[‘some_div’] creates a proxy for the dom element with that id. If you call a special method on it (like render, visual_effect, insert, [] etc…) then rails will generate the appropriate bit of javascript. If you call anything else, then rails assumes you wanted to generate that javascript function call and does that for you (it converts the arguments for you, so strings are escaped properly, hashes are converted to json objects etc…).

So, some examples:

1
2
3
4
  page.select('.alert').each {|element| element.hide()}
  page['customers'].setStyle(:display => :inline, :padding => '1ex')
  page['header'].replace :partial => 'header'
  page['customers']['style']['display']='none'

these generate the following javascript:

1
2
3
4
$$(".alert").each(function(value, index) { value.hide(); });
$("customers").setStyle({"padding": "1ex", "display": "inline"});
$("header").replace("Contents of the partial");
$("customers").style.display = "none";

However you can’t write

1
page['customers'].select("li:not('.active')").each {|element| page.hide }

(hide all children of $(‘customers’) that are list elements without the class active) because the proxy doesn’t know how to proxy it. Not everything works completely seemlessly, so if something doesn’t work there’s a chance that RJS just doesn’t understand what you’ve trying to do. You can normally work you way out of it, in this case

1
2
page['customers'].select("li:not('.active')").each(
    page.literal 'function(element){element.hide()}')

will do the trick (page.literal dumps a literal bit of javascript), the question is whether it’s worth the effort or whether you’d be better off writing raw javascript instead of trying to bludgeon RJS into doing what you want.

R is for Ruby

Your rjs file is just normal ruby, so you can do things like

1
2
3
4
5
6
  if @divs_to_hide
    page.hide(@divs_to_hide)
  end
  if @element_to_remove
    page[@element_to_remove].remove()
  end

Which brings us to the main point: since rjs is just regular ruby (there’s just that special local variable), the if is a regular ruby if. It’s evaluated server-side, and so it’s just plain not possible for it to refer in any meaningful way to something client side.

A new hope

There is a way out though. What we want is a javascript if – so just generate one!

1
2
3
4
5
  page << "if($('some_div').visible()){"
  page['some_div'].replace :partial => 'some_partial'
  page << '}else{'
  page.insert_html :after, 'something_else', :partial => 'header'
  page << '}'

which generates

1
2
3
4
5
if($('some_div').visible()){
  $("some_div").replace("Contents of your partial here");
}else{
  new Insertion.After("something_else", "Contents of your other partial here");
}

Not very pretty [1], but it gets the job done. page<< inserts into the generated javascript the string you pass, we’ve just wrapped our other bits of javascript with some if statements and associated punctuation. Both partials are rendered and stuffed into the javascript and sent to browser (since we can only choose what to use client side) so this wouldn’t be very efficient if you were deciding which of 2 enormous bits of html to insert.

Another trick

It occured to me recently there’s another trick we can play as long as you can express your condition as the existence of an element specified by a css selector:

1
2
3
4
page.select('#foo').each do |element|
  element.highlight
  page.alert('Foo is on the page')
end

This will highlight the element with id foo and display a message if that element exists (I could have used page[‘foo’] in the block if I had wanted to).

As described above, with select we can iterate over a collection and this is just a special case. The collection of items with id foo should contain precisely 1 or 0 elements, so iterating over it is the same as testing whether #foo exists. You can of course put anything you want in this block. The same caveats about all your partials etc… being rendered whether they are used or not still apply and there is the additional limitation that you can’t express things like “do x if y doesn’t exist”.

Hopefully that’s cleared things up a bit. If you want to know more about the magic behind RJS, take a look at protototype_helper.rb in action_view/helpers, it’s where all this stuff lives.

[1] There’s a plugin that tidies this up.