vue: Checkbox doesn't have it's checked state set via v-model if the click event is prevented

Version

2.6.6

Reproduction link

https://jsfiddle.net/s4ub8dnc/3/

Steps to reproduce

I have a checkbox which needs to get it’s checked state from a VueX store, but I also need to be able to determine whether or not shift was being held when the checkbox was clicked. To do this I’m using @click.prevent on the checkbox (so that I have the event being passed to the method) and then using v-model to determine the checked state.

However, regardless of whether or not the v-model value is true or false, the checkbox never gains the checked state if the click is prevented. It seems that there is still a reliance on the click event in order to set the state.

What is expected?

I would expect the checkbox to be checked if the v-model value is set to true, regardless of how the checkbox arrived at that state - by something else setting it to true, or by the click handler setting it.

What is actually happening?

The click binding having .prevent on it stops the checkbox from receiving it’s checked state, even when the v-model value is true


The reason why I haven’t just used a setter in the computed is because the setter doesn’t get given the click event, so I can determine whether or not shift was being held at the point of clicking the checkbox.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 21 (8 by maintainers)

Most upvoted comments

As far as I’m aware there’s no other documented method of setting the checked state other than v-model.

What about this?

<input type="checkbox" v-bind:checked="selected" @change="handleChange">

Good old v-bind and v-on.

The problem with this argument is that if you implement the exact same behaviour in pure JS, it works:

Actually, your example here uses the change event, which is why it works. If you change your “vanilla js” examle to use the click event, like you did in the original code, it doesn’t work:

https://jsfiddle.net/Linusborg/qr2j9x4g/

So I’d argue the behaviour you are seeing with Vue 2.6 now is 100% in line with vanilla JS behaviour.

Just adding I ran into the same problem as well and it took a while to figure it out, so at the very least it is unexpected behavior. I’d summaries it as follows: “If using click.prevent handler on a checkbox that has its checked state bound to property X and changing that property X in the handler the checked state is not updated according to the property X”. I would consider this a bug as well, I do not see how click.prevent should cause dom and state getting out of sync.

My use case is a checkbox that is allowed to be unchecked normally but if it is to be checked a popup warns the user of some side effects. So I need to prevent the default behavior and in the handler decide whether the checkbox should be checked immediately (which is where I encountered the problem) or whether a popup is shown instead and the checkbox is only set after confirming.

Example with the problem and the workaround (setTimeout) applied (works in 2.5, breaks in 2.6): https://codesandbox.io/s/vue-template-obv1e?file=/src/App.vue

The two Checkboxes next to each other are bound to the same property and the one that is clicked is no longer in sync with the model unless the setTimeout workaround is applied.

https://jsfiddle.net/rwe2t04h/8/

I used setTimeout that will be execute right after all micros task will be execute.

Work for me. I think it’s because of the window.confirm that is a browser script and it execute immediately so i think the dom refresh update time is before the window.confirm that’s weird to me. I think something missing here

