app/javascript/js/controllers/fields/tags_field_controller.js
import { Controller } from '@hotwired/stimulus'
import { first, isObject, merge } from 'lodash'
import Tagify from '@yaireo/tagify'
import URI from 'urijs'
import { suggestionItemTemplate, tagTemplate } from './tags_field_helpers'
import debouncePromise from '../../helpers/debounce_promise'
export default class extends Controller {
static targets = ['input', 'fakeInput']
static values = {
whitelistItems: { type: Array, default: [] },
disallowedItems: { type: Array, default: [] },
enforceSuggestions: { type: Boolean, default: false },
closeOnSelect: { type: Boolean, default: false },
delimiters: { type: Array, default: [] },
suggestionsMaxItems: { type: Number, default: 20 },
mode: String,
fetchValuesFrom: String,
}
tagify = null
searchDebounce = 500
debouncedFetch = debouncePromise(fetch, this.searchDebounce)
get suggestionsAreObjects() {
return isObject(first(this.whitelistItemsValue)) || this.fetchValuesFromValue
}
get tagifyOptions() {
let options = {
whitelist: this.whitelistItemsValue,
blacklist: this.disallowedItemsValue,
enforceWhitelist: this.enforceSuggestionsValue,
delimiters: this.delimitersValue.join('|'),
dropdown: {
maxItems: this.suggestionsMaxItemsValue,
enabled: 0,
searchKeys: [this.labelAttributeValue],
closeOnSelect: this.closeOnSelectValue,
},
}
if (this.modeValue) {
options.mode = this.modeValue // null or "select"
}
if (this.suggestionsAreObjects) {
options = merge(options, {
tagTextProp: 'label',
dropdown: {
searchKeys: ['label'],
mapValueTo: 'value',
},
templates: {
tag: tagTemplate,
dropdownItem: suggestionItemTemplate,
},
originalInputValueFormat: (valuesArr) => valuesArr.map((item) => item.value),
})
} else {
options = merge(options, {
originalInputValueFormat: (valuesArr) => valuesArr.map((item) => item.value).join(','),
})
}
return options
}
connect() {
if (this.hasInputTarget) {
this.hideFakeInput()
this.showRealInput()
this.initTagify()
}
}
initTagify() {
this.tagify = new Tagify(this.inputTarget, this.tagifyOptions)
const that = this
function onInput(e) {
// Create the URL from which to fetch the values
const query = e.detail.value
const uri = new URI(that.fetchValuesFromValue)
uri.addSearch({
q: query,
})
// reset current whitelist
that.tagify.whitelist = null
// show the loader animation
that.tagify.loading(true)
// get new whitelist from a request
that.fetchResults(uri.toString())
.then((result) => {
that.tagify.settings.whitelist = result // add already-existing tags to the new whitelist array
that.tagify
.loading(false)
.dropdown.show(e.detail.value) // render the suggestions dropdown.
})
.catch(() => that.tagify.dropdown.hide())
}
if (this.fetchValuesFromValue) {
this.tagify.on('input', onInput)
}
}
fetchResults(endpoint) {
return this.debouncedFetch(endpoint)
.then((response) => response.json())
}
hideFakeInput() {
this.fakeInputTarget.classList.add('hidden')
}
showRealInput() {
this.inputTarget.classList.remove('hidden')
}
}