Search code examples
formsvue.jstagsvue-componentvuejs3

How to change text inputs into tags, in an existing Vue 3 form, using a Tag Input Component?


I have an existing form in a Vue 3 project that posts successfully to a Cloud Firestore database.

How do I use change two of the type="text" input fields into tag inputs, using a Tag Input Component like this? Here's a link to it on CodeSandbox

The standalone tutorial is very clear and works fine. What I'm struggling with is how to incorporate that into an existing Vue 3 form and continue posting everything to the Firestore database.

I stripped-away styles and many other input fields from the "base" code below:

<template>
  
<form @submit.prevent="handleSubmit" >

    <h3>What kind of pet?</h3>
    <div class="space-y-0">
        <select required name="pet_type" id="pet_type" v-model="pet_type">
          <option value="">Please select</option>
          <option value="cat">Cat</option>
          <option value="dog">Dog</option>
          <option value="wolverine">Wolverine</option>
        </select>
      </div>

      <hr>

      <div> <!-- Want to change this into the first tag input -->
        <h3>Pros</h3>
        <div for="petPros">What are 3 positive things about this pet?</div>
        <input type="text" placeholder="separate , with , commas" id="petPros" v-model="petPros">
        <label for="petPros">separate , with , commas</label>
      </div>

      <hr>

     <div> 
        <h3>Cons</h3>  <!-- Want to change this into a second tag input -->
        <div for="petCons">And what are 3 negative things about this pet?</div>
        <input type="text" placeholder="separate , with , commas" id="petPros" v-model="petCons">
        <label for="petCons">separate , with , commas</label>
      </div>

      <hr>


<h3>Privacy</h3>
      
      <div>
          <div>
            <input type="radio" id="Fullpublic" value="Fullpublic" name="reviewPrivacy" required v-model="reviewPrivacy">
            <label class="text-base inline-flex ml-4 align-bottom" for="Fullpublic">Public</label>
          </div>

          <div>
            <input type="radio" id="keepFullyPrivate" value="keepFullyPrivate" name="reviewPrivacy" required v-model="reviewPrivacy">
            <label class="text-base inline-flex ml-4 align-bottom" for="keepFullyPrivate">Private</label>
          </div>
      </div>

      <hr>

      <button v-if="!isPending" >Submit</button>
      <button v-else disabled>Saving...</button>
    </form>

</template>

<script>
import { ref } from 'vue'
import useStorage from '@/composables/useStorage'
import useCollection from '@/composables/useCollection'

export default {
    setup() {

    const { filePath, url, uploadImage } = useStorage()
    const { error, addDoc } = useCollection('reviews')
    const { user } = getUser()
    const router = useRouter()

    const pet_type = ref('')
    const petPros = ref('')
    const petCons = ref('')
    const reviewPrivacy = ref('')


    const file = ref(null)
    const fileError = ref(null)
    const isPending = ref(false)

    const handleSubmit = async () => {
      if (file.value) {
        isPending.value = true
        await uploadImage(file.value)
        const res = await addDoc({
          pet_type: pet_type.value,
          petPros: petPros.value,
          petCons: petCons.value,
          reviewPrivacy: reviewPrivacy.value,

          userId: user.value.uid,
          userName: user.value.displayName,

          createdAt: timestamp()
        })
        isPending.value = false
        if (!error.value) {
          router.push({ name: 'ReviewDetails', params: { id: res.id }})
        }
      }
    }

    // allowed file types
    const types = ['image/png', 'image/jpeg']

    const handleChange = (e) => {
      let selected = e.target.files[0]
      console.log(selected)

      if (selected && types.includes(selected.type)) {
        file.value = selected
        fileError.value = null
      } else {
        file.value = null
        fileError.value = 'Please select an image file (png, jpg or jpeg)'
      }
    }
    
    return { pet_type, petPros, petCons, reviewPrivacy, handleSubmit, fileError, handleChange, isPending }  
  }
}
</script>


<style>

</style>

Thanks for any help!


Solution

    1. In the form, replace the two <input type="text">s with <TagInput>. Keep the v-model the same, as TabInput implements v-model.

      <!-- BEFORE -->
      <input type="text" placeholder="separate , with , commas" id="petPros" v-model="petPros">
      <input type="text" placeholder="separate , with , commas" id="petPros" v-model="petCons">
      
      <!-- AFTER -->
      <TagInput id="petPros" v-model="petPros" />
      <TagInput id="petCons" v-model="petCons" />
      
    2. Locally register TagInput.vue in the form component:

      import TagInput from "./TagInput.vue";
      
      export default {
        components: {
          TagInput,
        },
      }
      
    3. Change the initial values of petPros and petCons refs to arrays, as the TabInput outputs a string array of the input tag values:

      // BEFORE
      const petPros = ref('')
      const petCons = ref('')
      
      // AFTER
      const petPros = ref([])
      const petCons = ref([])
      
    4. TabInput normally adds a tag upon hitting TAB, but your placeholder suggests that you want to use the comma key , instead. To do that, update the modifier in the @keydown binding:

      <!-- BEFORE -->
      <input v-model="newTag"
         @keydown.prevent.tab="addTag(newTag)"
      >
      
      <!-- AFTER -->
      <input v-model="newTag"
         @keydown.prevent.,="addTag(newTag)"
      >
      

    demo