Resources

Download Source Code

Summary

# Terminal
bin/importmap pin tom-select
yarn add tom-select
bin/rails g stimulus select
bin/rails g stimulus select_with_dependent
rails g controller searches/products

# config/importmap.rb
pin "tom-select", to: "https://cdn.jsdelivr.net/npm/tom-select@2.4.3/+esm"

# app/views/layouts/application.html.erb
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.css" rel="stylesheet">

# app/javascript/controllers/select_controller.js
import { Controller } from "@hotwired/stimulus"
import TomSelect from "tom-select"

// Connects to data-controller="select"
export default class extends Controller {
  connect() {
    new TomSelect(this.element)
  }

  disconnect() {
    if (this.element.tomselect) {
      this.element.tomselect.destroy()
    }
  }
}

# app/views/welcome/index.html.erb
<div>
  <h1 class="font-bold text-4xl">Welcome#index</h1>
  <p>Find me in app/views/welcome/index.html.erb</p>

  <%= form_with url: root_path do |form| %>
    <%= form.label :company, "Search companies", class: "font-bold" %>

    <%= form.select :company, Company.all.collect { |c| [c.name, c.id] },
      { prompt: "Select a company" },
      "data-controller": "select-with-dependent",
      "data-action": "change->select-with-dependent#update",
      "data-select-with-dependent-url-value": searches_products_path,
      "data-select-with-dependent-target-value": "product_select",
      "data-select-with-dependent-param-value": :company_id,
      class: "mb-3" %>

    <%= form.label :product, "Search products", class: "font-bold" %>

    <%= form.select :product, Product.all.collect { |c| [c.name, c.id] },
      { prompt: "Select a product" },
      "data-controller": "select",
      id: :product_select,
      class: "mb-3" %>

    <%= form.submit "Search", class: "bg-blue-500 text-white p-2 rounded" %>
  <% end %>
</div>

# config/routes.rb
namespace :searches do
  resources :products, only: :index
end

# app/controllers/searches/products_controller.rb
module Searches
  class ProductsController < ApplicationController
    def index
      @products = Product.where(filter_by_company)
      render json: @products.to_json
    end

    private

    def filter_by_company
      return {} unless params[:company_id].present?

      { company_id: params[:company_id] }
    end
  end
end

# app/javascript/controllers/select_with_dependent_controller.js
import { Controller } from "@hotwired/stimulus"
import TomSelect from "tom-select"

// Connects to data-controller="select-with-dependent"

export default class extends Controller {
  static values = {
    url: String,
    param: "id",
    target: String,
    target_name: "name",
    target_value: "id"
  }

  connect() {
    new TomSelect(this.element)
  }

  disconnect() {
    if (this.element.tomselect) {
      this.element.tomselect.destroy()
    }
  }

  update() {
    const url = new URL(this.urlValue, window.location.origin)
    url.searchParams.set(this.paramValue, this.element.value)
    fetch(url, {
      headers: { Accept: "application/json" }
    })
      .then(response => response.json())
      .then(data => {
        const target = document.getElementById(this.targetValue)
        if (target) {
          const valueKey = this.targetValueValue
          const nameKey = this.targetNameValue
          if (target.tomselect) {
            target.tomselect.clearOptions()
            data.forEach(option => {
              target.tomselect.addOption({ value: option[valueKey], text: option[nameKey] })
            })
            target.tomselect.refreshOptions(false)
          } else {
            target.innerHTML = ""
            data.forEach(option => {
              const opt = document.createElement("option")
              opt.value = option[valueKey]
              opt.textContent = option[nameKey]
              target.appendChild(opt)
            })
          }
        }
      })
  }
}