diff --git a/packages/runtime-core/__tests__/componentEmits.spec.ts b/packages/runtime-core/__tests__/componentEmits.spec.ts index 973099545e0..274fdddbc26 100644 --- a/packages/runtime-core/__tests__/componentEmits.spec.ts +++ b/packages/runtime-core/__tests__/componentEmits.spec.ts @@ -1,7 +1,7 @@ // Note: emits and listener fallthrough is tested in // ./rendererAttrsFallthrough.spec.ts. -import { render, defineComponent, h, nodeOps } from '@vue/runtime-test' +import { render, defineComponent, ref, h, nodeOps } from '@vue/runtime-test' import { isEmitListener } from '../src/componentEmits' describe('component: emit', () => { @@ -28,6 +28,49 @@ describe('component: emit', () => { expect(onBaz).toHaveBeenCalled() }) + test('trigger handlers by templateRef added', () => { + const root = nodeOps.createElement('div') + let innerCtx = {} as any + const CustomButton = defineComponent({ + setup(_, ctx) { + innerCtx = ctx + return { + refKey: ref(null) + } + }, + emits: ['click'], + render() { + return h('div') + } + }) + + const button = ref(null) + const Wrapper = defineComponent({ + setup(_, ctx) { + innerCtx = ctx + return { + buttonRef: button + } + }, + render() { + return h(CustomButton, { ref: 'buttonRef' }) + } + }) + + render(h(Wrapper), root) + const clickHandler = jest.fn() + // @ts-ignore + button.value.onClick = clickHandler + innerCtx.emit('click') + expect(clickHandler).toHaveBeenCalled() + + // @ts-ignore + button.value.onclick = clickHandler + expect( + `Attempting to add handler "onclick". but it is neither declared by Component` + ).toHaveBeenWarned() + }) + // for v-model:foo-bar usage in DOM templates test('trigger hyphenated events for update:xxx events', () => { const Foo = defineComponent({ diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index e0e239a7997..d11bf0531d7 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -29,7 +29,7 @@ import { resolveMergedOptions, isInBeforeCreate } from './componentOptions' -import { EmitsOptions, EmitFn } from './componentEmits' +import { EmitsOptions, EmitFn, isEmitListener } from './componentEmits' import { Slots } from './componentSlots' import { currentRenderingInstance, @@ -348,7 +348,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { key: string, value: any ): boolean { - const { data, setupState, ctx } = instance + const { data, setupState, ctx, vnode, emitsOptions } = instance if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { setupState[key] = value } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { @@ -378,6 +378,27 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { }) } else { ctx[key] = value + /** + * #2155: allow add event handler by templateRef + * e.g. + * buttonRef.value.onClick = clickHandler + * listen component emitted click event + */ + if (key.indexOf('on') === 0) { + if (isEmitListener(emitsOptions, key)) { + /* istanbul ignore else */ + if (vnode.props) { + vnode.props[key] = value + } + } else { + __DEV__ && + warn( + `Attempting to add handler "${key}". ` + + `but it is neither declared by Component`, + instance + ) + } + } } } return true