Repository: vuejs/vue
Pull request: fix($vdom): Don't replace input for text-like type change, fix #6313

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)
 
**Other information:**
fix #6313

Changed files:
filename: src/core/vdom/patch.js
patch: @@ -6,8 +6,6 @@
  *
  * modified by Evan You (@yyx990803)
  *
-
-/*
  * Not type-checking this because this file is perf-critical and the cost
  * of making flow understand it is not worth it.
  */
@@ -17,6 +15,7 @@ import config from '../config'
 import { SSR_ATTR } from 'shared/constants'
 import { registerRef } from './modules/ref'
 import { activeInstance } from '../instance/lifecycle'
+import { isTextInputType } from 'web/util/element'
 
 import {
   warn,
@@ -48,14 +47,12 @@ function sameVnode (a, b) {
   )
 }
 
-// Some browsers do not support dynamically changing type for <input>
-// so they need to be treated as different nodes
 function sameInputType (a, b) {
   if (a.tag !== 'input') return true
   let i
   const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
   const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
-  return typeA === typeB
+  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
 }
 
 function createKeyToOldIdx (children, beginIdx, endIdx) {

filename: src/platforms/web/runtime/directives/model.js
patch: @@ -3,11 +3,10 @@
  * properties to Elements.
  */
 
-import { looseEqual, looseIndexOf, makeMap } from 'shared/util'
+import { isTextInputType } from 'web/util/element'
+import { looseEqual, looseIndexOf } from 'shared/util'
 import { warn, isAndroid, isIE9, isIE, isEdge } from 'core/util/index'
 
-const isTextInputType = makeMap('text,number,password,search,email,tel,url')
-
 /* istanbul ignore if */
 if (isIE9) {
   // http://www.matts411.com/post/internet-explorer-9-oninput/

filename: src/platforms/web/util/element.js
patch: @@ -73,3 +73,5 @@ export function isUnknownElement (tag: string): boolean {
     return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()))
   }
 }
+
+export const isTextInputType = makeMap('text,number,password,search,email,tel,url')

filename: test/unit/modules/vdom/patch/edge-cases.spec.js
patch: @@ -135,4 +135,30 @@ describe('vdom patch: edge cases', () => {
       expect(vm.$el.children[0].value).toBe('a')
     }).then(done)
   })
+
+  // #6313
+  it('should not replace node when switching between text-like inputs', done => {
+    const vm = new Vue({
+      data: { show: false },
+      template: `
+        <div>
+          <input :type="show ? 'text' : 'password'">
+        </div>
+      `
+    }).$mount()
+    const node = vm.$el.children[0]
+    expect(vm.$el.children[0].type).toBe('password')
+    vm.$el.children[0].value = 'test'
+    vm.show = true
+    waitForUpdate(() => {
+      expect(vm.$el.children[0]).toBe(node)
+      expect(vm.$el.children[0].value).toBe('test')
+      expect(vm.$el.children[0].type).toBe('text')
+      vm.show = false
+    }).then(() => {
+      expect(vm.$el.children[0]).toBe(node)
+      expect(vm.$el.children[0].value).toBe('test')
+      expect(vm.$el.children[0].type).toBe('password')
+    }).then(done)
+  })
 })