SajidBarcha
SajidBarcha

Reputation: 25

In Many to Many relationships in Ruby on Rails, how to create records in join table with data from a parent table form?

I have been learning RoR for 2 months now so pardon me if you think I am stuck in something too basic. I am sure the rails expert will solve this in a couple of minutes, but I am unable to do it for 2 days now. I have provided a detailed explanation of the situation, although I feel the problem is not half as big as my question. Please bear with me and help me out.

Problem: I have two models/tables, listings and tags, that have a many-to-many relationship, joined in the middle by a join table called taggings that contains as FKs the PK's of the two tables that it belongs to. Tag model has one attribute called tag_name, in addition to rails provided id

class Tag < ActiveRecord::Base
  has_many :taggings, dependent: :destroy
  has_many :listings, :through => :taggings
end

Listing model has one attribute, description (in addition to the default id field)

class Listing < ActiveRecord::Base
  has_many :taggings, dependent: :destroy
  has_many :tags, :through => :taggings 
end

The Above two have a m-n relationship, joined by the following join table, which has two attributes, listing_id, and tag_id as foreign keys, in addition to rails provided id attribute.

class Tagging < ActiveRecord::Base
  belongs_to :listing
  belongs_to :tag
end

In my Listings new/create page, I have a form, where I enter a description into text field and then choose tags (that I get from Tags table) using check boxes. The following is the form.

<%= form_for @listing, :html => { :class => 'form-horizontal' } do |f| %>

  <div class="control-group">
    <%= f.label :description, :class => 'control-label' %>

    <div class="controls">
      <%= f.text_field :description, :class => 'text_field' %>
    </div>
  </div>

  <div class="control-group">
    <% Tag.all.each do |tag| %>
      <%= check_box_tag "listing[tag_ids][]", tag.id%>
      <%= tag.tag_name %> <br>
    <%end%>
  </div>

</div>

  <div class="form-actions">
    <%= f.submit nil, :class => 'btn btn-primary' %>
    <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
            listings_path, :class => 'btn' %>
  </div>
<% end %>

When I submit this form after selecting three tags from check boxes, I expect the following things to happen:

  1. A new record in listings table should be created. This happens.
  2. Three new rows in taggings table should be added. The listing_id field in taggings table should contain the same id of newly created listing, and the tag_id in three rows contain the id of the tags that were selected using check boxes. This doesn't happen and this is the problem. Only a single row is inserted into taggings table which contains the correct listing_id but the tag_id is left empty.

I think this is because the check box in form sends an array of values, and I don't know how to put the values in arrays in multiple rows in taggings table.

Here is my listings controller's create and new actions.

def new
  @listing = Listing.new
end

def create
  @listing = Listing.new(listing_params)

  respond_to do |format|
    if @listing.save
      format.html { redirect_to @listing, notice: 'Listing was successfully created.' }
      format.json { render action: 'show', status: :created, location: @listing }
    else
      format.html { render action: 'new' }
      format.json { render json: @listing.errors, status: :unprocessable_entity }
    end
  end
 end

 def listing_params
   params.require(:listing).permit(:description, :id)
 end

I have been stuck at this for last two days, spent a lot of time before seeking help here. I would greatly appreciate if someone helps me solve this. Thanks a lot.

Upvotes: 2

Views: 2804

Answers (3)

Richard Peck
Richard Peck

Reputation: 76784

Your code should work with @Wali Ali's fix

You'll gain a better perspective by posting your request log which will probably show unpermitted params for tag_ids. As you're passing an array, you'll have to define it as such, hence setting it in your strong params hash:

params.require(:listing).permit(tag_ids: [])

Upvotes: 0

Wally Ali
Wally Ali

Reputation: 2508

permit the tag ids as an array in your controller as in this:

def listing_params
   params.require(:listing).permit(:description, :id, :tag_ids => [])
end

Upvotes: 2

Nimir
Nimir

Reputation: 5839

I prefer doing it through rails nested forms, something like this:

model

class Listing < ActiveRecord::Base
  has_many :taggings, dependent: :destroy
  has_many :tags, :through => :taggings

  accepts_nested_attributes_for :taggings, allow_destroy: true, reject_if: proc { |attributes| attributes['tag_id'].blank?}
# the reject option is necessary because we put a hidden field for list.id and it will submit a tagging object (tag_id = nil) for every tag if we don't reject ones without tag_id 
end

_form

<%= form_for @listing, :html => { :class => 'form-horizontal' } do |f| %>

  <div class="control-group">
    <%= f.label :description, :class => 'control-label' %>

    <div class="controls">
      <%= f.text_field :description, :class => 'text_field' %>
    </div>
  </div>

  <%= f.fields_for :taggings, Tag.all do |tags_builder| %>
    <%= tags_builder.hidden_field :list_id, @list.id %>
    <div class="controls">
      <%= tags_builder.check_box :tag_id %>
      <%= tags_builder.object.tag_name %> <br>
    </div>
  <%end%>

</div>

  <div class="form-actions">
    <%= f.submit nil, :class => 'btn btn-primary' %>
    <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
            listings_path, :class => 'btn' %>
  </div>
<% end %>

controller

You should tell your controller to allow params of the nested form

def list_params
  params.require(:list).permit(:list_attr_1, :list_attr_2 :taggings_attributes =>[:tag_id, :list_id, :_destroy])
end


Note:

I haven't tested this code so it will propaply require some fixing but i wanted to give you an idea on the steps (Here are some useful links: fields_for api docs , RailsCasts on nested forms )

Upvotes: 1

Related Questions