5
votes

I am using Form Tags components bootstrap-vue framework. I want to use vue-simple-suggest component (from npm) with form tags to suggest words related to user's query. User can select multiple words from suggestion, and the selected word will be saved in form tags as a pill as shown in image below.

enter image description here

I don't know how to merge both the components as a single component (or more better way), so that I can use UI feature of bootstrap with auto-suggest feature of a third party module.

I am learning VueJs, I don't know what should I learn to do this?

Here is my code:

<template>
  <div>
    <vue-simple-suggest
      v-model="chosen"
      mode="select"
      :list="simpleSuggestionsList"
      :filter-by-query="true"
      :destyled="false"
    >

      <b-form-tags 
        placeholder="Enter Keyword"
        size="lg"
        tag-variant="success"
        tag-pills
        remove-on-delete
        separator=","
        class="my-3"
        @input="updateValue"
      ></b-form-tags>

    </vue-simple-suggest>

  </div>
</template>

<script>
import VueSimpleSuggest from 'vue-simple-suggest'
import 'vue-simple-suggest/dist/styles.css'

export default {
  name: "SeedWordsSuggestions",

  data() {
    return {
      chosen: '',
      seedWords: []
    }
  },

  components: {
    VueSimpleSuggest
  },

  methods: {

    simpleSuggestionsList() {
      return [
        'Angular',
        'ReactJs',
        'VueJs'
      ]
    },

    addSelectedWord(e) {
      console.log(`addSelectedWord`, e)
    },

    updateValue(value) {
      const pos = value.length
      this.seedWords.push(value[pos - 1])
      console.log(this.seedWords)
    }
  }
}
</script>

<style scoped>

</style>
4
could you provide a code in codesandbox.ioBoussadjra Brahim
@BoussadjraBrahim I have uploaded the code, but I don't know why it's not working. codesandbox.io/s/… Also I have uploaded the zip code here drive.google.com/open?id=1vR5fnHgcP56hqDg7yldh44iiGdzMQh58Divya
the vue router is not configured in the codesandboxBoussadjra Brahim
I have uploaded the code as it was in my localhostDivya

4 Answers

2
votes

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:

  1. 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.
  2. Clicking auto-suggestion adds the auto-suggestion as a tag.
  3. BACKSPACE on an auto-suggestion tag deletes it.

Feature 1 implementation:

  1. Add refs to the vue-simple-suggest and b-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>
  1. Add a keydown-handler on the inner input of b-form-tags:
export default {
  mounted() {
    //...

    // <b-form-tags ref="tags">
    const input = this.$refs.tags.getInput()
    input.addEventListener('keydown', e => this.onKeyDown(e))
  },
}
  1. 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 = ''
          }
        }
      }
    }
  }
}
  1. To prevent conflict with our handler, disable b-form-tag's automatic tag-adding upon ENTER by adding no-add-on-enter prop:
<b-form-tags no-add-on-enter />

Feature 2 implementation:

  1. Bind a suggestion-click-event handler:
<vue-simple-suggest @suggestion-click="onSuggestionClick">
  1. Implement the handler as follows:
export default {
  methods: {
    onSuggestionClick(suggestion) {
      this.$refs.tags.addTag(suggestion);
    },
  }
}

Feature 3 implementation:

  1. Add the remove-on-delete prop to b-form-tags:
<b-form-tags remove-on-delete />

full demo


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 :)

0
votes

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 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;
  }); **/
}

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.

0
votes

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.

0
votes

I have never use these two libs, just a little bit advise.

I think the main point is don't use the UI provided by vue-simple-suggest, you know , the UI framework choosed is BootstrapVue and it wouldn't like others getting in its enviroment.

Just using the API to fetch suggestions instead.

The steps may look like this:

1. Show dropdown menu with {fetchSuggestions(something)} when typeing {something}
2. Add a pill labeled with the {suggestion} when {suggestion} being clicked

Hoping that will be helpful.