@suggestions   @rss   @archive   @codeforpeople.com     @random   @radio[:m3u|:pls|:ruby]   @family   @neighbors  @twitter 



Reinventing Rails’ Components

Lately I’ve been hacking on some Rails code for http://eparklabs.com. Dan, the guy that runs it, is unhappy about the state of affairs with Rails’ views. Of course he’s not alone - many people out there are looking for more powerful view abstractions. Just the other night I was down at BDRG and listening to Bruce Williams talk about his new Stencil plugin for Rails. Basically it addes a little state, or context, to rendering.

Why is this good?

Because state + funcionality == objects! Now, I am not, for one second, saying that objects are a panacea, but they have proven to be an extremely useful paradigm for solving many computer programming problems. in Rails, we do not use objects to contruct our views. “What!?”, you say. Yes, it’s true. Ruby may be object oriented. Rails may even be object oriented. But Rails’s views are not themselves object oriented. In fact, they are almost purely procedural. Partials, helpers, and even componets simply allow us make layer upon layer of functions to call, each of which simply belches out a bunch of html. All html generation in Rails basically boils down to this:

class View
  def to_html
    header + body + footer
  end
  def header
    <header/>
  end
  def body
    <body/>
  end
  end
  def footer
    <footer/>
  end
end

Now, some people will object that, in fact, the situation is more like this:

class Controller
  def initialize
    @header = header
    @body = body
    @footer = footer
  end
  def action
    View.new.to_html self
  end
end

class View
  def to_html controller
    controller.instance_eval{ header(@header) + body(@body) + footer(@footer) }
  end
  def header content
    <header>#{ content }</header>
  end
  def body content
    <body>#{ content }</body>
  end
  def footer content
    <footer>#{ content }</footer>
  end
end

And that this is clearly a case where state and functionality are being wrapped in an object. Sadly, this is only an illusion. The reason I say that is this: when a request comes into Rails a controller instance is created, this controller has state and executes logic before handing of a bit of munged state to a view for rendering. Seems pretty encapsulated right? Before you answer, what would you say about this program:

$global = 42

def foo
  p $global
end

Probably that it’s creepy seeing that global variable sitting there right? That it’s clearly not OO? Now, how about this program:

var = 42

def foo var
  p var
end

Ok better. At least we’re procedural now. None of those freaky global variables. We can, of course, do better:

class C
  def initialize
    @var = 42
  end
  def foo
    p @foo
  end
end

And this is starting to look pretty OO, pretty much like Rails. Here is the rub: there is only one controller in effect at a time in Rails and that controller’s data is, effectively, global for all rendering actions:

class Controller
  def initialize
    @highlander = 42 # there can be only one!
  end
  def foo
    render # all views will have access to @highlander - just like a global var
  end
end

Now, if Rails had something that resembled an OO view we might be used to seeing code that looked vaugely like this:

class Controller
  def initialize
    @var = 42
  end
  def foo
    widget = Widget.new :var => @var
    widget.width = 42%
    widget.height = 100%
    render widget.to_html
  end
end

Ah ha! Components you say! Close, but no banana. While it’s quite true that we can parameterize a controller while reusing it’s logic and rendering abilities via:

class Controller
  def foo
    render_component bar, :params => {4 => 2}
  end
end

We can see right away that this too, is a simple procedural call. It is true that the call chain for render_component will construct an object and parameterize it along the way, but this a one shot deal whose end result is exactly like:

class Controller
  def foo
    method_that_belches_html bar, :params => {4 => 2}
  end
end

Which is to say we don’t get a handle on an object or two, have a go at munging their states, and letting them do the html belching themselves based those (altered) states.

DHH’s views on components, which, if you haven’t guessed, is what we are going to be talking about here, are well known and I tend to agree with him. I do, however think that there is a big difference between what we can call ‘fat’ components and ‘thin’ ones. The essential issues with components are precisely those we have in object oriented languages: it’s very, very hard to make good re-usable ones. However, I’ll take a language with objects over one without any day.

So the basic issue I see with components in Rails as they stand now - is that they are not components! The current implementation provides nothing more that a function call with state, although that state is contained an object rather than inside a closure and the rendering function may be broken up into many sub-functions via helpers, partials, etc. What if we could remedy that? What if we made good use of the ability for a controller to bind together state (via the model), logic, and view into a tidy bundle? If we did, we might be able to do something like this:

class BarController < ApplicationController
  def foo
    render :text => @foo
  end
  def bar
    render :text => @bar
  end
end

And then:

class FooController < ApplicationController
  def bar
    # get a handle on, and parameterize, a bar controller
    bar_controller = component_for(:controller => bar) do
      @foo = 42
      @bar = forty-two
    end

    # call two actions on the bar controller, potentially reusing the entire model+view+controller stack
    foo = bar_controller.content_for :action => foo
    bar = bar_controller.content_for :action => bar

    render :text => [foo, bar].inspect #=> [42, “forty-two”]
  end
end

Wouldn’t that be nice?

Here is a sneak-peak at my soon to be released ‘componentry’ lib. There’s not too much to it, but it allows all this and more.

module ActionController
  module Components
    module InstanceMethods 
      # returns a child component with both initializaion and defered-rendering ability
      def component_for(options, &block)
        options.to_options!
        klass = component_class(options)
        parent_controller = options.delete(:parent_controller) || self

        returning(klass.new) do |controller|
          (class « controller; self; end).class_eval do
            define_method(:parent_controller){ parent_controller }

            define_method(:content_for) do |options|
              options.to_options!

              parent_controller.send(:component_logging, options) do
                request = parent_controller.instance_eval do
                  request_for_component(klass.controller_name, options)
                end

                reuse_response = options.delete(:reuse_response)
                response = reuse_response ? parent_controller.response : parent_controller.response.dup
                response = process(request, response)

                if redirected = response.redirected_to
                  content_for(redirected)
                else
                  response.body
                end
              end
            end
          end

          controller.parent_controller = parent_controller
          controller.instance_eval(&block) if block
        end
      end
    end
  end
end

Enjoy.

Comments (View)
blog comments powered by Disqus