Editing Many-to-Many Relationships with Multiselect Lists

Problem

You have two models that have a many-to-many relationship to each other. You want to create a select list in the edit view of one model that allows you to associate with one or more records of the other model.

Solution

As part of your application's authentication system, you have users that can be assigned to one or more roles that define access privileges. The many-to-many relationship between users and roles is set up by the following class definitions:

app/models/user.rb:

class User < ActiveRecord::Base
 has_and_belongs_to_many :roles end

app/models/role.rb:

class Role < ActiveRecord::Base
 has_and_belongs_to_many :users end

In the edit action of the Users controller, add an instance variable named @selected_roles and populate it with all of the Role objects. Define a private method named handle_roles_users to handle updating a User object with associated roles from the params hash.

app/controllers/users_controller.rb:

class UsersController < ApplicationController
 def edit
 @user = User.find(params[:id])
 @roles = {}
 Role.find(:all).collect {|r| @roles[r.name] = r.id }
 end
 def update
 @user = User.find(params[:id])
 handle_roles_users
 if @user.update_attributes(params[:user])
 flash[:notice] = 'User was successfully updated.'
 redirect_to :action => 'show', :id => @user
 else 
 render :action => 'edit'
 end
 end
 private 
 def handle_roles_users
 if params['role_ids']
 @user.roles.clear
 roles = params['role_ids'].map { |id| Role.find(id) }
 @user.roles << roles 
 end 
 end
end

In the Users edit view, create a multiple option select list using options_for_select to generate the options from the objects in the @roles instance variable. Construct a list of existing role associations and pass it in as the second parameter.

app/views/users/edit.rhtml:

<h1>Editing user</h1>
<% form_tag :action => 'update', :id => @user do %>
 <%= render :partial => 'form' %>
<p>
<select id="role_ids" multiple="multiple">
 <%= options_for_select(@roles, @user.roles.collect {|d| d.id }) %>
</select>
</p>
 <%= submit_tag 'Edit' %>
<% end %>
<%= link_to 'Show', :action => 'show', :id => @user %> |
<%= link_to 'Back', :action => 'list' %>

To display the roles associated with each user, join them as a comma-separated list in view of the show action:

app/views/users/show.rhtml:

<% for column in User.content_columns %>
<p>
 <b><%= column.human_name %>:</b> <%=h @user.send(column.name) %>
</p>
<% end %>
<p>
 <b>Role(s):</b> <%=h @user.roles.collect {|r| r.name}.join(', ') %>
</p>
<%= link_to 'Edit', :action => 'edit', :id => @user %> |
<%= link_to 'Back', :action => 'list' %>

Discussion

There are a number of helpers available for turning collections of objects into select lists in Rails. For example, the select or select_tag methods of ActionView::Helpers::FormOptionsHelper will generate the entire HTML select tag based on a number of options. Most of these helper methods generate only the options list.

shows two roles selected for a user and how those roles are listed in the view of the show action.

Figure 5-4. A form allowing selection of multiple items from a select list

See Also