Skip to content

Commit 380c679

Browse files
committed
fix(v-on): refactor DOM event options modifer handling
fix #1567 Previously multiple `v-on` handlers with different event attach option modifers (`.once`, `.capture` and `.passive`) are generated as an array of objects in the form of `[{ handler, options }]` - however, this makes it pretty complex for `runtime-dom` to properly handle all possible value permutations, as each handler may need to be attached with different options. With this commit, they are now generated as event props with different keys - e.g. `v-on:click.capture` is now generated as a prop named `onClick.capture`. This allows them to be patched as separate props which makes the runtime handling much simpler.
1 parent 9152a89 commit 380c679

File tree

8 files changed

+202
-191
lines changed

8 files changed

+202
-191
lines changed

packages/compiler-core/__tests__/transforms/vOn.spec.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
CompilerOptions,
77
ErrorCodes,
88
NodeTypes,
9-
VNodeCall
9+
VNodeCall,
10+
helperNameMap,
11+
CAPITALIZE
1012
} from '../../src'
1113
import { transformOn } from '../../src/transforms/vOn'
1214
import { transformElement } from '../../src/transforms/transformElement'
@@ -73,7 +75,11 @@ describe('compiler: transform v-on', () => {
7375
{
7476
key: {
7577
type: NodeTypes.COMPOUND_EXPRESSION,
76-
children: [`"on" + (`, { content: `event` }, `)`]
78+
children: [
79+
`"on" + _${helperNameMap[CAPITALIZE]}(`,
80+
{ content: `event` },
81+
`)`
82+
]
7783
},
7884
value: {
7985
type: NodeTypes.SIMPLE_EXPRESSION,
@@ -94,7 +100,11 @@ describe('compiler: transform v-on', () => {
94100
{
95101
key: {
96102
type: NodeTypes.COMPOUND_EXPRESSION,
97-
children: [`"on" + (`, { content: `_ctx.event` }, `)`]
103+
children: [
104+
`"on" + _${helperNameMap[CAPITALIZE]}(`,
105+
{ content: `_ctx.event` },
106+
`)`
107+
]
98108
},
99109
value: {
100110
type: NodeTypes.SIMPLE_EXPRESSION,
@@ -116,7 +126,7 @@ describe('compiler: transform v-on', () => {
116126
key: {
117127
type: NodeTypes.COMPOUND_EXPRESSION,
118128
children: [
119-
`"on" + (`,
129+
`"on" + _${helperNameMap[CAPITALIZE]}(`,
120130
{ content: `_ctx.event` },
121131
`(`,
122132
{ content: `_ctx.foo` },

packages/compiler-dom/__tests__/transforms/vOn.spec.ts

+65-54
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import {
55
ElementNode,
66
ObjectExpression,
77
NodeTypes,
8-
VNodeCall
8+
VNodeCall,
9+
helperNameMap,
10+
CAPITALIZE
911
} from '@vue/compiler-core'
1012
import { transformOn } from '../../src/transforms/vOn'
1113
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers'
1214
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
1315
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
14-
import {
15-
createObjectMatcher,
16-
genFlagText
17-
} from '../../../compiler-core/__tests__/testUtils'
16+
import { genFlagText } from '../../../compiler-core/__tests__/testUtils'
1817
import { PatchFlags } from '@vue/shared'
1918

2019
function parseWithVOn(template: string, options: CompilerOptions = {}) {
@@ -83,42 +82,37 @@ describe('compiler-dom: transform v-on', () => {
8382
})
8483
expect(prop).toMatchObject({
8584
type: NodeTypes.JS_PROPERTY,
86-
value: createObjectMatcher({
87-
handler: {
88-
callee: V_ON_WITH_MODIFIERS,
89-
arguments: [{ content: '_ctx.test' }, '["stop"]']
90-
},
91-
options: createObjectMatcher({
92-
capture: { content: 'true', isStatic: false },
93-
passive: { content: 'true', isStatic: false }
94-
})
95-
})
85+
key: {
86+
content: `onClick.capture.passive`
87+
},
88+
value: {
89+
callee: V_ON_WITH_MODIFIERS,
90+
arguments: [{ content: '_ctx.test' }, '["stop"]']
91+
}
9692
})
9793
})
9894

9995
it('should wrap keys guard for keyboard events or dynamic events', () => {
10096
const {
10197
props: [prop]
102-
} = parseWithVOn(`<div @keyDown.stop.capture.ctrl.a="test"/>`, {
98+
} = parseWithVOn(`<div @keydown.stop.capture.ctrl.a="test"/>`, {
10399
prefixIdentifiers: true
104100
})
105101
expect(prop).toMatchObject({
106102
type: NodeTypes.JS_PROPERTY,
107-
value: createObjectMatcher({
108-
handler: {
109-
callee: V_ON_WITH_KEYS,
110-
arguments: [
111-
{
112-
callee: V_ON_WITH_MODIFIERS,
113-
arguments: [{ content: '_ctx.test' }, '["stop","ctrl"]']
114-
},
115-
'["a"]'
116-
]
117-
},
118-
options: createObjectMatcher({
119-
capture: { content: 'true', isStatic: false }
120-
})
121-
})
103+
key: {
104+
content: `onKeydown.capture`
105+
},
106+
value: {
107+
callee: V_ON_WITH_KEYS,
108+
arguments: [
109+
{
110+
callee: V_ON_WITH_MODIFIERS,
111+
arguments: [{ content: '_ctx.test' }, '["stop","ctrl"]']
112+
},
113+
'["a"]'
114+
]
115+
}
122116
})
123117
})
124118

@@ -206,9 +200,21 @@ describe('compiler-dom: transform v-on', () => {
206200
type: NodeTypes.COMPOUND_EXPRESSION,
207201
children: [
208202
`(`,
209-
{ children: [`"on" + (`, { content: 'event' }, `)`] },
210-
`).toLowerCase() === "onclick" ? "onContextmenu" : (`,
211-
{ children: [`"on" + (`, { content: 'event' }, `)`] },
203+
{
204+
children: [
205+
`"on" + _${helperNameMap[CAPITALIZE]}(`,
206+
{ content: 'event' },
207+
`)`
208+
]
209+
},
210+
`) === "onClick" ? "onContextmenu" : (`,
211+
{
212+
children: [
213+
`"on" + _${helperNameMap[CAPITALIZE]}(`,
214+
{ content: 'event' },
215+
`)`
216+
]
217+
},
212218
`)`
213219
]
214220
})
@@ -232,9 +238,21 @@ describe('compiler-dom: transform v-on', () => {
232238
type: NodeTypes.COMPOUND_EXPRESSION,
233239
children: [
234240
`(`,
235-
{ children: [`"on" + (`, { content: 'event' }, `)`] },
236-
`).toLowerCase() === "onclick" ? "onMouseup" : (`,
237-
{ children: [`"on" + (`, { content: 'event' }, `)`] },
241+
{
242+
children: [
243+
`"on" + _${helperNameMap[CAPITALIZE]}(`,
244+
{ content: 'event' },
245+
`)`
246+
]
247+
},
248+
`) === "onClick" ? "onMouseup" : (`,
249+
{
250+
children: [
251+
`"on" + _${helperNameMap[CAPITALIZE]}(`,
252+
{ content: 'event' },
253+
`)`
254+
]
255+
},
238256
`)`
239257
]
240258
})
@@ -254,24 +272,17 @@ describe('compiler-dom: transform v-on', () => {
254272
expect((root as any).children[0].codegenNode.patchFlag).toBe(
255273
genFlagText(PatchFlags.HYDRATE_EVENTS)
256274
)
257-
expect(prop.value).toMatchObject({
258-
type: NodeTypes.JS_CACHE_EXPRESSION,
259-
index: 1,
275+
expect(prop).toMatchObject({
276+
key: {
277+
content: `onKeyup.capture`
278+
},
260279
value: {
261-
type: NodeTypes.JS_OBJECT_EXPRESSION,
262-
properties: [
263-
{
264-
key: { content: 'handler' },
265-
value: {
266-
type: NodeTypes.JS_CALL_EXPRESSION,
267-
callee: V_ON_WITH_KEYS
268-
}
269-
},
270-
{
271-
key: { content: 'options' },
272-
value: { type: NodeTypes.JS_OBJECT_EXPRESSION }
273-
}
274-
]
280+
type: NodeTypes.JS_CACHE_EXPRESSION,
281+
index: 1,
282+
value: {
283+
type: NodeTypes.JS_CALL_EXPRESSION,
284+
callee: V_ON_WITH_KEYS
285+
}
275286
}
276287
})
277288
})

packages/compiler-dom/src/transforms/vOn.ts

+10-15
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
DirectiveTransform,
44
createObjectProperty,
55
createCallExpression,
6-
createObjectExpression,
76
createSimpleExpression,
87
NodeTypes,
98
createCompoundExpression,
@@ -80,7 +79,7 @@ const transformClick = (key: ExpressionNode, event: string) => {
8079
? createCompoundExpression([
8180
`(`,
8281
key,
83-
`).toLowerCase() === "onclick" ? "${event}" : (`,
82+
`) === "onClick" ? "${event}" : (`,
8483
key,
8584
`)`
8685
])
@@ -126,20 +125,16 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
126125
}
127126

128127
if (eventOptionModifiers.length) {
129-
handlerExp = createObjectExpression([
130-
createObjectProperty('handler', handlerExp),
131-
createObjectProperty(
132-
'options',
133-
createObjectExpression(
134-
eventOptionModifiers.map(modifier =>
135-
createObjectProperty(
136-
modifier,
137-
createSimpleExpression('true', false)
138-
)
139-
)
128+
key = isStaticExp(key)
129+
? createSimpleExpression(
130+
`${key.content}.${eventOptionModifiers.join(`.`)}`,
131+
true
140132
)
141-
)
142-
])
133+
: createCompoundExpression([
134+
`(`,
135+
key,
136+
`) + ".${eventOptionModifiers.join(`.`)}"`
137+
])
143138
}
144139

145140
return {

packages/runtime-core/__tests__/componentEmits.spec.ts

+56-25
Original file line numberDiff line numberDiff line change
@@ -160,30 +160,61 @@ describe('component: emit', () => {
160160
expect(`event validation failed for event "foo"`).toHaveBeenWarned()
161161
})
162162

163-
test('isEmitListener', () => {
164-
const def1 = { emits: ['click'] }
165-
expect(isEmitListener(def1, 'onClick')).toBe(true)
166-
expect(isEmitListener(def1, 'onclick')).toBe(false)
167-
expect(isEmitListener(def1, 'onBlick')).toBe(false)
168-
169-
const def2 = { emits: { click: null } }
170-
expect(isEmitListener(def2, 'onClick')).toBe(true)
171-
expect(isEmitListener(def2, 'onclick')).toBe(false)
172-
expect(isEmitListener(def2, 'onBlick')).toBe(false)
173-
174-
const mixin1 = { emits: ['foo'] }
175-
const mixin2 = { emits: ['bar'] }
176-
const extend = { emits: ['baz'] }
177-
const def3 = {
178-
emits: { click: null },
179-
mixins: [mixin1, mixin2],
180-
extends: extend
181-
}
182-
expect(isEmitListener(def3, 'onClick')).toBe(true)
183-
expect(isEmitListener(def3, 'onFoo')).toBe(true)
184-
expect(isEmitListener(def3, 'onBar')).toBe(true)
185-
expect(isEmitListener(def3, 'onBaz')).toBe(true)
186-
expect(isEmitListener(def3, 'onclick')).toBe(false)
187-
expect(isEmitListener(def3, 'onBlick')).toBe(false)
163+
test('.once', () => {
164+
const Foo = defineComponent({
165+
render() {},
166+
emits: {
167+
foo: null
168+
},
169+
created() {
170+
this.$emit('foo')
171+
this.$emit('foo')
172+
}
173+
})
174+
const fn = jest.fn()
175+
render(
176+
h(Foo, {
177+
'onFoo.once': fn
178+
}),
179+
nodeOps.createElement('div')
180+
)
181+
expect(fn).toHaveBeenCalledTimes(1)
182+
})
183+
184+
describe('isEmitListener', () => {
185+
test('array option', () => {
186+
const def1 = { emits: ['click'] }
187+
expect(isEmitListener(def1, 'onClick')).toBe(true)
188+
expect(isEmitListener(def1, 'onclick')).toBe(false)
189+
expect(isEmitListener(def1, 'onBlick')).toBe(false)
190+
})
191+
192+
test('object option', () => {
193+
const def2 = { emits: { click: null } }
194+
expect(isEmitListener(def2, 'onClick')).toBe(true)
195+
expect(isEmitListener(def2, 'onclick')).toBe(false)
196+
expect(isEmitListener(def2, 'onBlick')).toBe(false)
197+
})
198+
199+
test('with mixins and extends', () => {
200+
const mixin1 = { emits: ['foo'] }
201+
const mixin2 = { emits: ['bar'] }
202+
const extend = { emits: ['baz'] }
203+
const def3 = {
204+
mixins: [mixin1, mixin2],
205+
extends: extend
206+
}
207+
expect(isEmitListener(def3, 'onFoo')).toBe(true)
208+
expect(isEmitListener(def3, 'onBar')).toBe(true)
209+
expect(isEmitListener(def3, 'onBaz')).toBe(true)
210+
expect(isEmitListener(def3, 'onclick')).toBe(false)
211+
expect(isEmitListener(def3, 'onBlick')).toBe(false)
212+
})
213+
214+
test('.once listeners', () => {
215+
const def2 = { emits: { click: null } }
216+
expect(isEmitListener(def2, 'onClick.once')).toBe(true)
217+
expect(isEmitListener(def2, 'onclick.once')).toBe(false)
218+
})
188219
})
189220
})

packages/runtime-core/src/component.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ export interface ComponentInternalInstance {
246246
slots: InternalSlots
247247
refs: Data
248248
emit: EmitFn
249+
// used for keeping track of .once event handlers on components
250+
emitted: Record<string, boolean> | null
249251

250252
/**
251253
* setup related
@@ -396,7 +398,8 @@ export function createComponentInstance(
396398
rtg: null,
397399
rtc: null,
398400
ec: null,
399-
emit: null as any // to be set immediately
401+
emit: null as any, // to be set immediately
402+
emitted: null
400403
}
401404
if (__DEV__) {
402405
instance.ctx = createRenderContext(instance)

0 commit comments

Comments
 (0)