Ruby on Rails
HowToReuseEditViewsForNewViews (Version #11)

Reusing views

In many applications, you’ll find that your edit and new views are basically the same. A common annoyance can be that if you change the model and add a new attribute, you are forced to do the change in both views, edit and new.

This violates the principle of “Don’t Repeat Yourself”, and Rails can help you solve it by letting you easily reuse the edit view as the new view of a model.

First make a new variable that holds the “mode” of the view you are in. The two modes might be called “edit” and “new.” You want your form to post to specific controller methods depending on the mode.

Add an if branch to the view.

<% if @mode == "edit" -%>
  <%= start_form_tag :action=> "edit", :id => @member.id %>
  <%= hidden_field "member", "id" %>
<% else -%>
  <%= start_form_tag :action=> "new" %>
<% end -%>

Then create methods in the controller that can handle these modes:

def edit
    @member = Member.find(@params['id'])
    @mode = "edit"
    # will use the "edit.rhtml" template by default
end	

def new
    @member = Member.new()
    @mode = "new"
    render_action "edit"
end

The tricky part is the render_action “edit” one in the new method. This call uses the “edit.rhtml” template to render the view instead of the default “new.rhtml”. and therefore makes it possible to reuse the template.

Thanks to noradio on #rubyonrails for pointing this out, and clarifications by msp here.


Here’s how I do it (with thanks to you for the original idea). This doesn’t require the @mode variable.
In my edit.rhtml I use:

<% if @params['action'] == "new" %>
  <h1>New part</h1>
  <%= start_form_tag :action=> "create" %>
<% else %>
  <h1>Edit part</h1>
  <%= start_form_tag :action=> "update", :id => @part.id %>
  <%= hidden_field "part", "id" %>
<% end %>

In the controller I have:

  def new
    @part = Part.new
    render_action 'edit'
  end

  def create
    @part = Part.new(@params['part'])
    if @part.save
      flash['notice'] = 'Part was successfully created.'
      redirect_to :action => 'list'
    else
      render_action 'edit'
    end
  end

  def edit
    @part = Part.find(@params['id'])
  end

  def update
    @part = Part.find(@params['part']['id'])
    if @part.update_attributes(@params['part'])
      flash['notice'] = 'Part was successfully updated.'
      redirect_to :action => 'show', :id => @part.id
    else
      render_action 'edit'
    end
  end

Thanks, JJ


Alternatively, you can make use of the fact that the controller is accessible during the page rendering. For example, if you create this method in your \ApplicationHelper file:

def auto_form
  action = case controller.action_name
    when "new" then "create"
    when "edit" then "update"
    else controller.action_name
  end
  start_form_tag :controller => controller.controller_name, :action => action
end

Now you can start your forms with:

<%= auto_form %>

Which seems a lot cleaner to me. It’s also possible to make use of \ActiveRecord::Base#new_record? ( http://rails.rubyonrails.com/classes/ActiveRecord/Base.html#M000411 ) to determine if this is a new object.


[whynot Sun 10th April 05]
The code in sugesstion #2 (the one from JJ) does not work correctly for me. In case I get a validation error while entering a new dataset the code will push me in to the edit view insted of staying in new since action will no longer be “new” but “create”. Fixed this by replacing

<% if @params['action'] == "new" %>

with


<% if @params['action'] == "new" or 
@params['action'] == "create" %>

Is there any reason not to merge “new” and “create” into one controller? (Likewise with “edit” and "update"). If you do that, you can use <%= form_tag %> with no arguments at all, and it will generate the url for the current controller, so you don’t have to detect which controller you’re in.

— tyler kiley

Nope that works just fine too.
— adam sanderson

What do you think about this:

Controller:


class UsersController < ApplicationController
  def admin
    if params[:id]
      @user = User.find(params[:id])
      @title = 'Edit user'
    else
      @user = User.new
      @title = 'New user'
    end

if @request.post? @user.attributes = params[:user] if @user.save redirect_to :action => ‘list’ end end end

end

admin.rhtml


<h1><%= @title %>

<% form_for :user, User.new do |u| >

Username:

<= u.text_field :username %>

Password:

<%= u.password_field :password %>

<%= submit_tag %>

<% end %>

It’s new/create/edit/update in 1. The urls don’t look nice, but the code is short and clean.

— Jules Jacobs

Reusing views

In many applications, you’ll find that your edit and new views are basically the same. A common annoyance can be that if you change the model and add a new attribute, you are forced to do the change in both views, edit and new.

This violates the principle of “Don’t Repeat Yourself”, and Rails can help you solve it by letting you easily reuse the edit view as the new view of a model.

First make a new variable that holds the “mode” of the view you are in. The two modes might be called “edit” and “new.” You want your form to post to specific controller methods depending on the mode.

Add an if branch to the view.

<% if @mode == "edit" -%>
  <%= start_form_tag :action=> "edit", :id => @member.id %>
  <%= hidden_field "member", "id" %>
<% else -%>
  <%= start_form_tag :action=> "new" %>
<% end -%>

Then create methods in the controller that can handle these modes:

def edit
    @member = Member.find(@params['id'])
    @mode = "edit"
    # will use the "edit.rhtml" template by default
end	

def new
    @member = Member.new()
    @mode = "new"
    render_action "edit"
end

The tricky part is the render_action “edit” one in the new method. This call uses the “edit.rhtml” template to render the view instead of the default “new.rhtml”. and therefore makes it possible to reuse the template.

Thanks to noradio on #rubyonrails for pointing this out, and clarifications by msp here.


Here’s how I do it (with thanks to you for the original idea). This doesn’t require the @mode variable.
In my edit.rhtml I use:

<% if @params['action'] == "new" %>
  <h1>New part</h1>
  <%= start_form_tag :action=> "create" %>
<% else %>
  <h1>Edit part</h1>
  <%= start_form_tag :action=> "update", :id => @part.id %>
  <%= hidden_field "part", "id" %>
<% end %>

In the controller I have:

  def new
    @part = Part.new
    render_action 'edit'
  end

  def create
    @part = Part.new(@params['part'])
    if @part.save
      flash['notice'] = 'Part was successfully created.'
      redirect_to :action => 'list'
    else
      render_action 'edit'
    end
  end

  def edit
    @part = Part.find(@params['id'])
  end

  def update
    @part = Part.find(@params['part']['id'])
    if @part.update_attributes(@params['part'])
      flash['notice'] = 'Part was successfully updated.'
      redirect_to :action => 'show', :id => @part.id
    else
      render_action 'edit'
    end
  end

Thanks, JJ


Alternatively, you can make use of the fact that the controller is accessible during the page rendering. For example, if you create this method in your \ApplicationHelper file:

def auto_form
  action = case controller.action_name
    when "new" then "create"
    when "edit" then "update"
    else controller.action_name
  end
  start_form_tag :controller => controller.controller_name, :action => action
end

Now you can start your forms with:

<%= auto_form %>

Which seems a lot cleaner to me. It’s also possible to make use of \ActiveRecord::Base#new_record? ( http://rails.rubyonrails.com/classes/ActiveRecord/Base.html#M000411 ) to determine if this is a new object.


[whynot Sun 10th April 05]
The code in sugesstion #2 (the one from JJ) does not work correctly for me. In case I get a validation error while entering a new dataset the code will push me in to the edit view insted of staying in new since action will no longer be “new” but “create”. Fixed this by replacing

<% if @params['action'] == "new" %>

with


<% if @params['action'] == "new" or 
@params['action'] == "create" %>

Is there any reason not to merge “new” and “create” into one controller? (Likewise with “edit” and "update"). If you do that, you can use <%= form_tag %> with no arguments at all, and it will generate the url for the current controller, so you don’t have to detect which controller you’re in.

— tyler kiley

Nope that works just fine too.
— adam sanderson

What do you think about this:

Controller:


class UsersController < ApplicationController
  def admin
    if params[:id]
      @user = User.find(params[:id])
      @title = 'Edit user'
    else
      @user = User.new
      @title = 'New user'
    end

if @request.post? @user.attributes = params[:user] if @user.save redirect_to :action => ‘list’ end end end

end

admin.rhtml


<h1><%= @title %>

<% form_for :user, User.new do |u| >

Username:

<= u.text_field :username %>

Password:

<%= u.password_field :password %>

<%= submit_tag %>

<% end %>

It’s new/create/edit/update in 1. The urls don’t look nice, but the code is short and clean.

— Jules Jacobs