1

Coming from React, I am used to the concept of taking "control" of an html element value and events.

I am looking to do the same with VueJS.

I have, an input:

<input :value="foo" @input="changeFoo" pattern="[a-zA-Z0-9]+">

A computed value coming from my VueX store:

...mapState({
 foo: (state) => state.foo,
}),

A method that commits a mutation to my VueX state

changeFoo(e) { this.$store.commit('CHANGE_FOO', e.target)}

A mutation that updates the state if the regex pattern matches

CHANGE_FOO(state, target) {
  if (target.checkValidity()) {
    state.foo = target.value;
  }
 },

What works:

  • my VueX state updates
  • my input updates
  • if checkValidity returns false, the store is not updated

What doesn't work:

  • even if the store is not updated, the <input /> value changes... What I want is bind a single source of truth to my <input /> component
15
  • "even if the store is not updated, the <input /> value changes" - how does it change? Commented Nov 17, 2020 at 15:07
  • 1
    You may need to prevent the default behavior at different point, for example as soon as the validity check fails. Commented Nov 17, 2020 at 15:15
  • 1
    Or, try preventing the defaults on @keypress, that's probably the easiest way to stop certain characters from getting entered. Commented Nov 17, 2020 at 15:28
  • 1
    Actually, @keypress worked for me, I just tried :-) Let me know if you need some pointer. Commented Nov 17, 2020 at 15:40
  • 1
    I added my variation. Just an addition to your answers. They are great! :-) Commented Nov 17, 2020 at 16:40

3 Answers 3

2

Don't use :value. Use v-model with a computed:

<input v-model="foo" pattern="[a-zA-Z0-9]+" ref="fooInput">

computed: {
  foo: {
    get() {
      return this.$store.state.foo
    },
    set(value) {
      if (this.$refs.fooInput.checkValidity()) {
         this.$store.commit('CHANGE_FOO', value);
      } 
    }
  }
}

In Vue you don't "take control". You reference DOM elements.

Here's a way to prevent changing the value of the input when user inputs invalid values. It's considered bad practice.

