Space Vatican

Ramblings of a curious coder

:with or :without You: Link_to_remote's Mysterious Parameter

One of the nice things about Rails is that it makes it really easy to get off the ground with Ajax, thanks to the link_to_remote helper (and all the other functions that share the same options like remote_function, form_remote_tag, observe_field etc… [1]). The hard work is actually done by prototype but now is not the time to worry about that.

The question that comes up over and over again is how do you add extra parameters, and this is where the :with option comes into play. This is all in the docs, but it’s a little on the terse side. First things first though: if the parameters are known to you at the time when you call link_to_remote there’s no need to use :with. Just pretend it’s a link_to and do

1
2
  link_to_remote 'Click',
        :url => {:action => 'foo', :some_param => 42, :something_else => 'hello world'}

That of course is the boring case. The interesting case happens when you want to submit some javascript variables or some input elements. Take a step back and look at what remote_function actually generates. It looks a little like this

1
2
  new Ajax.Request('/dummy/foo', {asynchronous:true, evalScripts:true,
           parameters:'authenticity_token=' + encodeURIComponent('snip')})

The interesting thing here is the parameters option. It either takes a javascript object or a query string. By default all you’ll get here is the authenticity token (part of Rails’ CRSF protection), and if you’ve turned off that you’ll get nothing. Everything you stick here gets passed as a parameter to your action, and in a nutshell rails sticks the value of your :with option here.

There’s another easy case before we get stuck in. If you just want to submit the contents of the form then the :submit option is your best friend. Just pass it the id of a form, and rails will set the parameters to Form.serialize(‘some_form’) and all the inputs from that form will get sent over with the ajax request.

:with => ‘fancy pants’

In the most general case, you can pass whatever you want to :with as long as it evaluates to a valid query string (or if you don’t have forgery protection turned on any javascript object will do [2]). Object.toQueryString is your friend here, it will turn any javascript object into a query string and takes care of escaping anything. Your other important friend is Form.Element.serialize. Given a form element or id it chucks back an appropriate piece of query string. It’s even available as a method on extended elements, so instead of Form.Element.serialize(‘some_field’) you can write $(‘some_field’).serialize().

So if you’ve got a form element with id message you can create an ajax link that submits it with

1
  link_to_remote 'Click me', :url => {:action => 'foo'}, :with =>"$('message').serialize()"

If you had 2 elements you can just string them together (remember that all we’re doing here is building up a query string:

1
2
  link_to_remote 'Click me', :url => {:action => 'foo'},
                 :with =>"$('message').serialize() + '&' + $('comment').serialize()"

Personally I hate all that messing around concatenating strings and ampersands, so I would usually just write

1
2
3
  link_to_remote 'Click me', :url => {:action => 'foo'},
     :with =>"$H({message: $F('message'), comment: $F('comment'), 
        fromUser:prompt('Tell me something')}).toQueryString()"

What we’ve done here is build up a prototype hash (that’s the $H({…}) bit) and then called toQueryString on it, which does exactly what it says. $F is another prototype helper that, given an id, returns the value of the associated input element. The last part shows that we can do anything we want really. Here I’m asking the user to provide some text, but it could be some pre-existing javascript variable or anything you want.

If you decide not to us the various prototype serialisation helpers then you do need to be a little careful: it’s up to you to ensure that what needs to be escaped is escaped. A handy function here is encodeURIComponent:

1
2
  link_to_remote 'Click me', :url => {:action => 'foo'},
     :with =>"'thing='+encodeURIComponent('I am a nasty & funky string')"

This ensures the ampersand is encoded rather than messing up everything. You can of course combine that with some of the other tricks:

1
2
  link_to_remote 'Click me', :url => {:action => 'foo'},
     :with =>"'thing='+encodeURIComponent(someFunctionReturningAString())"

observe_field is a naughty boy

Everything I’ve said holds true for observe_field, but observe field allows you to take some short cuts. If what you pass to :with doesn’t look like a valid query string or interesting javascript expression then rails assumes you’re just specifying what name you want the parameter to be submitted as, so

1
  observe_field 'some_field', :with => 'q'

expands to

1
  observe_field 'some_field', :with => '"q="+value'

This is executed in a context where value is the new value of the form element. Can you spot the problem yet? You need to return a valid query string and value is just the value of the field. By some good fortune you get away with this right until you type an ampersand (I’ll file a ticket on thisit was fixed here, so edge and rails 2.1 are ok). It’s not escaped and so your query string is mangled. Until this is fixed you should use something like

1
  observe_field 'some_field', :with => '"q="+encodeURIComponent(value)'

[1]The canonical function is actually remote_function, everyone else just uses its output. For example form_remote_tag is basically just a normal form whose onSubmit is the output of remote_function. As far as the docs go though, all the goodies are under link_to_remote.

[2]Prototype is smart enough to know that if you pass an object as the parameters option it should call Object.toQueryString on it, so for example parameters: {a: 2+2, b: “hello”} would do the right thing. The reason forgery protection makes a difference is that rails assumes that your :with option evaluates to a query string and appends to that authenticity_token=xxx.