Niklas' Code Salad

ideas, code and rants

Arranged Marriage Between Draper and RJS

| Comments

How to solve the problem of Javascript-rich interfaces with a Rails backend.

RJS is dead

Do you remember RJS? It was introduced somewhere around version 1 of rails (or even earlier, please correct me) and provided a template language in pure ruby to respond to AJAX actions.

app/views/posts/update.js.rjs
1
2
page.replace_html 'posts', :partial => 'list'
page.visual_effect :highlight, 'posts'

This was still during the benign reign of Prototype and the template would have been called update.rjs. There also was a way of rendering Javascript directly from your controller actions:

app/controllers/posts_controller.b
1
2
3
4
5
6
7
8
9
10
class PostController < ApplicationController # no inherited resources or similar
  def create
     # update the model ..
     if request.rjs?
       render :update do |page|
         page['posts_count'].replace Post.count # this would target element #post_count
       end
     end
  end
end

The significant part here is the page object, which magically generated Prototype-JS for us. But over time, RJS became deprecated. I did not follow the proceedings, so I do not know the reasons. Maybe it tempted to pinch the MVC layer a little bit too much (Controller example above) or with the rise of jQuery (yeah!) it lost its primer target. Or there was to much magic involved.

I admit, if not properly cleaning up after every commit, RJS templates could become a mess. Additionally, the interaction with helpers was strange. You could call a helper on the page object, but from within this helper, the page object was only accessible through hoops. So no real refactoring was possible.

Until now!

Long live RJS!

I think the current approach of generating JS from Rails is tedious. lala.js.coffee.erb looks nice in theory, but in practice this is a mess. My HAML-coddled eyes don’t want to see no angle brackets no more! Manual escaping of HTML? Letting Rails serve only JSON on the other hand makes a full-blown Rails stack kind of obsolete.

With the recent occurrence of Presenters like Draper or your own quickly setup presenter, it now becomes possible to write Rails applications controller their browsed views by Javascript. I will show my findings with the first, but it may easily achieved with every other class following decorator pattern (TM).

Add draper and versatile-rjs to your Gemfile to resurrect that magic zombie.

Gemfile
1
2
gem 'draper'
gem 'versatile_rjs'

Open a bottle of champagne, dim the light, and initialize first contact.

Put this into an initializer or your favored monkey patch cage and require it once during application startup.

And it may prevail

As demonstrated in the comment of above gist, you can still know extend the page proxy, but use the presenter around it. And because it uses jQuery (of course), the calls to it can be chained. In our code we use symbol-based lookup for elements on the page. Here is some example code; some endpoints are still missing, but you’ll get the idea.

application_decorator.rb
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
29
30
31
32
class ApplicationDecorator < Draper::Base
  # You may implement #selector_for in your subclass (don't forget to call super in the else)
  def selector_for(name, resource=nil, *more)
    case name
    when :errors_for
      %Q~#{selector_for(:form_for, resource)} .errors~
    when :form_for
      if resource.to_key
        %Q~form##{h.dom_id resource, :edit}~
      else
        %Q~form##{h.dom_id resource}~
      end
    else
      raise ArgumentError, "cannot find selector for #{name}"
    end
  end

  # select a specific element on the page.
  def select(*a)
    page.select selector_for(*a)
  end

  def remove(*a)
    select(*a).remove()
  end

  # append validation errors for given `resource` to its form
  def insert_errors_for(resource)
    select(:errors_for, resource).remove()
    select(:form_for, resource).append errors_for(resource)
  end
end
fnord_decorator.rb
1
2
3
4
5
class FnordDecorator < ApplicationDecorator
  def update_box_for(resource)
    select(:box, resource).html render_the_resource_with_partial_or_whatever
  end
end
create.js.rjs
1
2
3
4
5
6
7
8
page.decorate resource do |fnord|
  if resource.errors.empty?
    fnord.update_box_for(resource)
    fnord.remove :form_for, resource
  else
    fnord.insert_errors_for(resource)
  end
end

So…

There we go, finally a JS generating interface that looks clean, removes duplication and uses all the Rails’ features.

Comments