I’ve been advancing a little in my rails work, to the point where I’m making more elaborate forms. So I’ve had to switch to using form_tag instead of the form_for that’s the default when you use scaffolding. I have a pretty good understanding of the differences now, so I’m documenting it here.

Typically, form_for is used with an instance method that’s an item in the table. A simple example from one of my apps is a user account. Ignoring the password checks, I store a username and a fullname in the database table.

Form View (very simplified)

<%= form_for(@user) do |f| %>
  <%= f.text_field :username %>
  <%= f.text_field :fullname %>
  <%= f.submit %>
<% end %>

Now, if I submit that form, I see this in the log file.

Parameters: {"utf8"=>"?", "authenticity_token"=>"+L5dwW/yTTgVD2i6izN6iP6vGP1PU6y9u3jmgQOih8c=", "user"=>{"username"=>"john", "fullname"=>"John Smith"}, "commit"=>"Create User"}

So, in my users_controller, the create method takes the params for user, makes a new user and saves it.

def create
  @user = User.new(params[:user])
  if @user.save
    redirect_to @user, notice: 'User was successfully created.'
  else
    render action: "new"
  end
end

Now, if I instead use a form_tag things work a little differently. I have a parts table that, for simplicity, say takes a category and size. Using a form_tag, the view looks like this:

<%= form_tag create_part_path, :method => :post %>
  <%= text_field_tag :category %>
  <%= text_field_tag :size %>
  <%= submit_tag %>
<% end %>

First note that I’m not making the form for a particular instance variable. Instead I’m saying that this is a form that I want to post and run the create_part method. What is the create_part method? It’s simply the create method in my parts controller. However, since I want to call it by name, I had to name the route (the create and update routes are usually not named) in my routes file. So I added these lines to routes.rb:

match 'parts/' => 'parts#create', :via => :post, :as => :create_part
match 'parts/:id' => 'parts#update', :via => :put, :as => :update_part

So now, when I post this form, in the logs, I see this:

Parameters: {"utf8"=>"?", "authenticity_token"=>"+L5dwW/yTTgVD2i6izN6iP6vGP1PU6y9u3jmgQOih8c=", "category"=>"window", "size"=>"7x7", "commit"=>"Save changes"}

Notice that the params aren’t in a part hash, but are just there individually. So now I have to work on the create method.

If this were just a scaffold, the first line in the create method would look like this:

@part = Part.new(params[:part])

In order to fix this, all I need to do is to change this line to

@part = Part.new(:category => params[:category], :size => params[:size])

And like that, the form_tag works exactly the same as the form_for in the case of the new method. However, if we want to use the edit method, we’re not quite done. Normally, with edit, the values will already be filled in with whatever values they already have. And update uses the put command, while create uses the post. So we have to change that as well. Here’s how the new and edit views will look:

New

<%= form_tag create_part_path, :method => :post do %>
  <%= render 'form' %>
<% end %>

Edit

<%= form_tag update_part_path, :method => :put do %>
  <%= render 'form' %>
<% end %>

Form

<%= text_field_tag :category, @part.category %>
<%= text_field_tag :size, @part.size %>
<%= submit_tag %>

That will take care of filling in values where they’ve already been set.

Now, if this were all I wanted to do, I’d stick with the form_for because it’s simpler. However, in this particular case, I want the form to do a bit more. I have another table called tiles, which is a very simple table. It just has a field for a name and a field for a part_id. We basically have three different types of tiles. And a part has to be for at least one tile, but it can be more more than one tile. So a part has_many tiles and a tile belongs_to a part. In the form, I want to have checkboxes showing the three tiles so that the user can check which tile this part is used in. Since this means I’ll be sending an array of some sort about the tiles, I no longer have a one-to-one relationship between the form and the part table. So I really can’t use the form_for method and have to use form_tag to create my form. Then in the create method, I will do whatever is necessary to create the part and tile entries. The other issue is that there’s a little bit of difference between the create and update methods. My solution to this was to still use a _form partial, but to put the form_tag line in the new view (which will call create_part_path) and the form_tag that calls update_part_path in the edit view.

So, to add our checkboxes of tiles, we need to change our new, edit, create and update methods like this:

def new
    @part = Part.new
    @parts = Part.all
    @tiles = Tile::NAMES
    @part_tiles = []
  end

  def edit
    @part = Part.find(params[:id])
    @tiles = Tile::NAMES
    @part_tiles = Tile.where(:part_id => @part.id).order(:name).pluck(:name)
  end

def create
    @part = Part.new(:category => params[:category], :size => params[:size])
    @tiles = params[:tiles] ||= []

    if @part.save
      @tiles.each do |tile|
        Tile.create(:name => tile, :part_id => @part.id)
      end
      redirect_to root_url, notice: 'Part was successfully created.'
    else
      render action: "new"
    end
  end

def update
    @part = Part.find(params[:id])
    @tiles = params[:tiles] ||= []

    if @part.update_attributes(:category => params[:category], :size => params[:size])
      @tiles.each do |tile|
        t = Tile.find_or_initialize_by_name_and_part_id(tile, @part.id)
        t.update_attributes(:name => tile, :part_id => @part.id)
      end
      redirect_to root_url, notice: 'Part was successfully updated.'
    else
      render action: "edit"
    end
  end

Tile::NAMES is just an array in the model I use with the names of the three tiles. This should not change, so a hardcoded array is fine. In edit, the @part_tiles is just an array of tile names that have been set for that particular tile. If none of the checkboxes are checked, it should just be an empty array. We’ll need this to precheck the boxes in the edit form.

The final version of the form looks something like this:

<%= text_field_tag :category, @part.category %>
  <%= text_field_tag :size, @part.size %>
  <% @tiles.each do |tile| %>
    <%= check_box_tag "tiles[]", tile, @part_tiles.include?(tile) %>
    <%= tile %>
  <% end %>
  <%= submit_tag %>

That’s it. At the end of the form, we loop through our list of tiles and make a checkbox next to each one. If the name of the tile is included in the array @part_tiles, then we know it’s already been set and should default to checked.