shobeurself
shobeurself

Reputation: 128

Ruby on Rails multiselect adding blank attribute for nested query causing failure, using tom-select

In my Rails application, I have a dream model and a location model. A dream can have many locations and a location can have many dreams. I've created a dream_location model to facilitate this. I would like, when creating a new dream to be able to add many locations upon saving. For this, I am trying to use 'tom-select', but I am running into a problem where the functionality is including a blank/empty location_id and this is causing the save to fail (and rightly so...). So I need help to figure out how not send this blank/empty location_id upon submitting the form. The js looks like this

// Initialize Tom Select on the relevant fields
document.addEventListener('DOMContentLoaded', () => {
  const elements = document.querySelectorAll('.tom-select');
  elements.forEach((element) => {
    new TomSelect(element, {
      create: false,
      onInitialize: function() {
        // Remove the blank option if it exists
        const blankOption = this.getOption('');
        if (blankOption) {
          this.removeOption('');
        }
      }
    });
  });
});

Here is the relevant form code in the erb file:

  <h3>Locations</h3>
  <%= form.fields_for :dream_locations do |dl_form| %>
    <div class="form-group">
      <%= dl_form.collection_select :location_id, Location.where(user_id: current_user.id), :id, :name, 
        {},
        { :multiple => true, class: "form-control tom-select" } %>
      <%= dl_form.check_box :_destroy %>
      <%= dl_form.label :_destroy, "Remove this location" %>
    </div>
  <% end %>

And here is what is being sent, not the blank/empty location_id in

{"authenticity_token" => "[FILTERED]", "dream" => {"title" => "Dream 2346", "date" => "2025-01-06", "location" => "Some Other Location", "dream_locations_attributes" => {"0" => {"location_id" => ["", "3"], "_destroy" => "0"}}, "dream" => "fewfwe", "analysis" => "rewr"}

Upvotes: 0

Views: 63

Answers (1)

max
max

Reputation: 102250

If by "add many" you mean select existing records and add an assocation there is much easier Rails way.

Both the has_many/has_and_belongs_to_many macros add special _ids/_ids= accessors. You just pass an array and presto it adds/deletes the join table records for you.

You just add a select with the form helpers and point it to the getter:

<%= form_with model: @dream do |form| %>
  <div class="form-group">
     <%= dl_form.collection_select 
        :location_ids, 
        @locations, # Database queries in the view are a code smell
        :id, 
        :name, 
        multiple: true, 
        class: "form-control" 
    %>
  </div>
<% end %>

On the controller side you just whitelist the attribute as an array:

class DreamsController < ApplicationController
  def new
    @dream = Dream.new
    # add an assocation instead of having all the guts hanging out.
    @locations = current_user.locations 
  end

  def create
    @dream = Dream.new(dream_attributes)
    if @dream.save
      redirect_to @dream, status: :created
    else
      render :new
    end
  end

  private

  def dream_attributes
    params.require(:dream)
          .permit(
            :foo, :bar, :baz,
            location_ids: []
          )
  end
end

This permits an array of ids (or any other permitted scalar).

Using fields_for and a hidden input for deleting is only actually necissary if you are creating the nested records at the same time and need to pass attributes for that record or if the join table entity has attributes that need to filled in. In that case follow the guides article for nested attributes.

For this, I am trying to use 'tom-select'

YAGNI.

This approach is not going to work with Turbo Drive (or Turbolinks in the case of legacy applications) which both create persistent browser sessions.

The non-idempotent transformation you apply when the document is loaded is not going to applied to elements that are added to the document dynamically. So you get functionality that only works on the initial page load.

Just start with the basic Rails way setup and add enhancement with JS if you actually need it. Most users are better off with the form controls the way the browser intended and you're better off with something written with idempotency in mind instead of carry over from the age of the jQuery Ninja.

Upvotes: 1

Related Questions