1
votes

I am trying to modify the sample code at https://vuetifyjs.com/en/components/autocompletes#example-scoped-slots to allow arbitrary content not matching any autocomplete items in between chips (so user can tag other users in a message similar to slack and facebook)

So for example, the user could type "Sandra" and then select "sandra adams", then type "foo" and then type another space and start typing "John" and the autcomplete would pop up again and allow the user to select "John Smith".

I've been through all the properties in the docs and there doesn't seem to be support for this built in.

I tried using custom filtering to ignore the irrelevant parts of the message when displaying autocomplete options, but the autocomplete seems to remove non-chip content when it loses focus and I can't see a property that allows me to prevent this behavior.

not sure if the autcomplete is the thing to be using or if I would be better off hacking combo box to meet this requirement, because this sample seems closer to what I'm tryng to do https://vuetifyjs.com/en/components/combobox#example-no-data, but then I believe I lose the ajax capabilities that come with automcomplete.

2

2 Answers

2
votes

You can achieve this by combining the async search of the autocomplete with the combobox.

For example:

new Vue({
  el: '#app',
  data: () => ({
    activator: null,
    attach: null,
    colors: ['green', 'purple', 'indigo', 'cyan', 'teal', 'orange'],
    editing: null,
    descriptionLimit: 60,
    index: -1,
    nonce: 1,
    menu: false,
    count: 0,
    model: [],
    x: 0,
    search: null,
    entries: [],
    y: 0
  }),
   computed: {
      fields () {
        if (!this.model) return []

        return Object.keys(this.model).map(key => {
          return {
            key,
            value: this.model[key] || 'n/a'
          }
        })
      },
      items () {
        return this.entries.map(entry => {
          const Description = entry.Description.length > this.descriptionLimit
            ? entry.Description.slice(0, this.descriptionLimit) + '...'
            : entry.Description

          return Object.assign({}, entry, { Description })
        })
      }
    },

  watch: {
    search (val, prev) {
    
        // Lazily load input items
        axios.get('https://api.publicapis.org/entries')
          .then(res => {
          console.log(res.data)
            const { count, entries } = res.data
            this.count = count
            this.entries = entries
          })
          .catch(err => {
            console.log(err)
          })
          .finally(() => (this.isLoading = false))
          
      /*if (val.length === prev.length) return

      this.model = val.map(v => {
        if (typeof v === 'string') {
          v = {
            text: v,
            color: this.colors[this.nonce - 1]
          }

          this.items.push(v)

          this.nonce++
        }

        return v
      })*/
    },
     model (val, prev) {
        if (val.length === prev.length) return

        this.model = val.map(v => {
          if (typeof v === 'string') {
            v = {
              Description: v
            }

            this.items.push(v)

            this.nonce++
          }

          return v
        })
      }
  },

  methods: {
    edit (index, item) {
      if (!this.editing) {
        this.editing = item
        this.index = index
      } else {
        this.editing = null
        this.index = -1
      }
    },
    filter (item, queryText, itemText) {
      const hasValue = val => val != null ? val : ''

      const text = hasValue(itemText)
      const query = hasValue(queryText)

      return text.toString()
        .toLowerCase()
        .indexOf(query.toString().toLowerCase()) > -1
    }
  }
})
<link href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons' rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js" integrity="sha256-mpnrJ5DpEZZkwkE1ZgkEQQJW/46CSEh/STrZKOB/qoM=" crossorigin="anonymous"></script>

<div id="app">
<v-app>
<v-content>
        <v-container>
<v-combobox
    v-model="model"
    :filter="filter"
    :hide-no-data="!search"
    :items="items"
    :search-input.sync="search"
    hide-selected
    label="Search for an option"
    :allow-overflow="false"
    multiple
    small-chips
    solo
    hide-selected
    return-object
    item-text="Description"
    item-value="API"
    :menu-props="{ closeOnClick: false, closeOnContentClick: false, openOnClick: false, maxHeight: 200 }"
    dark
  >
   <template slot="no-data">
      <v-list-tile>
        <span class="subheading">Create</span>
        <v-chip
          label
          small
        >
          {{ search }}
        </v-chip>
      </v-list-tile>
    </template>
    <template
      v-if="item === Object(item)"
      slot="selection"
      slot-scope="{ item, parent, selected }"
    >
      <v-chip
        :selected="selected"
        label
        small
      >
        <span class="pr-2">
          {{ item.Description }}
        </span>
        <v-icon
          small
          @click="parent.selectItem(item)"
        >close</v-icon>
      </v-chip>
    </template>
    <template
      slot="item"
      slot-scope="{ index, item, parent }"
    >
      <v-list-tile-content>
        <v-text-field
          v-if="editing === item.Description"
          v-model="editing"
          autofocus
          flat
          hide-details
          solo
          @keyup.enter="edit(index, item)"
        ></v-text-field>
        <v-chip
          v-else
          dark
          label
          small
        >
          {{ item.Description }}
        </v-chip>
      </v-list-tile-content>
    </template>
  </v-combobox>
  </v-container>
  </v-content>
  </v-app>
</div>
0
votes

so I ended up building a renderless component that is compatible with vuetify as it goes through the default slot and finds any of the types of tags (textarea, input with type of text, or contenteditable) that tribute supports, and allows you to put arbitrary vue that will be used to build the tribute menu items via a scoped slot.

in future might try to wrap it as a small NPM package to anyone who wants a declarative way to leverage tribute.js for vue in a more flexible way than vue-tribute allows, but for now here's my proof of concept

InputWithMentions.vue

<script>
import Tribute from "tributejs"
// eslint-disable-next-line 
import * as css from "tributejs/dist/tribute.css"
import Vue from "vue"

export default {
    mounted() {
       let menuItemSlot = this.$scopedSlots.default

        let tribute = new Tribute({
          menuItemTemplate: item => 
          {
              let menuItemComponent =  
                new Vue({
                    render: function (createElement) { 
                        return createElement('div', menuItemSlot({ menuItem: item }))
                    }
                })

                menuItemComponent.$mount()
                return menuItemComponent.$el.outerHTML
          },
           values: [
                {key: 'Phil Heartman', value: 'pheartman'},
                {key: 'Gordon Ramsey', value: 'gramsey'}
          ]})

          tribute.attach(this.$slots.default[0].elm.querySelectorAll('textarea, input[type=text], [contenteditable]'))
    },
    render(createElement) {
        return createElement('div', this.$slots.default)
    }
}
</script>

User.vue

   <InputWithMentions>
          <v-textarea
            box
            label="Label"
            auto-grow
            value="The Woodman set to work at once, and so sharp was his axe that the tree was soon chopped nearly through.">
          </v-textarea>
          <template slot-scope="{ menuItem }">
                <v-avatar size="20" color="grey lighten-4">
                  <img src="https://vuetifyjs.com/apple-touch-icon-180x180.png" alt="avatar">
                </v-avatar>
                {{ menuItem.string }}
          </template>
     </InputWithMentions>