new Vue({
  el: '#app',
  data: () => ({
    storeFoo: null
  }),
  computed: {
    foo: {
      get() {
        return this.storeFoo
      }, 
      set(value) {
        if (this.$refs.fooInput.checkValidity()) {
          this.storeFoo = value;
        }
      }
    }
  },
  methods: {
    onInput(e) {
      if (!e.target.checkValidity()) {
        e.preventDefault();
        e.target.value = this.storeFoo || null;
      }
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <input v-model="foo" pattern="[a-zA-Z0-9]+" ref="fooInput" @input="onInput">
</div>

Used a storeFoo instead of a proper store prop, for brevity.


I should also explain why preventing input changes is bad practice:

As far as UX goes, in a form you want to let the user know when the current value is invalid and why it's invalid, in the friendliest (non-obtrusive) manner possible.

If you override or prevent their actions you show a lack of trust and respect for them.

Sign up to request clarification or add additional context in comments.

13 Comments

Thanks @tao, learned some things about $refs with this methods. However the problem stays the same, for instance you can even get() { return '' } and set(value) { // do nothing }. Input will still updates its value on its own, not overriding the default behavior of the html component. At least in my use case
You don't want to prevent input default behavior. That's frustrating for users and will basically lose you (or your client) money. You should prevent invalid values from being committed and let the user know when their value is invalid. That's what's considered best-practice. Obviously, you could be aggressive and override the input value back to its previous value, but I strongly advise against it. Or you could prevent default. I'll update.
You're doing something wrong. As you can see, it is working as expected. What do you mean is not available in the $refs? $refs.fooInput returns the actual DOM element. Are you saying the <input> element doesn't have a .checkValidity() for you? That's highly improbable.
What I'm saying is pretty common bad practice. But I have absolutely no reason to convince you of this. Also, my personal choice for validation is to have it as close to the <input> as possible. Unless I had more than one input source, I'd never place it in the store. The store needs to be as clean as possible and only concerned with data and business logic. With valid data, I might add (already validated). I find this principle to be extremely productive. I find myself coding apps faster with this principle. But it might not be the same for you. I have no idea.
|
1

Normally we would use v-model to bind the input element two-way, but since you have a slightly different case of something closer to input masking, a quick workaround is to perhaps do the input validity check on keypress event as opposed to input or change. Here's a quick demo (BTW, I'm leaving out the Vuex mutation implementation):

Vue.createApp({
  setup() {
    const foo = Vue.ref('');
    const invalid = Vue.ref(false);
    
    const changeFoo = e => {
      if (!e.target.checkValidity()) {
        e.preventDefault();
        invalid.value = true;
      }
      else {
        invalid.value = false;
      }
    }

    return {
      foo,
      invalid,
      changeFoo
    }
  }
}).mount('#app');
.error {
  color: crimson;
}
<script src="https://unpkg.com/vue@next"></script>

<div id="app">
  <input 
    v-model="foo" 
    pattern="[a-zA-Z0-9]+"
    @keypress="changeFoo" />

  <p v-if="invalid" class="error">Pattern mismatch</p>
</div>

You may notice the error message not appearing until the next keystroke gets entered. This is expected behavior and is comparable to the input event.

We can fix this by checking against the keystroke instead of the whole input value.

Vue.createApp({
  setup() {
    const foo = Vue.ref('');
    const invalid = Vue.ref(false);
    const pattern = /[a-zA-Z0-9]+/;
    
    function changeFoo(e) {
      if (!checkValidity(e)) {
        e.preventDefault();
        invalid.value = true;
      }
      else {
        invalid.value = false;
      }
    }
    
    function checkValidity({ keyCode }) {
      return pattern.test(String.fromCharCode(keyCode));
    }

    return {
      pattern,
      foo,
      invalid,
      changeFoo
    }
  }
}).mount('#app');
.error {
  color: crimson;
}
<script src="https://unpkg.com/vue@next"></script>

<div id="app">
  <input 
    v-model="foo" 
    :pattern="pattern"
    @keypress="changeFoo" />

  <p v-if="invalid" class="error">You pressed a forbidden character!</p>
</div>

16 Comments

Cool. Glad it helped. :-)
@tao Then let's talk about buttons, how do you ensure the actual click action isn't tampered? The possibility is endless. After all, we're talking about UX here, aren't we?
Tampering the client side is always possible, so one has to handle common case, after all my user could also directly my input element or mess without intention with the view because of a chrome extension. It's impossible to handle every case.
That's why you should never override their input. You should only inform them when it's not valid. Your responsibility ends there. That's all I'm saying. If you want a perfect UI consider your user a king. Literally. A royalty. In my estimation, you shouldn't change a king's input. You should helpfully and kindly point out it might not work with that value. It's all about showing the user as much respect as possible.
@tao Well, with my approach we're obviously not overriding anything -- we are restricting unwanted inputs. With regard to "informing" your users, are you saying that we are to allow texts on phone/credit card fields when we should be (at least try to) preventing them from getting entered in the first place? I'd say that's a really bad UX.
|
1

Answering my own question.

First solution found by @Yom S: as pointed out during our discussion, key related events will all fire before onInput event. You can then preventDefault() on keydown event and concatenate the key value to your current string. I didn't choose it because it's verbose and splits the logic between my mutation and custom methods in component.

Second solution that I picked. Not overriding the default behavior, but overwriting it systematically in my mutation. So, before:

  if (target.checkValidity()) {
    state.foo = { name: target.value };
  }

becomes

  if (target.checkValidity()) {
    state.foo = { name: value };
  } else {
    state.foo = { name: value.substring(0, value.length - 1) };
  }

By always mutating the state each time onInput fires, I am sure that the default input behavior will always be overridden.

NOTE: I will pick the first solution if I have to do async actions before updating my state.

Third solution, using setters and getters, see @Tao solution. Same than second solution, not recommended for async calls but less hacky than preventing keypress defaults.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.