user11668595
user11668595

Reputation:

how to validate both input fields when one updates

I am trying to validate both input fields when one changes its value. This is required because otherwise the validation between those two fields would not work properly.

I created an example to reproduce the problem, the html code should be self explanatory

<div id="app">
  <v-app id="inspire">
          <v-text-field
              :value="values[0]"
              :rules="firstRules"
              @input="setFirstValue"
            ></v-text-field>

            <v-text-field
              :value="values[1]"
              :rules="secondRules"
              @input="setSecondValue"
            ></v-text-field>
  </v-app>
</div>

It is important to note that a v-model is not possible because this component takes in the values as a prop and passes the updated values back to the parent via emitting update events.

The vue instance:

new Vue({
  el: '#app',
  data () {
    return {
      values: [5345, 11],
      firstRules: [true],
      secondRules: [true]
    }
  },
  created: function () {
    this.setFirstValue(this.values[0])
    this.setSecondValue(this.values[1])
  },
  computed: {
    firstValidation: function () {
      return [value => value.length < this.values[1].length || "Must have less characters than second value"]
    },
    secondValidation: function () {
      return [value => value.length > this.values[0].length || "Must have more characters than first value"]
    }
  },
  methods: {
    setFirstValue: function (newValue) {
      this.values[0] = newValue
      this.firstRules = this.validateValue(this.values[0], this.firstValidation)
      this.secondRules = this.validateValue(this.values[1], this.secondValidation)
    },
    setSecondValue: function (newValue) {
      this.values[1] = newValue
      this.secondRules = this.validateValue(this.values[1], this.secondValidation)
      this.firstRules = this.validateValue(this.values[0], this.firstValidation)
    },
    validateValue: function (value, rules) {
      for (const rule of rules) {
          const result = rule(value)

          if (typeof result === 'string') {
            return [result]
          }
      }

      return [true]
    }
  }
})

On "start" the rules return a valid state but I want to validate both fields when loading the component (created hook?) to update this state immediately.

I have to put the validation rules to the computed properties because they have to access the current values. Otherwise they would validate old values.

Each input event will validate both fields and updates the rules state.

I created an example to play around here

https://codepen.io/anon/pen/OeKVME?editors=1010

Unfortunately two problems come up:

How can I setup a validation for both fields when one field updates?

Upvotes: 17

Views: 24289

Answers (3)

Freewi
Freewi

Reputation: 141

If you cannot call this.$refs.form.validate(); because it is in a parent component, what you can do is put a ref attribute on your v-text-field.

<v-text-field ref="textFieldRef"></v-text-field>

And call it's validate fonction where you want

this.$refs.textFieldRef.validate();

Upvotes: 1

Afril
Afril

Reputation: 1

I have to do like below to make it work.

watch: {
    rangeAmuount: {
      async handler() {
        await this.$nextTick()
        if (this.$refs.form) (this.$refs.form as any).validate()
      },
      deep: true,
    },
}

PS: I'm using typescript on Vue2.

Upvotes: 0

thanksd
thanksd

Reputation: 55644

Seems like you're overthinking things.

By default, a vuetify input's validation logic only triggers when the value bound to that input changes. In order to trigger the validation for the other input, you can wrap both inputs in a v-form component and give it a ref attribute. That way, you'll have access to that component's validate method, which will trigger the validation logic for any inputs inside the form.

The template would look something like this:

<v-form ref="form">
  <v-text .../>
  <v-text .../>
</v-form>

And to trigger the validation in your script:

mounted() {
  this.$refs.form.validate();
}

The above will validate the form when the component is mounted, but you'll also need to trigger the validation for both inputs whenever either input's value changes. For this, you can add a watcher to values. However, you'll need to call the form's validate method after Vue has updated the DOM to reflect the change in values.

To do this, either wrap the call in a this.$nextTick call:

watch: {
  values() {
    this.$nextTick(() => {
      this.$refs.form.validate();
    });
  }
}

Or use an async function and await this.$nextTick:

watch: {
  async values() {
    await this.$nextTick();
    this.$refs.form.validate();
  }
}

So now validation will trigger for both inputs when the component is initialized and whenever either value changes. However, if you prefer to keep the validation call in one spot instead of in both the mounted hook and the values watcher, you can make the watcher immediate and get rid of the call in the mounted hook.

So here's the final example:

watch: {
  immediate: true,
  async handler() {
    await this.$nextTick();
    this.$refs.form.validate();
  }
}

So now the validation logic is triggering when it would be expected to, but there is still one issue with your validation logic. When your component initializes, the values data property is set to an array of Number type values, which don't have a length property. So if, for example, you changed just the first input to "5" and the second input was still 11, then (11).length is undefined and "5".length < undefined is false.

Anyways, you'll need to change the values you're comparing to strings before comparing their lengths. Something like this:

value => (value + '').length < (this.values[1] + '').length

Finally, because you are able to dynamically call validate on the form, there's an opportunity to reduce much of the complexity of your component.

Here's a simplified version:

Vue.config.devtools = false;
Vue.config.productionTip = false;

new Vue({
  el: '#app',
  data() {
    return {
      values: [5345, 11]
    }
  },
  computed: {
    rules() {
      const valid = (this.values[0] + '').length < (this.values[1] + '').length;
      return {
        first: [() => valid || "Must have less characters than second value"], 
        second: [() => valid || "Must have more characters than first value"]
      };
    }
  },
  watch: {
    values: {
      immediate: true,
      async handler() {
        await this.$nextTick();
        this.$refs.form.validate();
      }
    }
  }
})
<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://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>

<div id="app">
  <v-app id="inspire">
    <v-form ref="form">
      <v-text-field v-model="values[0]" :rules="rules.first"></v-text-field>
      <v-text-field v-model="values[1]" :rules="rules.second"></v-text-field>
    </v-form>
  </v-app>
</div>

Upvotes: 26

Related Questions