The primary case where the checkbox is not set is when it is some critical option which could cause additional side effects. In these cases, there is a modal displayed with additional information for the user to confirm their choice (and prior to that decision they can not interact with the checkbox.

Since the choice of state is taken from the input element or component, in this situation it would actually make sense to simply ignore the input event completely and instead use àn @click.prevent. You really want to prevent the DOM to change from this user interaction, so using the mechanism intended for this makes sense.

https://jsfiddle.net/Linusborg/h4xaf86q/

Your latest post isn’t using the same setup, so the premise changed.

Sorry for my confusion. Also, actually I obviously missed that with the text input (it does have the same issue). Reverting the change works well, thanks.

In general I would personally think of this as bad UX though.

I actually agree with this. The primary case where the checkbox is not set is when it is some critical option which could cause additional side effects. In these cases, there is a modal displayed with additional information for the user to confirm their choice (and prior to that decision they can not interact with the checkbox. It is not something that is common (but it is definitely a use case we have). Doing this using the @input works nicely as we can pass the change to something handling the modal and the new state can simply be the output of that process.

I am very confused as to why you feel “the usage doesn’t really make much sense”.

That was specifically referring to using v-model plus some secondary event that also changes state or tries to prevent it -the original example. If you are using v-model, you want a direct 1:1 data binding - no conditionals or any other special “in between” behaviour. If you don’t want that, don’t use v-model

Your latest post isn’t using the same setup, so the premise changed.

Concerning your example:

  1. The text input would have the same issue as the checkbox if it had a similar timeout.
  2. If you want to defer the control over the checkbox’s checked state to the parent, you canundo the change that the user interaction did, like this:
 methods: {
    input(e) {
        this.$emit("input", e.target.checked)
        e.target.checked = !e.target.checked
    }
  }

Updated Example: https://jsfiddle.net/Linusborg/h5k37b4e/

In general I would personally think of this as bad UX though. Users expect checkboxes (that are not disabled) to be checked immediately when they click them. The user communicates intent with this interaction, they want the checkbox checked. You should not prevent that and instead accept it. If you have to revert this user choice later (asynchronously) because of reason X, let the user know about why you overruled their choice.

@yyx990803

I’d say this is a wontfix since the usage doesn’t really make much sense… preventing the click event has the side effect of preventing the checkbox from being checked, and the fact that it worked in 2.5 was just an accident. This was never meant to work.

There’s definitely some way to achieve what you want without having to do this, and it would be helpful if there’s more information on why you are doing this in the first place.

I actually agree that preventDefault seemed like a broken solution initially. I initially arrived at that solution because preventing the value from changing outside the element by simply not changing the prop passed down did not work. What I don’t agree with is that “the usage doesn’t really make any sense” (not entirely sure if you mean preventDefault or not changing the value). This seems pretty counter-intuitive compared to the behavior of other element types (except maybe radio which is also broken?). For any other type of element, we can easily respond to an input event and then at a location outside the element which is actually receiving the input event. This could mean setting state the same way as the browser would, but it could also mean not doing this. This also means that at any point in time, we can consider the rendered DOM as always a function of props (f(props) => rendered result) for a simple component. What you are seem to be stating is that, “no” the owner of that state is specifically the DOM element, and it controls and owns that state. This means we can no longer assume f(props) => rendered result because it is now actually f(props, DOM state) => rendered result where basically, the DOM state can be potentially out of sync from props. This also means (much more importantly), I can no longer rely on the values passed in as props to always result in the same rendered DOM (it is now very situational).

I had some use cases that I feel are quite legitimate use cases for styled checkboxes as well as being able to have some async task which must complete prior to determining if the state changed or not. I documented some basic description of that here: https://github.com/vuejs/vue/issues/10045

Yes, there are many other things I could do to implement the same thing. Using a simple checkbox seems the most intuitive way to accomplish this with no additional logic involved to accomplish this.

I am very confused as to why you feel “the usage doesn’t really make much sense”. Seeing that I am not the only one reporting this issue seems to indicate otherwise. It seems quite natural based on other behaviors within Vue that if state is owned outside a component, then it can be controlled there. That means, my expectation would be that if someone clicked a checkbox but the event did not trigger a change in the value which is passed to the checkbox within the component, then it’s state should not change. I changed my example from my issue report and added an text input with an example. This input behaves as I would expect where I can set a different value than the value which is set during the event. I updated the checkbox to use this same type of behavior. The text input behaves as expected and the rendered result is what I set the prop value passed to the component. The checkbox input does not behave in the same manner in that even though the prop value does not change in some cases, the rendered result actually changes the value. https://jsfiddle.net/goblinlord/k64bx8gp/24/

Those are very reasonable points, I’d missed the event being different and I can certainly see it reproduces with pure JS. In that case, I’ll concur with your assessment of it being expected behaviour and adjust my approach. Thanks

Then in this case you probably shouldn’t be using v-model at all?

As far as I’m aware there’s no other documented method of setting the checked state other than v-model. I’d prefer not to set .checked manually (perhaps in a watcher) as that feels like it goes against the point of having Vue manage the DOM state. Given that the workaround is simply to nextTick the setting of the value, this does feel like more of a bug.

I’d be interested to see what your approach would be to achieve the aims in my use-case outlined above.

My team is also having this issue, we have a component library with a checkbox component. One of our developer’s use cases is this:

https://jsfiddle.net/linnea/uhm8a2b7/5/

They need to check some permission or prompt a confirmation before changing the value of the checkbox. The value changes to true, but the checkbox doesn’t actually appear checked

The setTimeout workaround works, but that means they’ll have to update all @click.prevents on their checkboxes with this workaround before updating.

I think this is working as expected but the scenario and browser behaviour is weird and makes it complicated: you are preventing the checkbox to get checked but still setting the state on the same function, it’s like saying to the browser not to tick the checkbox at the same time you change the state that is supposed to control that checkbox: it doesn’t make any sense. Adding a delay of 0ms before setting the state works around the problem and I think that given the case, it’s good enough. What is even the use case for this? If you want to control whether it’s possible to check the checkbox or not, you are better off preventing default inside of the handler only when you don’t want to set the checked state