Agile Ajax

Hide, Seek, and Stay Dry, part two: Controllers

Tasker_ Dashboard.jpg

When last we discussed this, I was adding some hide-and-show functionality all over a Rails web site. In Part 1, I worked over the view code, trying to transform the HTML into a series of Rails helpers that allowed any block of code in view to have hide-and-show functionality just by surrounding the block with a helper call.

This time around, we'll cover the controller side, and when we're done -- SPOILER ALERT -- any controller can become a hide-and-show server with as little as a single line of code.

In order for a controller to manage the hide and show, it needs two method. One of them will obviously be called hide, the other can't be called show, as that's already reserved by Rails and will cause nasty name crashes if you try it. I've gone with reveal in this code (I didn't think of seek until after I was done with the code, and in any case, seek has other connotations in a computer context...)

My first stab at these controller methods was for a controller where the blocks being controlled were inside a loop and each had a specific object associated with it. All the hide method needs to do is blank out the container part of the hide seek and redraw the header to use the other version of the icon. The header redraw is managed using the same helper methods defined in part one of this post. Like so:


def hide
@project = Project.find(params[:id])
render :update do |page|
page.replace_html dom_id(@project, :container), ""
page.replace_html dom_id(@project, :display),
collapse_expand_header(false, @project, "#{h @project.longname} Project")
end
end

Couple notes: I realize that the caption was moved to a header in the view part of this post... bear with me, I'll get there. Also, you could make the only updating in the header be the image itself, rather than the image and text -- I included the text to allow for the possibility that the text might change based in the visual status of the block. You could probably also do this by making the block invisible via JavaScript.

The reveal method is similar, but is responsible for gathering any data needed to draw the code again, and also for rendering the right partial. Hmm, on reflection, changing a JavaScript property might be cleaner... Anyway, here's the reveal method, with some of the details of gathering the data elided.

def reveal
@project = Project.find(params[:id])
@things = @project.things
render :update do |page|
page.replace_html dom_id(@project, :container),
:partial => 'one_project',
:locals => {:project => @project, :things => @things}
page.replace_html dom_id(@project, :display),
collapse_expand_header(true, @project, "#{h @project.longname} Project")
end
end

In trying to convert this to a general feature, the two questions are What parts are specific to each individual application, and How best to account for those differences?

In this case, trying to adapt this code to the next controller revealed the following points of freedom.

  • The class of the object passed as the id, which can be an ActiveRecord or a String in this case.

  • The name of the partial view to be rendered

  • The data passed to that partial view in its locals hash.

After writing the nearly identical methods in the second controller, I was ready to refactor. I decided to use some metaprogramming to build the methods. I added the following class method to ApplicationController. It builds the hide and reveal method, given one argument: the class of the expected object being passed in the id parameter.

def self.hide_and_reveal(klass = nil)
define_method(:hide) do
obj_from_id(klass)
render :update do |page|
page.replace_html smart_dom_id(@obj, :container), ""
page.replace_html smart_dom_id(@obj, :display),
collapse_expand_content(false, @obj)
end
end

define_method(:reveal) do
obj_from_id(klass)
locals = reveal_locals(@obj)
partial = reveal_partial(@obj)
render :update do |page|
page.replace_html smart_dom_id(@obj, :container),
:partial => partial, :locals => locals
page.replace_html smart_dom_id(@obj, :display),
collapse_expand_content(true, @obj)
end
end
end

Both hide and reveal are built inside define_method blocks. The hide part is almost identical, except that the object initialization is how handled by the obj_from_id method, which takes the class into account. If there's no class, it assumes the id is a string and returns it directly. Also, the smart_dom_id method defined in the previous blog post is used to allow for string objects:

def obj_from_id(klass = nil)
@id = params[:id]
@obj = if klass then klass.find(@id) else @id end
end

In the reveal method block, the name of the partial and the definition of the locals are offloaded to other methods. Defaults are provided, but the expectation is that a participating controller will override one or the other method:

def reveal_locals(obj)
{}
end

def reveal_partial(obj)
'reveal_partial'
end

There are various other possibilities for this involving having the locals and partials passed as arguments or a block to hide_and_reveal, but in the end I decided this version was the most readable.

One thing to notice, though, is that the locals and partial name need to be assigned outside the render block. I believe that instance_eval or a similar trick is used to change the context inside the render block, with the upshot that controller methods aren't available to self inside the block. Again, that probably could have been worked around with some other tricks, but the version as written seemed clearest.

With that refactoring, the initial hide and reveal methods are replaced by the following:

hide_and_reveal(Project)

private

def reveal_locals(obj)
@things = obj.things
{:project => obj, :things => @things}
end

def reveal_partial(obj)
'one_project'
end

I like that. The hide_and_reveal method isn't completely general, but I was able to add it to the remainder of my controllers and views without further changes to either the view helpers or the controller method (although one case where I had to pass an ID and a type was a bit of a hack).


If you liked this, you might also enjoy my book Professional Ruby on Rails.

Topics:

Leave a comment

Powered by WP Hashcash

About Pathfinder

  • We design and build extraordinary applications for companies looking to make the next great idea a reality.
  • learn more

Topics

WordPress

Comments about this site: info@pathf.com