From 0739f8909a0e56ae0fa760f233dfb8c113c9bde2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 30 Aug 2022 14:07:35 +0800 Subject: [PATCH] fix(custom-element): fix event listeners with capital letter event names on custom elements close https://github.com/vuejs/docs/issues/1708 close https://github.com/vuejs/docs/pull/1890 --- .../transforms/transformElement.spec.ts | 34 ++++++++++++++- .../src/transforms/transformElement.ts | 2 +- packages/compiler-core/src/transforms/vOn.ts | 17 +++++--- .../runtime-core/src/helpers/toHandlers.ts | 11 ++++- packages/runtime-dom/src/modules/events.ts | 3 +- packages/runtime-test/src/patchProp.ts | 2 +- .../vue/__tests__/customElementCasing.spec.ts | 42 +++++++++++++++++++ 7 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 packages/vue/__tests__/customElementCasing.spec.ts diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index bec7e119bd3..43bd2589df1 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -314,6 +314,37 @@ describe('compiler: element transform', () => { ) expect(root.helpers).toContain(MERGE_PROPS) + expect(node.props).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: MERGE_PROPS, + arguments: [ + createObjectMatcher({ + id: 'foo' + }), + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: TO_HANDLERS, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `obj` + }, + `true` + ] + }, + createObjectMatcher({ + class: 'bar' + }) + ] + }) + }) + + test('v-on="obj" on component', () => { + const { root, node } = parseWithElementTransform( + `` + ) + expect(root.helpers).toContain(MERGE_PROPS) + expect(node.props).toMatchObject({ type: NodeTypes.JS_CALL_EXPRESSION, callee: MERGE_PROPS, @@ -358,7 +389,8 @@ describe('compiler: element transform', () => { { type: NodeTypes.SIMPLE_EXPRESSION, content: `handlers` - } + }, + `true` ] }, { diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 2d15227f70f..0eb3bb57628 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -647,7 +647,7 @@ export function buildProps( type: NodeTypes.JS_CALL_EXPRESSION, loc, callee: context.helper(TO_HANDLERS), - arguments: [exp] + arguments: isComponent ? [exp] : [exp, `true`] }) } } else { diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 060a7ef9097..a9dfe77eff7 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -47,12 +47,17 @@ export const transformOn: DirectiveTransform = ( if (rawName.startsWith('vue:')) { rawName = `vnode-${rawName.slice(4)}` } - // for all event listeners, auto convert it to camelCase. See issue #2249 - eventName = createSimpleExpression( - toHandlerKey(camelize(rawName)), - true, - arg.loc - ) + const eventString = + node.tagType === ElementTypes.COMPONENT || + rawName.startsWith('vnode') || + !/[A-Z]/.test(rawName) + ? // for component and vnode lifecycle event listeners, auto convert + // it to camelCase. See issue #2249 + toHandlerKey(camelize(rawName)) + // preserve case for plain element listeners that have uppercase + // letters, as these may be custom elements' custom events + : `on:${rawName}` + eventName = createSimpleExpression(eventString, true, arg.loc) } else { // #2388 eventName = createCompoundExpression([ diff --git a/packages/runtime-core/src/helpers/toHandlers.ts b/packages/runtime-core/src/helpers/toHandlers.ts index d366a9b76c9..78ad164d7c0 100644 --- a/packages/runtime-core/src/helpers/toHandlers.ts +++ b/packages/runtime-core/src/helpers/toHandlers.ts @@ -5,14 +5,21 @@ import { warn } from '../warning' * For prefixing keys in v-on="obj" with "on" * @private */ -export function toHandlers(obj: Record): Record { +export function toHandlers( + obj: Record, + preserveCaseIfNecessary?: boolean +): Record { const ret: Record = {} if (__DEV__ && !isObject(obj)) { warn(`v-on with no argument expects an object value.`) return ret } for (const key in obj) { - ret[toHandlerKey(key)] = obj[key] + ret[ + preserveCaseIfNecessary && /[A-Z]/.test(key) + ? `on:${key}` + : toHandlerKey(key) + ] = obj[key] } return ret } diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index bd2279cf5f2..d0f8d364a29 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -101,7 +101,8 @@ function parseName(name: string): [string, EventListenerOptions | undefined] { ;(options as any)[m[0].toLowerCase()] = true } } - return [hyphenate(name.slice(2)), options] + const event = name[2] === ':' ? name.slice(3) : hyphenate(name.slice(2)) + return [event, options] } function createInvoker( diff --git a/packages/runtime-test/src/patchProp.ts b/packages/runtime-test/src/patchProp.ts index 9c4bfca583f..3ebc64d1b04 100644 --- a/packages/runtime-test/src/patchProp.ts +++ b/packages/runtime-test/src/patchProp.ts @@ -16,7 +16,7 @@ export function patchProp( }) el.props[key] = nextValue if (isOn(key)) { - const event = key.slice(2).toLowerCase() + const event = key[2] === ':' ? key.slice(3) : key.slice(2).toLowerCase() ;(el.eventListeners || (el.eventListeners = {}))[event] = nextValue } } diff --git a/packages/vue/__tests__/customElementCasing.spec.ts b/packages/vue/__tests__/customElementCasing.spec.ts new file mode 100644 index 00000000000..90e4453bcd7 --- /dev/null +++ b/packages/vue/__tests__/customElementCasing.spec.ts @@ -0,0 +1,42 @@ +import { createApp } from '../src' + +// https://github.com/vuejs/docs/pull/1890 +// https://github.com/vuejs/core/issues/5401 +// https://github.com/vuejs/docs/issues/1708 +test('custom element event casing', () => { + customElements.define( + 'custom-event-casing', + class Foo extends HTMLElement { + connectedCallback() { + this.dispatchEvent(new Event('camelCase')) + this.dispatchEvent(new Event('CAPScase')) + this.dispatchEvent(new Event('PascalCase')) + } + } + ) + + const container = document.createElement('div') + document.body.appendChild(container) + + const handler = jest.fn() + const handler2 = jest.fn() + createApp({ + template: ` + `, + methods: { + handler, + handler2 + } + }).mount(container) + + expect(handler).toHaveBeenCalledTimes(3) + expect(handler2).toHaveBeenCalledTimes(3) +})