back

Checkboxes with has_many :through

Cover Image for Checkboxes with has_many :through

Let's assume that you want to store the information about a user, who is enrolled in several courses, and you also want to list all users enrolled in a particular course, then you can use many-to-many relationship in Rails. Here is an article by Aihui about why you should prefer has_many :through association over _has_many_and_belongs_to_many _(HMBM) association.

This is how the has_many :through implementation may look like:

First, we have the classes User and Course (which I generated with two scaffolds):

  # rails generate scaffold User name:string
  class User < ActiveRecord::Base; end

  # rails generate scaffold Course name:string
  class Course < ActiveRecord::Base; end

Now we generate a new class Enrollment to store the user-to-course relationship:

  # rails generate migration CreateEnrollments course:belongs_to user:belongs_to
  class CreateEnrollments < ActiveRecord::Migration
    def change
      create_table :enrollments do |t|
        t.belongs_to :course, index: true
        t.belongs_to :user, index: true
      end
      add_foreign_key :enrollments, :courses
      add_foreign_key :enrollments, :users
    end
  end

  # /app/models/enrollment.rb
  class Enrollment < ActiveRecord::Base
    belongs_to :course
    belongs_to :user
  end

And we update our User and Course classes with:

  class User < ActiveRecord::Base
    has_many :enrollments, dependent: :destroy
    has_many :courses, through: :enrollments
  end

  class Course < ActiveRecord::Base
   has_many :enrollments, dependent: :destroy
   has_many :users, through: :enrollments
  end

dependent: :destroy indicates that the existence of the enrollments is dependent on the existence of the user/course. Now let's try to edit a user and enroll him/her in some courses with the help of checkboxes. We want the result to look like this: View for editing user with checkboxes to add courses.To achieve that, we first change the _edit _template from user's controller by adding this to _form.html.erb :

  <!-- /app/views/users/_form.html.erb -->

  <%= form_for(@user) do |f| %>
    // ...
    <%= f.label :name %>
    <%= f.text_field :name %>


    <%= hidden_field_tag "user[course_ids][]", nil %>
    <% Course.all.each do |course| %>
      <%= check_box_tag 'user[course_ids][]', course.id, @user.courses.include?(course), id: dom_id(course) %>
      <%= label_tag dom_id(course), course.name %>
    <% end %>
    <%= f.submit %>
  <% end %>

The hidden_field_tag allows us to update the user with all courses unchecked. With dom_id(course) we can (un)check a course by simply clicking on the checkbox's label. To update the user, we have to whitelist course_ids in our user's controller:

  class UsersController < ApplicationController
    # ...
    def update
      if @user.update(user_params)
        redirect_to @user, notice: 'User was successfully updated.'
      else
        render :edit
      end
    end


    private
      def user_params
        params.require(:user).permit(:name, {:course_ids=>[]})
      end
  end

Finally, we update the view to list user's courses:

View with a listing of user's courses.

  <!-- /app/views/users/show.html.erb -->

  <ul>
    <% @user.courses.each do |course| %>
      <li><%= course.name %></li>
    <% end %>
  </ul>

Here is the source code. The implementation for listing all users from a particular course is equivalent.

Sources: this article, this video and this book.