From 6f312d636c3d6049dc9e60007f88ea871b8e8173 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Sep 2017 19:29:34 -0400 Subject: [PATCH] fix(v-model): fix input listener with modifier blocking v-model update fix #6552 --- src/core/vdom/helpers/update-listeners.js | 24 +++++++- .../features/directives/model-text.spec.js | 61 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js index d587eecf38..be44c6a0a2 100644 --- a/src/core/vdom/helpers/update-listeners.js +++ b/src/core/vdom/helpers/update-listeners.js @@ -5,9 +5,11 @@ import { cached, isUndef } from 'shared/util' const normalizeEvent = cached((name: string): { name: string, + plain: boolean, once: boolean, capture: boolean, - passive: boolean + passive: boolean, + handler?: Function } => { const passive = name.charAt(0) === '&' name = passive ? name.slice(1) : name @@ -15,8 +17,10 @@ const normalizeEvent = cached((name: string): { name = once ? name.slice(1) : name const capture = name.charAt(0) === '!' name = capture ? name.slice(1) : name + const plain = !(passive || once || capture) return { name, + plain, once, capture, passive @@ -40,6 +44,11 @@ export function createFnInvoker (fns: Function | Array): Function { return invoker } +// #6552 +function prioritizePlainEvents (a, b) { + return a.plain ? -1 : b.plain ? 1 : 0 +} + export function updateListeners ( on: Object, oldOn: Object, @@ -48,10 +57,13 @@ export function updateListeners ( vm: Component ) { let name, cur, old, event + const toAdd = [] + let hasModifier = false for (name in on) { cur = on[name] old = oldOn[name] event = normalizeEvent(name) + if (!event.plain) hasModifier = true if (isUndef(cur)) { process.env.NODE_ENV !== 'production' && warn( `Invalid handler for event "${event.name}": got ` + String(cur), @@ -61,12 +73,20 @@ export function updateListeners ( if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur) } - add(event.name, cur, event.once, event.capture, event.passive) + event.handler = cur + toAdd.push(event) } else if (cur !== old) { old.fns = cur on[name] = old } } + if (toAdd.length) { + if (hasModifier) toAdd.sort(prioritizePlainEvents) + for (let i = 0; i < toAdd.length; i++) { + const event = toAdd[i] + add(event.name, event.handler, event.once, event.capture, event.passive) + } + } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name) diff --git a/test/unit/features/directives/model-text.spec.js b/test/unit/features/directives/model-text.spec.js index af7cd5d613..7032574ff5 100644 --- a/test/unit/features/directives/model-text.spec.js +++ b/test/unit/features/directives/model-text.spec.js @@ -293,5 +293,66 @@ describe('Directive v-model text', () => { triggerEvent(vm.$el, 'compositionend') expect(spy.calls.count()).toBe(2) }) + + // #4392 + it('should not update value with modifiers when in focus if post-conversion values are the same', done => { + const vm = new Vue({ + data: { + a: 1, + foo: false + }, + template: '
{{ foo }}
' + }).$mount() + + document.body.appendChild(vm.$el) + vm.$refs.input.focus() + vm.$refs.input.value = '1.000' + vm.foo = true + + waitForUpdate(() => { + expect(vm.$refs.input.value).toBe('1.000') + }).then(done) + }) + + // #6552 + // Root cause: input listeners with modifiers are added as a separate + // DOM listener. If a change is triggered inside this listener, an update + // will happen before the second listener is fired! (obviously microtasks + // can be processed in between DOM events on the same element) + // This causes the domProps module to receive state that has not been + // updated by v-model yet (because v-model's listener has not fired yet) + // Solution: make sure to always fire v-model's listener first + it('should not block input when another input listener with modifier is used', done => { + const vm = new Vue({ + data: { + a: 'a', + foo: false + }, + template: ` +
+ {{ a }} +
foo
+
+ `, + methods: { + onInput (e) { + this.foo = true + } + } + }).$mount() + + document.body.appendChild(vm.$el) + vm.$refs.input.focus() + vm.$refs.input.value = 'b' + triggerEvent(vm.$refs.input, 'input') + + // not using wait for update here because there will be two update cycles + // one caused by onInput in the first listener + setTimeout(() => { + expect(vm.a).toBe('b') + expect(vm.$refs.input.value).toBe('b') + done() + }, 16) + }) } })