Repository: vuejs/vue
Pull request: Use 'click' event for checkbox and radio (fix #4620)

fix #4620 

1. use *click* event instead of *change* event.
2. add test case for #4521 

Changed files:
filename: src/platforms/web/compiler/directives/model.js
patch: @@ -62,7 +62,7 @@ function genCheckboxModel (
           : `:_q(${value},${trueValueBinding})`
       )
   )
-  addHandler(el, 'change',
+  addHandler(el, 'click',
     `var $$a=${value},` +
         '$$el=$event.target,' +
         `$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
@@ -93,7 +93,7 @@ function genRadioModel (
   let valueBinding = getBindingAttr(el, 'value') || 'null'
   valueBinding = number ? `_n(${valueBinding})` : valueBinding
   addProp(el, 'checked', `_q(${value},${valueBinding})`)
-  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
+  addHandler(el, 'click', genAssignmentCode(value, valueBinding), null, true)
 }
 
 function genDefaultModel (

filename: src/platforms/web/runtime/modules/dom-props.js
patch: @@ -29,13 +29,7 @@ function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
       if (vnode.children) vnode.children.length = 0
       if (cur === oldProps[key]) continue
     }
-    // #4521: if a click event triggers update before the change event is
-    // dispatched on a checkbox/radio input, the input's checked state will
-    // be reset and fail to trigger another update.
-    /* istanbul ignore next */
-    if (key === 'checked' && !isDirty(elm, cur)) {
-      continue
-    }
+
     if (key === 'value') {
       // store value as _value as well since
       // non-string values will be stringified
@@ -59,17 +53,15 @@ function shouldUpdateValue (
   vnode: VNodeWithData,
   checkVal: string
 ): boolean {
-  if (!elm.composing && (
+  return (!elm.composing && (
     vnode.tag === 'option' ||
     isDirty(elm, checkVal) ||
     isInputChanged(vnode, checkVal)
-  )) {
-    return true
-  }
-  return false
+  ))
 }
 
 function isDirty (elm: acceptValueElm, checkVal: string): boolean {
+  // return true when textbox (.number and .trim) loses focus and its value is not equal to the updated value
   return document.activeElement !== elm && elm.value !== checkVal
 }
 

filename: test/unit/features/directives/model-checkbox.spec.js
patch: @@ -148,7 +148,7 @@ describe('Directive v-model checkbox', () => {
       `
     }).$mount()
     document.body.appendChild(vm.$el)
-    var checkboxInputs = vm.$el.getElementsByTagName('input')
+    const checkboxInputs = vm.$el.getElementsByTagName('input')
     expect(checkboxInputs[0].checked).toBe(false)
     expect(checkboxInputs[1].checked).toBe(false)
     expect(checkboxInputs[2].checked).toBe(true)
@@ -173,7 +173,7 @@ describe('Directive v-model checkbox', () => {
           '<input type="checkbox" value="true" v-model="test">' +
         '</div>'
     }).$mount()
-    var checkboxInput = vm.$el.children
+    const checkboxInput = vm.$el.children
     expect(checkboxInput[0].checked).toBe(false)
     expect(checkboxInput[1].checked).toBe(true)
     expect(checkboxInput[2].checked).toBe(false)
@@ -217,6 +217,46 @@ describe('Directive v-model checkbox', () => {
     }).then(done)
   })
 
+  // #4521
+  it('should work with click event', (done) => {
+    const vm = new Vue({
+      data: {
+        num: 1,
+        checked: false
+      },
+      template: '<div @click="add">click {{ num }}<input ref="checkbox" type="checkbox" v-model="checked"/></div>',
+      methods: {
+        add: function () {
+          this.num++
+        }
+      }
+    }).$mount()
+    document.body.appendChild(vm.$el)
+    const checkbox = vm.$refs.checkbox
+    checkbox.click()
+    waitForUpdate(() => {
+      expect(checkbox.checked).toBe(true)
+      expect(vm.num).toBe(2)
+    }).then(done)
+  })
+
+  it('should get updated with model when in focus', (done) => {
+    const vm = new Vue({
+      data: {
+        a: '2'
+      },
+      template: '<input type="checkbox" value="1" v-model="a"/>'
+    }).$mount()
+    document.body.appendChild(vm.$el)
+    vm.$el.click()
+    waitForUpdate(() => {
+      expect(vm.$el.checked).toBe(true)
+      vm.a = 2
+    }).then(() => {
+      expect(vm.$el.checked).toBe(false)
+    }).then(done)
+  })
+
   it('warn inline checked', () => {
     const vm = new Vue({
       template: `<input type="checkbox" v-model="test" checked>`,

filename: test/unit/features/directives/model-radio.spec.js
patch: @@ -197,6 +197,61 @@ describe('Directive v-model radio', () => {
     }).then(done)
   })
 
+  // #4521
+  it('should work with click event', (done) => {
+    const vm = new Vue({
+      data: {
+        num: 1,
+        checked: 1
+      },
+      template:
+        '<div @click="add">' +
+          'click {{ num }}<input name="test" type="radio" value="1" v-model="checked"/>' +
+          '<input name="test" type="radio" value="2" v-model="checked"/>' +
+        '</div>',
+      methods: {
+        add: function () {
+          this.num++
+        }
+      }
+    }).$mount()
+    document.body.appendChild(vm.$el)
+    const radios = vm.$el.getElementsByTagName('input')
+    radios[0].click()
+    waitForUpdate(() => {
+      expect(radios[0].checked).toBe(true)
+      expect(radios[1].checked).toBe(false)
+      expect(vm.num).toBe(2)
+      radios[0].click()
+    }).then(() => {
+      expect(radios[0].checked).toBe(true)
+      expect(radios[1].checked).toBe(false)
+      expect(vm.num).toBe(3)
+      radios[1].click()
+    }).then(() => {
+      expect(radios[0].checked).toBe(false)
+      expect(radios[1].checked).toBe(true)
+      expect(vm.num).toBe(4)
+    }).then(done)
+  })
+
+  it('should get updated with model when in focus', (done) => {
+    const vm = new Vue({
+      data: {
+        a: '2'
+      },
+      template: '<input type="radio" value="1" v-model="a"/>'
+    }).$mount()
+    document.body.appendChild(vm.$el)
+    vm.$el.click()
+    waitForUpdate(() => {
+      expect(vm.$el.checked).toBe(true)
+      vm.a = 2
+    }).then(() => {
+      expect(vm.$el.checked).toBe(false)
+    }).then(done)
+  })
+
   it('warn inline checked', () => {
     const vm = new Vue({
       template: `<input v-model="test" type="radio" value="1" checked>`,