Skip to content

Commit

Permalink
feat(setup): support listeners on setup context + useListeners() he…
Browse files Browse the repository at this point in the history
…lper

These are added because Vue 2 does not include listeners in
`context.attrs` so there is no way to access the equivalent of
`this.$listeners` in `setup()`.
  • Loading branch information
yyx990803 committed Jul 20, 2022
1 parent 135d074 commit adf3ac8
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 32 deletions.
28 changes: 18 additions & 10 deletions src/core/instance/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
invokeWithErrorHandling
} from '../util/index'
import { currentInstance, setCurrentInstance } from 'v3/currentInstance'
import { syncSetupAttrs } from 'v3/apiSetup'
import { syncSetupProxy } from 'v3/apiSetup'

export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false
Expand Down Expand Up @@ -288,19 +288,33 @@ export function updateChildComponent(
// force update if attrs are accessed and has changed since it may be
// passed to a child component.
if (
syncSetupAttrs(
syncSetupProxy(
vm._attrsProxy,
attrs,
(prevVNode.data && prevVNode.data.attrs) || emptyObject,
vm
vm,
'$attrs'
)
) {
needsForceUpdate = true
}
}
vm.$attrs = attrs

vm.$listeners = listeners || emptyObject
// update listeners
listeners = listeners || emptyObject
const prevListeners = vm.$options._parentListeners
if (vm._listenersProxy) {
syncSetupProxy(
vm._listenersProxy,
listeners,
prevListeners || emptyObject,
vm,
'$listeners'
)
}
vm.$listeners = vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, prevListeners)

// update props
if (propsData && vm.$options.props) {
Expand All @@ -317,12 +331,6 @@ export function updateChildComponent(
vm.$options.propsData = propsData
}

// update listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)

// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
Expand Down
1 change: 1 addition & 0 deletions src/types/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export declare class Component {
_setupProxy?: Record<string, any>
_setupContext?: SetupContext
_attrsProxy?: Record<string, any>
_listenersProxy?: Record<string, Function | Function[]>
_slotsProxy?: Record<string, () => VNode[]>
_preWatchers?: Watcher[]

Expand Down
52 changes: 33 additions & 19 deletions src/v3/apiSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { proxyWithRefUnwrap } from './reactivity/ref'
*/
export interface SetupContext {
attrs: Record<string, any>
listeners: Record<string, Function | Function[]>
slots: Record<string, () => VNode[]>
emit: (event: string, ...args: any[]) => any
expose: (exposed: Record<string, any>) => void
Expand Down Expand Up @@ -87,7 +88,19 @@ function createSetupContext(vm: Component): SetupContext {
let exposeCalled = false
return {
get attrs() {
return initAttrsProxy(vm)
if (!vm._attrsProxy) {
const proxy = (vm._attrsProxy = {})
def(proxy, '_v_attr_proxy', true)
syncSetupProxy(proxy, vm.$attrs, emptyObject, vm, '$attrs')
}
return vm._attrsProxy
},
get listeners() {
if (!vm._listenersProxy) {
const proxy = (vm._listenersProxy = {})
syncSetupProxy(proxy, vm.$listeners, emptyObject, vm, '$listeners')
}
return vm._listenersProxy
},
get slots() {
return initSlotsProxy(vm)
Expand All @@ -109,26 +122,18 @@ function createSetupContext(vm: Component): SetupContext {
}
}

function initAttrsProxy(vm: Component) {
if (!vm._attrsProxy) {
const proxy = (vm._attrsProxy = {})
def(proxy, '_v_attr_proxy', true)
syncSetupAttrs(proxy, vm.$attrs, emptyObject, vm)
}
return vm._attrsProxy
}

export function syncSetupAttrs(
export function syncSetupProxy(
to: any,
from: any,
prev: any,
instance: Component
instance: Component,
type: string
) {
let changed = false
for (const key in from) {
if (!(key in to)) {
changed = true
defineProxyAttr(to, key, instance)
defineProxyAttr(to, key, instance, type)
} else if (from[key] !== prev[key]) {
changed = true
}
Expand All @@ -142,12 +147,17 @@ export function syncSetupAttrs(
return changed
}

function defineProxyAttr(proxy: any, key: string, instance: Component) {
function defineProxyAttr(
proxy: any,
key: string,
instance: Component,
type: string
) {
Object.defineProperty(proxy, key, {
enumerable: true,
configurable: true,
get() {
return instance.$attrs[key]
return instance[type][key]
}
})
}
Expand All @@ -171,19 +181,23 @@ export function syncSetupSlots(to: any, from: any) {
}

/**
* @internal use manual type def
* @internal use manual type def because it relies on legacy VNode types
*/
export function useSlots(): SetupContext['slots'] {
return getContext().slots
}

/**
* @internal use manual type def
*/
export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
}

/**
* Vue 2 only
*/
export function useListeners(): SetupContext['listeners'] {
return getContext().listeners
}

function getContext(): SetupContext {
if (__DEV__ && !currentInstance) {
warn(`useContext() called without active instance.`)
Expand Down
2 changes: 1 addition & 1 deletion src/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export { provide, inject, InjectionKey } from './apiInject'

export { h } from './h'
export { getCurrentInstance } from './currentInstance'
export { useSlots, useAttrs, mergeDefaults } from './apiSetup'
export { useSlots, useAttrs, useListeners, mergeDefaults } from './apiSetup'
export { nextTick } from 'core/util/next-tick'
export { set, del } from 'core/observer'

Expand Down
23 changes: 23 additions & 0 deletions test/unit/features/v3/apiSetup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,27 @@ describe('api: setup context', () => {
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
})

it('context.listeners', async () => {
let _listeners
const Child = {
setup(_, { listeners }) {
_listeners = listeners
return () => {}
}
}

const Parent = {
data: () => ({ log: () => 1 }),
template: `<Child @foo="log" />`,
components: { Child }
}

const vm = new Vue(Parent).$mount()

expect(_listeners.foo()).toBe(1)
vm.log = () => 2
await nextTick()
expect(_listeners.foo()).toBe(2)
})
})
2 changes: 0 additions & 2 deletions types/v3-manual-apis.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@ export function getCurrentInstance(): { proxy: Vue } | null

export const h: CreateElement

export function useAttrs(): SetupContext['attrs']

export function useSlots(): SetupContext['slots']
4 changes: 4 additions & 0 deletions types/v3-setup-context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export type EmitFn<

export interface SetupContext<E extends EmitsOptions = {}> {
attrs: Data
/**
* Equivalent of `this.$listeners`, which is Vue 2 only.
*/
listeners: Record<string, Function | Function[]>
slots: Slots
emit: EmitFn<E>
expose(exposed?: Record<string, any>): void
Expand Down

0 comments on commit adf3ac8

Please sign in to comment.