Here is how I would solve it if I had to do it without JS.
Create an M-2-M association with a join model between deparments and employees. Let departments accept nested attributes for the join model:
class Department < ApplicationRecord
has_many :positions
has_many :employees, through: :positions
accepts_nested_attributes_for :positions, reject_if: :reject_position?
private
def reject_position?(attributes)
!ActiveModel::Type::Boolean.new.cast(attributes['_keep'])
end
end
class Employee < ApplicationRecord
has_many :departments
has_many :positions, through: :departments
end
# rails g model position employee:belongs_to department:belongs_to
class Position < ApplicationRecord
belongs_to :employee
belongs_to :department
attribute :_keep, :boolean
end
Setup the routes:
# config/routes.rb
Rails.application.routes.draw do
# ...
# @todo merge this with your existing routes
resources :departments, only: [] do
resources :employees, only: [], module: :departments do
collection do
get :search
patch '/',
action: :update_collection,
as: :update
end
end
end
end
Now lets create the search form:
# /app/views/departments/employees/search.rb
<%= form_with(
url: search_department_employees_path(@department),
local: true,
method: :get
) do |form| %>
<div class="field">
<%= form.label :search_by_name %>
<%= form.text_field :search_by_name %>
</div>
<% @results&.each do |employee| %>
<%= hidden_field_tag('stored_employee_ids[]', employee.id) %>
<% end %>
<%= form.submit("Search") %>
<% end %>
Note that we are using GET instead of POST. Since this action is idempotent (it does not actually alter anything) you can use GET.
Note <%= hidden_field_tag('stored_employee_ids[]', employee.id) %>
. Rack will merge any pairs where the key ends with []
into an array.
Now lets setup the controller:
module Departments
# Controller that handles employees on a per department level
class EmployeesController < ApplicationController
before_action :set_department
# Search employees by name
# This route is nested in the department since we want to exclude employees
# that belong to the department
# GET /departments/1/employees?search_by_name=john
def search
@search_term = params[:search_by_name]
@stored_ids = params[:stored_employee_ids]
if @search_term.present?
@results = not_employed.where('employees.name LIKE ?', @search_term)
end
# merge the search results with the "stored" employee ids we are passing along
if @stored_ids.present?
@results = not_employed.or(Employee.where(id: @stored_ids))
end
@positions = (@results||[]).map do |employee|
Position.new(employee: employee)
end
end
private
def not_employed
@results ||= Employee.where.not(id: @department.employees)
end
def set_department
@department = Department.find(params[:department_id])
end
end
end
This just creates a form that "loops back on itself" and just keeps adding more ids to the query string - without all that hackery.
Now lets create a second form where we actually do something with the search results as a partial:
# app/views/departments/employees/_update_collection_form.html.erb
<%= form_with(
model: @department,
url: update_department_employees_path(@department),
local: true,
method: :patch
) do |form |%>
<legend>Add to <%= form.object.name %></legend>
<table>
<thead>
<tr>
<td>Add?</td>
<td>Name</td>
<tr>
</thead>
<tbody>
<%= form.fields_for(:positions, @positions) do |p_fields| %>
<tr>
<td>
<%= p_fields.label :_keep, class: 'aria-hidden' %>
<%= p_fields.check_box :_keep %>
</td>
<td>
<%= p_fields.object.employee.name %>
<%= p_fields.hidden_field :employee_id %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= form.submit 'Add employees to department' %>
<% end %>
form.fields_for(:positions, @positions)
loops through the array and creates inputs for each position.
And render the partial in app/views/departments/employees/search.html.erb
:
# ...
<%= render partial: 'update_collection_form' if @positions.any? %>
You should not nest this form inside another form. That will result in invalid HTML and will not work properly.
Unlike your solution I'm not cramming everything and the bathtub into a single endoint. This form sends a PATCH request to /departments/1/employees
. Using PATCH on an entire collection like this is somewhat rare as we usually just use it for individual members. But here we really are adding a bunch of stuff to the collection itself.
Now lets add the action to the controller:
module Departments
# Controller that handles employees on a per department level
class EmployeesController < ApplicationController
# ...
# Adds a bunch of employees to a department
# PATCH /departments/:department_id/employees
def update_collection
if @department.update(nested_attributes)
redirect_to action: :search,
flash: 'Employees added'
else
@postions = @department.positions.select(&:new_record?)
render :search,
flash: 'Some employees could not be added'
end
end
private
# ...
def update_collection_attributes
params.require(:department)
.permit(
positions_attributes: [
:keep,
:employee_id
]
)
end
end
end
There is almost nothing to it since accepts_nested_attributes
is doing all the work on the controller layer.
I'll leave it up to you to convert this ERB to Slim or Haml and adapt it to your existing code base.