Created
April 16, 2020 08:08
-
-
Save leastbad/e6773a0c800e96ba7c76e342e4e40ef7 to your computer and use it in GitHub Desktop.
Revisions
-
leastbad created this gist
Apr 16, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,15 @@ # Choices.js Stimulus wrapper https://joshuajohnson.co.uk/Choices/ Soon, this will be published as an NPM package, but there's an absence of documentation right now. It supports *almost* all functions from the original library; soon it will support 100% of them. This wrapper *adds* Ajax pre-fetch search. Happens if controller has a `data-search-path` attribute. Stimulus controller targets use new v2 syntax. Controller attaches a reference to itself on the element so that you can access the internal state from external scripts. Verified to support single and multi-select drop-downs. You can pre-populate options into the datalist. Full compatibility with Turbolinks 5/6 including page caching. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,124 @@ import { Controller } from 'stimulus' import * as Choices from 'choices.js' export default class extends Controller { static targets = ['select', 'options'] initialize () { this.element['choices'] = this this.refresh = this.refresh.bind(this) this.add = this.add.bind(this) this.remove = this.remove.bind(this) this.search = this.search.bind(this) this.update = this.update.bind(this) this.filter = this.filter.bind(this) this.options = this.options.bind(this) this.optionsReducer = this.optionsReducer.bind(this) this.searchPath = this.element.dataset.searchPath this.forceOption = this.element.dataset.forceOption || true } connect () { setTimeout(this.setup.bind(this), 5) } setup () { this.choices = new Choices(this.selectTarget, this.options()) this.input = this.element.querySelector('input') this.refresh() if (this.searchPath) this.input.addEventListener('input', this.search) this.selectTarget.addEventListener('change', this.refresh) this.selectTarget.addEventListener('addItem', this.add) this.selectTarget.addEventListener('removeItem', this.remove) } disconnect () { if (this.searchPath) this.input.removeEventListener('input', this.search) this.selectTarget.removeEventListener('change', this.refresh) this.selectTarget.removeEventListener('addItem', this.add) this.selectTarget.removeEventListener('removeItem', this.remove) try { this.choices.destroy() } catch {} this.choices = undefined } refresh () { this.choices.setChoices([], 'value', 'label', true) if (this.hasOptionsTarget) { ;[...this.optionsTarget.children].forEach(this.append.bind(this)) } } append (option) { if ( ![...this.selectTarget.options].some(o => { return o.label === option.label }) ) this.choices.setChoices([option], 'value', 'label', false) } add (event) { if (this.hasOptionsTarget) { const option = [...this.optionsTarget.children].find(option => { return option.label === event.detail.label }) if (option) { option.setAttribute('selected', '') } else { const newOption = document.createElement('option') newOption.setAttribute('label', event.detail.label) newOption.setAttribute('value', event.detail.value) newOption.setAttribute('selected', '') this.optionsTarget.appendChild(newOption) } } } remove (event) { if (this.hasOptionsTarget) { const option = [...this.optionsTarget.children].find(item => { return item.label === event.detail.label }) if (option) this.searchPath ? option.remove() : option.removeAttribute('selected') } if (this.forceOption && !this.selectTarget.options.length) this.selectTarget.add(document.createElement('option')) } search (event) { if (event.target.value) { fetch(this.searchPath + event.target.value, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(this.update) } else { this.refresh() } } update (data) { this.choices.setChoices(data.filter(this.filter), 'value', 'label', true) } filter (item) { return ![...this.selectTarget.options].some(option => { return option.label === item.label }) } options () { return 'silent renderChoiceLimit maxItemCount addItems removeItems removeItemButton editItems duplicateItemsAllowed delimiter paste searchEnabled searchChoices searchFloor searchResultLimit position resetScrollPosition addItemFilter shouldSort shouldSortItems placeholder placeholderValue prependValue appendValue renderSelectedChoices loadingText noResultsText noChoicesText itemSelectText addItemText maxItemText' .split(' ') .reduce(this.optionsReducer, {}) } optionsReducer (accumulator, currentValue) { if (this.element.dataset[currentValue]) accumulator[currentValue] = this.element.dataset[currentValue] return accumulator } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,10 @@ class Event < ApplicationRecord # gem "pg_search", "~> 2.3" # https://github.com/Casecommons/pg_search include PgSearch::Model pg_search_scope :stemmed, against: :name, using: {tsearch: {prefix: true}, trigram: {}} def self.typeahead_search(term) Event .stemmed(term) .map { |event| {value: event.id, label: event.name} } end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,7 @@ class EventsController < ApplicationController skip_before_action :verify_authenticity_token, only: [:search] def search render json: Event.typeahead_search(params[:name]) end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,4 @@ <div data-controller="choices" data-search-path="/events/search?name=" data-remove-item-button="true" data-duplicate-items-allowed="false" data-search-result-limit="100" data-no-choices-text="Start typing to search..."> <datalist data-choices-target="options"></datalist> <select data-choices-target="select"></select> </div> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,7 @@ Rails.application.routes.draw do resources :events do collection do get "search", constraints: lambda { |request| request.xhr? } end end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,4 @@ <div data-controller="choices" data-search-path="/events/search?name=" data-remove-item-button="true" data-search-result-limit="100" data-no-choices-text="Start typing to search..."> <datalist data-choices-target="options"></datalist> <%= f.collection_select :event_id, @events, :id, :name, {include_blank: true}, {data: {"choices-target": "select"}} %> </div>