We're hiring!

We're actively seeking developers & designers for our new Detroit location. Learn more

Announcers: A layer between the controller and rendering

For my last Rails project, we went to a completely Ajax interface for everything. This meant that nearly every action on a controller would result in several lines of RJS calls, along with the logic of which partial to render depending on the state of updating or creating an object.

All that logic and RJS stuff was a pain to test in a controller spec, because so much setup is needed every time you call a controller action in a test (authentication, session, etc). Any branching you have in a controller action really becomes a nuisance to test, and when the customer wants to be able to change what happens on the page when a user saves some object, rewriting those tests and the controller are even less fun.

So we came up with the idea of moving all the presentation related logic out of the controller and into helper objects we call announcers. The purpose of an announcer is to make the actual rendering calls, based on state that is passed to it by the controller.

The controller actions in most cases become two lines of code. Perform the action (create, destroy, etc.) and then tell the announcer what happened with the results. It is left to the announcer to decide what rendering should result.

As an example, here is an update action, and update announcer for that action:

PanelsController : update

1 2 3 4 5 
  def update     @panel.update_from_params params[:panel]     @panel.reload     @panel_update_announcer.panel_updated :panel => @panel, :controller => self   end

PanelUpdateAnnouncer

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 
class PanelUpdateAnnouncer   def panel_updated(opts = { })     panel, controller = opts.values_at! :panel, :controller     controller.responds_to_parent do       if panel.valid?         controller.replace("panel_#{panel.id}", "panels/panel", :locals => { :panel => panel }) do |page|           page.call('init_sortable')         end       else         controller.replace("panel_#{panel.id}", "panels/edit", :locals => { :panel => panel }) do |page|           page.call('init_sortable')         end       end     end   end end

Basically, what happens is that the announcer looks to see if the panel was saved, and either re-renders the panel partial, or if not it re-renders the edit partial.

The controller action is now very to test, because of the lack of branching.

The announcer is simple to test, because it is just a method call that can be passed a bunch of mock objects. No crap with sessions, permission, etc.

Here’s the test for the announcer:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 
require File.dirname(__FILE__) + '/../spec_helper'  describe PanelUpdateAnnouncer, 'panel updated event' do   before do     @panel_update_announcer = PanelUpdateAnnouncer.new     @panel = mock_model(Panel)     @controller = mock('controller')     @controller.should_receive(:responds_to_parent).and_yield   end    it 'renders the panel partial if the panel is valid' do     @panel.should_receive(:valid?).and_return(true)     @controller.should_receive(:replace).with("panel_#{@panel.id}", "panels/panel", :locals => { :panel => @panel })   end    it 'renders the edit panel partial if the panel is invalid' do     @panel.should_receive(:valid?).and_return(false)     @controller.should_receive(:replace).with("panel_#{@panel.id}", "panels/edit", :locals => { :panel => @panel })   end    after do     @panel_update_announcer.panel_updated(:panel => @panel, :controller => @controller)   end end

And now the test for the action:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
describe PanelsController, 'update panel' do   before do     mock_user_permission     @panel = mock_editable_panel     @panel_update_announcer = mock('announcer')     @controller.panel_update_announcer = @panel_update_announcer     @params = mock('params')   end    it 'calls the announcer when update succeeds' do     @panel.should_receive(:update_from_params).with(@params)     @panel.should_receive(:reload)     @panel_update_announcer.should_receive(:panel_updated).with(:panel => @panel, :controller => @controller)   end    after do     post :update, { :id => @panel.id, :panel => @params }, { :user_id => @user_id }   end end

It’s a very simple pattern to follow. We use the DIY plugin to inject the announcers into the controller to make it even simpler. We also have some of the RJS methods wrapped in our top level application controller so the announcers can get to them without doing send() tricks. Besides that, there isn’t much required to use this pattern, and the benefits for testing and refactoring have been great.

This entry was posted in Design & Development and tagged . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">