Skip to content

Instantly share code, notes, and snippets.

@secretpray
Last active October 31, 2022 06:37
Show Gist options
  • Save secretpray/1a325b3d2616bcdde336bedda5f4bd16 to your computer and use it in GitHub Desktop.
Save secretpray/1a325b3d2616bcdde336bedda5f4bd16 to your computer and use it in GitHub Desktop.
Create and select Tag with JS library TomSelect (tom-select) without jQuery
  1. Add tom-select to Rails app
yarn add tom-select
  1. Add to 'app/javascript/stylesheets/application.scss'
@import "tom-select/dist/css/tom-select.bootstrap5";

PS Need bootstrap 5 library

  1. Add to 'app/javascript/packs/application.js'
import 'tom-select'
import TomSelect from "tom-select"

document.addEventListener("turbolinks:load", () => {
  const selectInput = document.getElementById('select-tags')
  if (selectInput) {
    new TomSelect(selectInput, {
      plugins: {
        remove_button:{
          title:'Remove this item',
        }
      },
      persist: false,
      // create: true,
      create: function(input, callback) {
        const data = { name: input }
        const token = document.querySelector('meta[name="csrf-token"]').content
        fetch('/tags', {
          method: 'POST',
          headers:  {
            "X-CSRF-Token": token,
            "Content-Type": "application/json",
            "Accept": "application/json"
          },
          body: JSON.stringify(data)
        })
        .then((response) => {
          return response.json();
        })
        .then((data) => {
          callback({value: data.id, text: data.name })
        });
      },
      onDelete: function(values) {
        return confirm(values.length > 1 ? 'Are you sure you want to remove these ' + values.length + ' items?' : 'Are you sure you want to remove "' + values[0] + '"?');
      }
    })
  }
})
  1. Create Tags controller (app/controllers/tags_controller.rb)
class TagsController < ApplicationController
  def create
    tag = Tag.new(tag_params)
    if tag.valid?
      tag.save
      render json: tag
    end
  end

  private

  def tag_params
    params.require(:tag).permit(:name)
  end
end
  1. Add new route to: 'config/routes.rb'
  resources :tags, only: :create
  1. Add to form input with id 'select-tags' (app/views/posts/_form.html.erb)
  <div class="field">
    <%= form.label :tags %>
    <%= form.select :tag_ids, Tag.all.pluck(:name, :id), {}, { multiple: true, id: "select-tags" } %>
  </div>

PS Check this in app/views/layouts/application.html.erb

<head>
    ...
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    ....
  </head>
@yshmarov
Copy link

yshmarov commented Sep 14, 2021

missing

config/routes.rb

  resources :tags, only: :create

without that we get the error

ActionController::RoutingError (No route matches [POST] "/tags"):

@secretpray
Copy link
Author

secretpray commented Sep 14, 2021

async/await variant

  1. Create tom_select.js and move to: app/javascript/utilities/tom_select.js
import TomSelect from "tom-select"

document.addEventListener("turbolinks:load", () => {
  const selectInput = document.getElementById('select-tags')
  if (selectInput) {
    new TomSelect(selectInput, {
      plugins: {
        remove_button:{
          title:'Remove this item',
        }
      },
      persist: false,      
      create: async function(input, callback) {
        const data = { name: input }
        const token = document.querySelector('meta[name="csrf-token"]').content
        let response = await fetch('/tags', {
          method: 'POST',
          headers:  {
            "X-CSRF-Token": token,
            "Content-Type": "application/json",
            "Accept": "application/json"
          },
          body: JSON.stringify(data)
        })
        let newTag = await response.json();

        return await callback({ value: newTag.id, text: newTag.name })
      },
      onDelete: function(values) {
        return confirm(values.length > 1 ? 'Are you sure you want to remove these ' + values.length + ' items?' : 'Are you sure you want to remove "' + values[0] + '"?');
      }
    })
  }
})
  1. Edit 'app/javascript/packs/application.js' (delete old tom-select js code)
.....
import 'bootstrap'
// check this line!
import 'tom-select'

// add this line!
require("utilities/tom_select")
.....

@secretpray
Copy link
Author

secretpray commented Sep 14, 2021

missing

config/routes.rb

>   resources :tags, only: :create

without that we get the error

> ActionController::RoutingError (No route matches [POST] "/tags"):

Sorry. Forgot to insert) Now I will correct the recipe. Many thanks

@yshmarov
Copy link

adding this makes it more sexy

		onItemAdd:function(){
			this.setTextboxValue('');
			this.refreshOptions();
		},

@yshmarov
Copy link

we dont need 4 lines, 3 is enough :)

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

@secretpray
Copy link
Author

adding this makes it more sexy

		onItemAdd:function(){
			this.setTextboxValue('');
			this.refreshOptions();
		},

Странно, но tom-select и без этой опции очищает поле ввода тэга, а перезагрузка конфигурации зачем?

@secretpray
Copy link
Author

we dont need 4 lines, 3 is enough :)

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

Точно. Спасибо.

@secretpray
Copy link
Author

select_controller.js

import { Controller } from "stimulus"
import TomSelect from "tom-select"

export default class extends Controller {
  static values = { url: String }

  connect() {
    this.initTomSelect()
  }

  disconnect() {
    if (this.select) {
      this.select.destroy()
    }
  }

  initTomSelect() {
    if (this.element) {
      let url = `${this.urlValue}.json`

      this.select = new TomSelect(this.element, {
        plugins: ['remove_button'],
        valueField: 'id',
        labelField: 'name',
        searchField: ['name', 'email'],
        maxItems: 1,
        selectOnTab: true,
        placeholder: "Select user",
        closeAfterSelect: true,
        hidePlaceholder: false,
        preload: true,
        create: false,
        openOnFocus: true,
        highlight: true,
        sortField: {
          field: "name",
          direction: "asc"
        },
        searchField: 'name',
        load: (search, callback) => {
          fetch(url)
            .then(response => response.json())
            .then(json => {
              callback(json);
            }).catch(() => {
              callback();
            });
        },
        render: {
          option: function (data, escape) {
            return '<div>' +
              '<span class="block">' + escape(data.name) + '</span>' +
              '<span class="text-gray-400">' + escape(data.email) + '</span>' +
              '</div>';
          }
        }
      })

    }
  }
}

users_controller.rb

class UsersController < ApplicationController
  def index
    ....
    
    respond_to do |format|
      ...
      format.json { render json: @users.map { |r| {name: r.name, id: r.id, email: r.email} } }
    end
  end
end

view.html.erb

<div class="rounded-md shadow-sm">
  <%= f.select :account_user_id, {}, {placeholder: "Select user"}, {class: "w-full", data: { controller: "select", select_url_value: users_path } }%>
</div

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment