0
votes

I am using Vue 2 and Vuetify (not Vue 3) to create a form builder website. I was going perfectly well until I found out that something is wrong. So here's the case. I rendered text fields (inputs) from a reactive array using the following code.

<template>
// ... some other unrelated code
<template v-if="answer.type !== 1">
  <v-col 
    v-for="(_, i) in answer.options" 
    :key="`#question-${answer.id}-${i}`" 
    cols="12"
  >
    <div class="flex flex-row justify-between items-center">
      <TextField
        v-model="answer.options[i]"
        class="ml-4" 
        label="Option"
        underlined 
        hideLabel 
      />
      <v-btn @click="deleteOption(i)" icon>
        <v-icon>mdi-close</v-icon>
      </v-btn>
    </div>
  </v-col>
  <v-col>
    <TextButton @click="addOption()" text="+ ADD OPTION" />
  </v-col>
</template>
// ... some other unrelated code
</template>

<script>
  import { reactive, ref, watch } from '@vue/composition-api'
  import useInquiry from '@/composables/useInquiry'
  import TextButton from '@/components/clickables/TextButton.vue'
  import TextField from '@/components/inputs/TextField.vue'

  export default {
    components: { TextField, TextButton },
    setup() {
      const { answer, addOption, deleteOption } = useInquiry()

      return { answer, addOption, deleteOption }
    }
  }
</script>

Here's my useInquiry composable logic

import { reactive, watch, se } from '@vue/composition-api'
import ID from '@/helpers/id'

export default () => {
  const answer = reactive({
    id: ID(), // this literally just generates an ID
    type: 2,
    options: ['', '']
  })

  const addOption = () => {
    answer.options.push('')
  }

  const deleteOption = at => {
    const temp = answer.options.filter((_, i) => i !== at)
    answer.options = []
    answer.options = temp
  };

  return { answer, addOption, deleteOption }
}

And finally, here's my TextField.vue

<template>
  <v-text-field 
    v-model="inputValue"
    :label="label"
    :single-line="hideLabel"
    :type="password ? 'password' : 'text'"
    :outlined="!underlined"
    :dense="!large"
    hide-details="auto"
  />
</template>

<script>
  import { ref, watch } from '@vue/composition-api'

  export default {
    model: {
      props: 'value',
      event: 'change'
    },
    props: {
      label: String,
      value: String,
      password: Boolean,
      underlined: Boolean,
      large: Boolean,
      hideLabel: Boolean
    },
    setup(props, context) {
      const inputValue = ref(props.value)

      watch(inputValue, (currInput, prevInput) => {
        context.emit('change', currInput)
      })

      return { inputValue }
    }
  }
</script>

The problem is, everytime the delete button is clicked, the deleted input is always the last one on the array, even though I didn't click on last one. I tried to log my reactive array by watching it using Vue's composition watch method. Apparently, the data is correctly updated. The problem is the v-model looks un-synced and the last input is always the one that gets deleted.

1
I think I spotted the real issue...check my updated answer... - Michal Levý

1 Answers

1
votes

Looks good to me ....except the TextField.vue

Problem is in TextField.vue. What you actually doing inside setup of TextField.vue is this (Vue 2 API):

data() {
  return {
    inputValue: this.value
  }
}

...this is one time initialization of inputValue data member with value of value prop. So when one of the options is removed and components are reused (because that's what Vue does all the time - especially when index is used in :key) inputValue is not updated to a new value of value prop...

You don't need inputValue or model option at all, just remove it and use this template:

<v-text-field 
    :value="value"
    @input="$emit('input', $event)"
    :label="label"
    :single-line="hideLabel"
    :type="password ? 'password' : 'text'"
    :outlined="!underlined"
    :dense="!large"
    hide-details="auto"
  />

NOTE that in my example I'm using $event.target.value instead of just $event because I'm working with native <input> element and not Vuetify's custom component input...

working example...

const {
  reactive,
  ref,
  watch
} = VueCompositionAPI
Vue.use(VueCompositionAPI)

const BrokenInput = {
  model: {
    props: 'value',
    event: 'change'
  },
  props: {
    value: String,
  },
  setup(props, context) {
    const inputValue = ref(props.value)

    watch(inputValue, (currInput, prevInput) => {
      context.emit('change', currInput)
    })

    return {
      inputValue
    }
  },
  template: `<input type="text" v-model="inputValue" />`
}

const FixedInput = {
  props: {
    value: String,
  },
  template: `<input type="text" :value="value" @input="$emit('input', $event.target.value)"/>`
}

const useInquiry = () => {
  const answer = reactive({
    id: 1,
    type: 2,
    options: ['1', '2', '3', '4', '5']
  })

  const addOption = () => {
    answer.options.push('')
  }

  const deleteOption = at => {
    const temp = answer.options.filter((_, i) => i !== at)
    answer.options = temp
  };

  return {
    answer,
    addOption,
    deleteOption
  }
}

const app = new Vue({
  components: { 'broken-input': BrokenInput, 'fixed-input': FixedInput },
  setup() {
    return useInquiry()
  },
})
app.$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@vue/[email protected]"></script>
<div id="app">
  <table>
    <tr>
      <th>Option</th>
      <th>Broken input</th>
      <th>Model</th>
      <th>Fixed input</th>
      <th></th>
      <tr>
        <tr v-for="(_, i) in answer.options" :key="'#question-'+ answer.id + '-' + i">
          <td>Option {{ i }}:</td>
          <td>
            <broken-input v-model="answer.options[i]" />
          </td>
          <td>
            {{ answer.options[i] }}
          </td>
          <td>
            <fixed-input v-model="answer.options[i]" />
          </td>
          <td><button @click="deleteOption(i)">Remove option</button></td>
        </tr>
  </table>
  <button @click="addOption()">Add option</button>
</div>