I've got a has_many through:
relationship between two models: achievements
(the parent) and objectives
(the child), with memberships
as my join.
I'm trying to add a multiselect subform for the child items into the parent form. I can get the form to display correctly, but I'm missing the bit to actually save the association to the memberships
table. I'm finding tons of examples on how to make a subform that creates both the parent and new children at the same time, but I'm not trying to do that in my case. I only want the user to select existing child items while creating or modifying a parent item.
I'm new to Rails, but I feel like I'm missing something really fundamental here. I know I need to make changes to both the controller and the form, but how so? Do I need to use accepts_nested_attributes_for
in my achievements
model? My attempts to play with that (while not really knowing what it does in this case) make my form break completely (the multiselect completely disappears).
Here are my three models:
class Achievement < ApplicationRecord
has_many :memberships
has_many :objectives, through: :memberships
end
class Objective < ApplicationRecord
has_many :memberships
has_many :achievements, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :objective
belongs_to :achievement
end
My form partial:
<%= form_with(model: achievement) do |form| %>
<div>
<%= form.label :name, style: "display: block" %>
<%= form.text_field :name %>
<%= form.fields_for :objectives do |objectives_subform| %>
<%= objectives_subform.label :objectives, "Objectives", style: "display: block" %>
<%= objectives_subform.select :objectives, Objective.all.map{|o| [o.name]}, { }, multiple: true %>
<% end %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
And finally, my controller:
class AchievementsController < ApplicationController
before_action :set_achievement, only: %i[ show edit update destroy ]
# GET /achievements or /achievements.json
def index
@achievements = Achievement.all
end
# GET /achievements/1 or /achievements/1.json
def show
end
# GET /achievements/new
def new
@achievement = Achievement.new
@achievement.objectives.build
end
# GET /achievements/1/edit
def edit
end
# POST /achievements or /achievements.json
def create
@achievement = Achievement.new(achievement_params)
respond_to do |format|
if @achievement.save
format.html { redirect_to achievement_url(@achievement), notice: "Achievement was successfully created." }
format.json { render :show, status: :created, location: @achievement }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @achievement.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /achievements/1 or /achievements/1.json
def update
respond_to do |format|
if @achievement.update(achievement_params)
format.html { redirect_to achievement_url(@achievement), notice: "Achievement was successfully updated." }
format.json { render :show, status: :ok, location: @achievement }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @achievement.errors, status: :unprocessable_entity }
end
end
end
# DELETE /achievements/1 or /achievements/1.json
def destroy
@achievement.destroy
respond_to do |format|
format.html { redirect_to achievements_url, notice: "Achievement was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_achievement
@achievement = Achievement.find(params[:id])
end
# Only allow a list of trusted parameters through.
def achievement_params
params.require(:achievement).permit(:name, objectives_attributes: [:id])
end
end
Hey If I understand properly, you're trying to create a achievement
and you already have objectives
data that you're rendering it inside form as dropdown as a multi select.
And while you do multi select for objectives dropdown you're going to send array of objective id's
to the controller in the params and there you want to create memberships
for achievement
using the objective id's
.
If that's what you want to achieve here's how to do it in controller
If you're receiving the array of objective id's
Ex: [1,3,4,5]
SOLUTION
then you can create memberships
for an achievement
using the objective ids
like this
In your case you're receiving the array of objective ids so do like this
@achievement.memberships.create(achievement_params[:objectives_attributes].map { |obj_id| { objective_id: obj_id }})
In controller
create
action
should look like this
def create
@achievement = Achievement.new(achievement_params)
respond_to do |format|
if @achievement.save
@achievement.memberships.create(achievement_params[:objectives_attributes].map { |obj_id| { objective_id: obj_id }})
format.html { redirect_to achievement_url(@achievement), notice: "Achievement was successfully created." }
format.json { render :show, status: :created, location: @achievement }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @achievement.errors, status: :unprocessable_entity }
end
end
end
FUNDAMENTAL NOTES
To access the memberships of an achievement
@achievement.memberships
To create one membership
@achievement.memberships.create(objective_id: 1)
To create n number of memberships using array of objective id's
@achievement.memberships.create([ { objective_id: 1 }, { objective_id: 2 } ])