How to merge 2 third party components in VueJS
The following solution hacks together the two components to create an editable combobox with tag-pills.
According to the caveat docs of vue-simple-suggest
, its custom input components must emit the input
, focus
and blur
events, as well as have a value
prop. In addition, there are a few undocumented events that are required from the component: click
, keydown
, and keyup
.
b-form-tags
has a value
prop, but is missing several of the required events. However, you could access its internal input
element to attach your own event handlers that forward-$emit
the events:
export default { async mounted() { // wait a couple ticks to ensure the inner contents // of b-form-tags are fully rendered await this.$nextTick() await this.$nextTick() // <b-form-tags ref="tags"> const input = this.$refs.tags.getInput() const events = [ 'focus', 'blur', 'input', 'click', 'keydown', 'keyup' ] events.forEach(event => input.addEventListener(event, e => this.$refs.tags.$emit(event, e)) ) },}
The changes above alone will cause the vue-simple-suggest
to properly appear/disappear when typing. However, it doesn't add/remove tags when interacting with the auto-suggestions. That behavior could be implemented by the following features:
- ENTER or TAB keypress causes hovered auto-suggestion to be added as a tag. If nothing hovered, the keypress adds the first auto-suggestion as a tag.
- Clicking auto-suggestion adds the auto-suggestion as a tag.
- BACKSPACE on an auto-suggestion tag deletes it.
Feature 1 implementation:
- Add
ref
s to thevue-simple-suggest
andb-form-tags
so that we could access the components in JavaScript later:
<vue-simple-suggest ref="suggest"> <b-form-tags ref="tags" /></vue-simple-suggest>
- Add a
keydown
-handler on the inner input ofb-form-tags
:
export default { mounted() { //... // <b-form-tags ref="tags"> const input = this.$refs.tags.getInput() input.addEventListener('keydown', e => this.onKeyDown(e)) },}
- Implement the handler as follows:
export default { methods: { async onKeyDown(e) { if (e.key === 'Enter' || e.key === 'Tab') { // prevent default so that the auto-suggestion isn't also // added as plaintext in b-form-tags e.preventDefault() // <vue-simple-suggest ref="suggest"> if (this.$refs.suggest.hovered) { this.$refs.tags.addTag(this.$refs.suggest.hovered) } else { const suggestions = await this.$refs.suggest.getSuggestions(e.target.value) if (suggestions.length > 0) { this.$refs.tags.addTag(suggestions[0]) } else { // no match, so clear chosen this.chosen = '' } } } } }}
- To prevent conflict with our handler, disable
b-form-tag
's automatic tag-adding upon ENTER by addingno-add-on-enter
prop:
<b-form-tags no-add-on-enter />
Feature 2 implementation:
- Bind a
suggestion-click
-event handler:
<vue-simple-suggest @suggestion-click="onSuggestionClick">
- Implement the handler as follows:
export default { methods: { onSuggestionClick(suggestion) { this.$refs.tags.addTag(suggestion); }, }}
Feature 3 implementation:
- Add the
remove-on-delete
prop tob-form-tags
:
<b-form-tags remove-on-delete />
As an aside, you might be better off with Vuetify's v-combobox
, which supports the combination of the two components you're trying to merge, but I'll leave that to you to explore :)
Merging two component need to modified internal component of bootstrapvue and it take times.
Refer to documentation here https://bootstrap-vue.js.org/docs/components/form-tags.BootstrapVue already supporting searching tags. The available list saved in options
:
data() { return { options: ['Apple', 'Orange', 'Banana', 'Lime', 'Peach', 'Chocolate', 'Strawberry'], search: '', value: [] }},
You can achieve the suggestion by make modification to update this options list.
Add @change
event in b-form-input
to trigger updateOptionsList()
:
<!-- Add onChange event to update The List --><b-form-input v-model="search" id="tag-search-input" type="search" size="sm" @change="updateOptionsList()" autocomplete="off" ></b-form-input>
Also add updateOptionsList()
methods :
// Get Data From Server From URLupdateOptionsList() { console.log("update list"); this.options = ["Jakarta", "London", "Birmingham", "Rome"]; // Use axios.get('...') then attach the result to this.options /** axios.get("your-url-here").then(response => { // update options this.options = response.data; }); **/}
Note: You can use axios ( https://vuejs.org/v2/cookbook/using-axios-to-consume-apis.html) to get real data from your server ada update the options list.
Complete sample code :
<template> <div> <b-form-group label="Tagged input using dropdown"> <b-form-tags v-model="value" no-outer-focus class="mb-2"> <template v-slot="{ tags, disabled, addTag, removeTag }"> <ul v-if="tags.length > 0" class="list-inline d-inline-block mb-2"> <li v-for="tag in tags" :key="tag" class="list-inline-item"> <b-form-tag @remove="removeTag(tag)" :title="tag" :disabled="disabled" variant="info" >{{ tag }}</b-form-tag> </li> </ul> <b-dropdown size="sm" variant="outline-secondary" block menu-class="w-100"> <template v-slot:button-content> <b-icon icon="tag-fill"></b-icon>Choose tags </template> <b-dropdown-form @submit.stop.prevent="() => {}"> <b-form-group label-for="tag-search-input" label="Search tags" label-cols-md="auto" class="mb-0" label-size="sm" :description="searchDesc" :disabled="disabled" > <!-- Add onChange event to update The List --> <b-form-input v-model="search" id="tag-search-input" type="search" size="sm" @change="updateOptionsList()" autocomplete="off" ></b-form-input> </b-form-group> </b-dropdown-form> <b-dropdown-divider></b-dropdown-divider> <b-dropdown-item-button v-for="option in availableOptions" :key="option" @click="onOptionClick({ option, addTag })" >{{ option }}</b-dropdown-item-button> <b-dropdown-text v-if="availableOptions.length === 0" >There are no tags available to select</b-dropdown-text> </b-dropdown> </template> </b-form-tags> </b-form-group> </div></template><script>import axios from "axios";export default { data() { return { options: [ "Apple", "Orange", "Banana", "Lime", "Peach", "Chocolate", "Strawberry" ], search: "", value: [] }; }, computed: { criteria() { // Compute the search criteria return this.search.trim().toLowerCase(); }, availableOptions() { const criteria = this.criteria; // Filter out already selected options const options = this.options.filter( opt => this.value.indexOf(opt) === -1 ); if (criteria) { // Show only options that match criteria return options.filter(opt => opt.toLowerCase().indexOf(criteria) > -1); } // Show all options available return options; }, searchDesc() { if (this.criteria && this.availableOptions.length === 0) { return "There are no tags matching your search criteria"; } return ""; } }, methods: { onOptionClick({ option, addTag }) { addTag(option); this.search = ""; }, // Get Data From Server From URL updateOptionsList() { console.log("update list"); this.options = ["Jakarta", "London", "Birmingham", "Rome"]; // Use axios.get('...') then attach the result to this.options /** axios.get("your-url-here").then(response => { // update options this.options = response.data; }); **/ } }};</script>
I tried it here https://codesandbox.io/s/vue-bootstrap-tags-search-ldsqx and it looks goods.
Apparently, b-form-tags doesn't emit the input event until a tag has been entered. This is not the ideal case for working with vue-simple-suggest, as it would require its input to be changed every time user hits a key. As such, what you can rather do is slot vue-simple-suggest inside of b-form-tags, instead of doing it other way around. You could use something like this:
<template> <div> <b-form-tags size="lg" tag-variant="success" tag-pills remove-on-delete separator="," class="my-3" v-model="chosenTags" > <template v-slot="{tags, tagVariant, addTag, removeTag}"> <b-form-tag v-for="tag in tags" :key="tag" :variant="tagVariant" @remove="removeTag(tag)" class="mr-1 mb-1" >{{ tag }}</b-form-tag> <b-form @submit.prevent="addSelectedWord(chosen)"> <vue-simple-suggest placeholder="Enter Keyword" @input="textInput" :value="chosen" mode="select" @select="addSelectedWord" :list="simpleSuggestionsList" :filter-by-query="true" :destyled="false" ></vue-simple-suggest> <b-btn v-if="!!chosen" type="submit" class="my-1" outline>Add</b-btn> </b-form> </template> </b-form-tags> </div></template><script>import VueSimpleSuggest from "vue-simple-suggest";import "vue-simple-suggest/dist/styles.css";export default { name: "SeedWordsSuggestions", data() { return { chosen: "", chosenTags: [], seedWords: [] }; }, components: { VueSimpleSuggest }, methods: { simpleSuggestionsList() { return ["Angular", "ReactJs", "VueJs"]; }, textInput(text) { this.chosen = text; }, addSelectedWord(word) { console.log(word); this.chosenTags.push(word); this.chosen = ""; } }};</script><style scoped></style>
You can further customize the slot or components by adding custom styling. If you do not like two input fields being displayed, you could try removing border of one by setting border property to none in the CSS.