Akshdeep Singh
Akshdeep Singh

Reputation: 1457

Vue.js: @input for <input> not working with v-for

I am creating my own custom <input> Vue component. What I am doing is that the user can never enter the wrong type of input. For that I am using regex.test() at each input.

This is my code for my Vue component for taking an integer element or an integer array:

<template>
    <div>
        <label>{{ label }}
            <template v-if="isArray">
                <input 
                    v-model="arr[i - 1]" 
                    @input="filterInput" 
                    :disabled="disableWhen" 
                    v-for="i in arraySize" 
                    :key="i">
                </input>
            </template>
            <template v-else>
                <input 
                    v-model="num" 
                    @input="filterInput" 
                    :disabled="disableWhen">
                </input>
            </template>
        </label>
        <el-button 
            type="success" 
            icon="el-icon-check" 
            circle 
            @click="confirm" 
            :disabled="disableWhen">
        </el-button>
    </div>
</template>

<script>
    export default {
        props: {
            label: String,
            nonNegative: Boolean,
            disableWhen: Boolean,
            isArray: Boolean,
            arraySize: Number
        },
        data() {
            return {
                num: '',
                arr: []
            }
        },
        methods: {
            filterInput() {
                if (this.nonNegative) {
                    if (!/^[0-9]*$/.test(this.num)) {
                        this.num = '';
                    }
                } else if (!/^(-)?[0-9]*$/.test(this.num)) {
                    this.num = '';
                }
            },
            confirm() {
                if (this.isArray) {
                    let validArrayInput = true;
                    for (let i = 0; i < this.arraySize; i++) {
                        if (!this.validInput(this.arr[i])) {
                            validArrayInput = false;
                        }
                    }
                    if (validArrayInput) {
                        this.$emit('confirm', this.arr);
                    }
                } else if (this.validInput(this.num)) {
                    this.$emit('confirm', this.num);
                }
            },
            validInput(x) {
                return (x !== '' && x !== '-' && typeof x !== "undefined");
            }
        }
    }
</script>

The code is working correctly when isArray = false, that is, for integer elements. But the method filterInput is never being called when isArray = true, and there is no restriction for the wrong input. What is the problem?

Upvotes: 0

Views: 1723

Answers (1)

skirtle
skirtle

Reputation: 29132

filterInput is being called fine for both types of input but it only attempts to manipulate num, it doesn't change arr.

Here's my attempt at implementing this:

const MyInput = {
  template: `
    <div>
      <label>{{ label }}
        <template v-if="isArray">
          <input 
            v-for="i in arraySize" 
            v-model="arr[i - 1]" 
            :disabled="disableWhen" 
            :key="i"
            @input="filterInput" 
          >
        </template>
        <template v-else>
          <input 
            v-model="num" 
            :disabled="disableWhen"
            @input="filterInput"
          >
        </template>
      </label>
    </div>  
  `,

  props: {
    label: String,
    nonNegative: Boolean,
    disableWhen: Boolean,
    isArray: Boolean,
    arraySize: Number
  },
  
  data() {
    return {
      arr: []
    }
  },
  
  computed: {
    num: {
      get () {
        return this.arr[0]
      },
      
      set (num) {
        this.arr[0] = num
      }
    }
  },
  
  methods: {
    filterInput() {
      const arr = this.arr
      const re = this.nonNegative ? /^\d*$/ : /^-?\d*$/
      
      for (let index = 0; index < arr.length; ++index) {
        if (!re.test(arr[index])) {
          this.$set(arr, index, '')
        }
      }
    }
  }
}

new Vue({
  el: '#app',
  
  components: {
    MyInput
  }
})
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>

<div id="app">
  <my-input label="Single"></my-input>
  <br>
  <my-input label="Multiple" is-array :array-size="3"></my-input>
</div>

A few notes:

  1. I've changed num to be a computed property backed by arr[0]. This simplifies the filtering logic as it only has to consider arr for both types of input. It could be simplified further, e.g. the template doesn't really need to handle two cases, it could treat single-valued just the same as multi-valued but with array-size of 1. Only the value that's emitted (not included in my code) really needs to have different behaviour for the single-valued case. With a little refactoring num could probably be removed altogether.
  2. The implementation is painfully stateful. You're going to run into difficulties if you ever want to pass in values from the outside.
  3. Rather than setting the values to '' I would suggest just stripping out the disallowed characters using replace. I have not made this change in my code, I wanted to retain the behaviour from the original example.
  4. Closing </input> tags are invalid and I have removed them.
  5. There was a lot of duplication in your filterInput method that I've tried to remove. It now checks all the entries in the arr array. There didn't seem to be any need to target the specific input that had changed.
  6. this.$set is used as it's updating an array by index, which otherwise would not be detected by the reactivity system (the standard caveat for manipulating arrays).

Upvotes: 2

Related Questions