Search code examples
ruby-on-railsmulti-selecthas-many-throughruby-on-rails-7

How can I save values from multiselect to the database in Rails 7


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

Solution

  • 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 } ])