Thursday, July 17, 2014

Why Controller#pre_render?

Recently I was pleasantly surprised that my pre_render methodology still works with Rails 4.1.4, which led me to re-visit and re-answer for myself the question of why it is helpful.

Consider the following Account model. To properly create an account, we need to associate it with a user record:

class Account < ActiveRecord::Base
  belongs_to :user
  validates :user, :presence => true
end

To do so, the following AccountsController#new is necessary:

class AccountsController < AuthenticatedController

  before_action :set_account, only: [:show, :edit, :update, :destroy]

  # GET /accounts/new
  def new
    @account = Account.new
    # Will be used by view to populate a
    # user drop down
    @users = User.all.order(:last_name)
  end

  # GET /accounts/1/edit
  def edit
    # Will be used by view to populate a
    # user drop down
    @users = User.all.order(:last_name)
  end

  # POST /accounts
  # POST /accounts.json
  def create
    @account = Account.new(account_params)
    respond_to do |format|
      if @account.save
        format.html { ... }
        format.json { ... }
      else
        format.html do
          # Needed here again to populate the user
          # drop down in error case
          @users = User.all.order(:last_name)
          render action: 'new'
        end
        format.json { ... }
      end
    end
  end

  # PATCH/PUT /accounts/1
  # PATCH/PUT /accounts/1.json
  def update
    respond_to do |format|
      if @account.update(account_params)
        format.html { ... }
        format.json { ... }
      else
        format.html do
          # Needed here again to populate the user
          # drop down in error case
          @users = User.all.order(:last_name)
          render action: 'edit'
        end
        format.json { ... }
      end
    end
  end

end

The same pattern needs to be repeated for the edit/update actions as well. Which means there are 4 different code paths in which @users needs to be initialized. This problem will grow for models with more complex relationships as well. So wouldn't it be nice if we can have a central place where @user is initialized? We can accomplish this using the #pre_render approach to be DRY-ier:

class AccountsController < AuthenticatedController

  before_action :set_account, only: [:show, :edit, :update, :destroy]

  # GET /accounts/new
  def new
    @account = Account.new
  end

  # GET /accounts/1/edit
  def edit
  end

  # POST /accounts
  # POST /accounts.json
  def create
    @account = Account.new(account_params)
    respond_to do |format|
      if @account.save
        format.html { ... }
        format.json { ... }
      else
        format.html { render action: 'new' }
        format.json { ... }
      end
    end
  end

  # PATCH/PUT /accounts/1
  # PATCH/PUT /accounts/1.json
  def update
    respond_to do |format|
      if @account.update(account_params)
        format.html { ... }
        format.json { ... }
      else
        format.html { render action: 'edit' }
        format.json { ... }
      end
    end
  end

protected

  def pre_render(action)
    case action
    when :edit, :new
      # Will be used by view to populate a
      # user drop down
      @users = User.all.order(:last_name)
    end
  end

end

Pretty nifty!

No comments: