Skip to content

Commit ca54587

Browse files
authored
fix(core): correctly check if an event has any listeners bound to it (#1935)
* fix(core): separate listeners from emitter for correct check on hasListeners Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * fix(core): check for listeners bound via emits Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * chore(changeset): add --------- Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
1 parent 3e0442a commit ca54587

File tree

4 files changed

+98
-49
lines changed

4 files changed

+98
-49
lines changed

.changeset/neat-days-learn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vue-flow/core": patch
3+
---
4+
5+
Correctly check if an event listener was bound to the VueFlow component, using for example `@node-click` or if a listener was bound using the exposed event hooks from `useVueFlow` when determening if a listener for an event exists at all.

packages/core/src/container/VueFlow/VueFlow.vue

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ const modelValue = useVModel(props, 'modelValue', emit)
5656
const modelNodes = useVModel(props, 'nodes', emit)
5757
const modelEdges = useVModel(props, 'edges', emit)
5858
59-
const instance = useVueFlow(props)
59+
const vfInstance = useVueFlow(props)
6060
6161
// watch props and update store state
62-
const dispose = useWatchProps({ modelValue, nodes: modelNodes, edges: modelEdges }, props, instance)
62+
const disposeWatchers = useWatchProps({ modelValue, nodes: modelNodes, edges: modelEdges }, props, vfInstance)
6363
64-
useHooks(emit, instance.hooks)
64+
useHooks(emit, vfInstance.hooks)
6565
6666
useOnInitHandler()
6767
@@ -72,12 +72,9 @@ useStylesLoadedWarning()
7272
// as that would require a lot of boilerplate and causes significant performance drops
7373
provide(Slots, slots)
7474
75-
onUnmounted(() => {
76-
// clean up watcher scope
77-
dispose()
78-
})
75+
onUnmounted(disposeWatchers)
7976
80-
defineExpose<VueFlowStore>(instance)
77+
defineExpose<VueFlowStore>(vfInstance)
8178
</script>
8279

8380
<script lang="ts">
@@ -88,7 +85,7 @@ export default {
8885
</script>
8986

9087
<template>
91-
<div :ref="instance.vueFlowRef" class="vue-flow">
88+
<div :ref="vfInstance.vueFlowRef" class="vue-flow">
9289
<Viewport>
9390
<EdgeRenderer />
9491

packages/core/src/store/hooks.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { tryOnScopeDispose } from '@vueuse/core'
22
import type { Ref } from 'vue'
3-
import { onBeforeMount } from 'vue'
4-
import type { FlowHooks } from '../types'
3+
import { getCurrentInstance, onBeforeMount } from 'vue'
4+
import type { FlowEvents, FlowHooks } from '../types'
55
import { createExtendedEventHook, warn } from '../utils'
66

7-
// flow event hooks
87
export function createHooks(): FlowHooks {
98
return {
109
edgesChange: createExtendedEventHook(),
@@ -64,18 +63,40 @@ export function createHooks(): FlowHooks {
6463
}
6564

6665
export function useHooks(emit: (...args: any[]) => void, hooks: Ref<FlowHooks>) {
66+
const inst = getCurrentInstance()
67+
6768
onBeforeMount(() => {
6869
for (const [key, value] of Object.entries(hooks.value)) {
69-
const listener = (data: any) => {
70+
const listener = (data: unknown) => {
7071
emit(key, data)
7172
}
7273

73-
// push into fns instead of using `on` to avoid overwriting default handlers - the emits should be called in addition to the default handlers
74-
value.fns.add(listener)
74+
// push into fns instead of using `on` to avoid overwriting default handlers - the emitter should be called in addition to the default handlers
75+
value.setEmitter(listener)
76+
tryOnScopeDispose(value.removeEmitter)
7577

76-
tryOnScopeDispose(() => {
77-
value.off(listener)
78-
})
78+
value.setHasEmitListeners(() => hasVNodeListener(key as keyof FlowEvents))
79+
tryOnScopeDispose(value.removeHasEmitListeners)
7980
}
8081
})
82+
83+
function hasVNodeListener(event: keyof FlowEvents) {
84+
const key = toHandlerKey(event)
85+
// listeners live on vnode.props; value can be a Function or an array of Functions
86+
const h = inst?.vnode.props?.[key]
87+
return !!h
88+
}
89+
}
90+
91+
/**
92+
* Converts an event name to the corresponding handler key.
93+
* E.g. 'nodeClick' -> 'onNodeClick'
94+
*
95+
* @param event The event name to convert.
96+
* @returns The corresponding handler key.
97+
*/
98+
function toHandlerKey(event: string) {
99+
const [head, ...rest] = event.split(':')
100+
const camel = head.replace(/(?:^|-)(\w)/g, (_, c: string) => c.toUpperCase())
101+
return `on${camel}${rest.length ? `:${rest.join(':')}` : ''}`
81102
}
Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,89 @@
11
import type { EventHook } from '@vueuse/core'
22
import { tryOnScopeDispose } from '@vueuse/core'
33

4-
/**
5-
* Source code taken from https://github.com/vueuse/vueuse/blob/main/packages/shared/createEventHook/index.ts
6-
*
7-
* Modified to be able to check if there are any event listeners
8-
*/
94
export interface EventHookExtended<T> extends EventHook<T> {
5+
/** true if any user listeners are registered (emitter ignored) */
106
hasListeners: () => boolean
11-
fns: Set<(param: T) => void>
7+
/** current user listeners (read-only; do not mutate externally) */
8+
listeners: ReadonlySet<(param: T) => void>
9+
/** wire a single external emitter (e.g., for `emit`) */
10+
setEmitter: (fn: (param: T) => void) => void
11+
/** remove the external emitter */
12+
removeEmitter: () => void
13+
/** wire a function to detect if any emit listeners exist (e.g., for `$listeners` in Vue 2) */
14+
setHasEmitListeners: (fn: () => boolean) => void
15+
/** remove the emit listeners detector */
16+
removeHasEmitListeners: () => void
1217
}
1318

14-
export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) => void): EventHookExtended<T> {
15-
const fns = new Set<(param: T) => void>()
19+
type Handler<T = any> = (param: T) => any | Promise<any>
20+
21+
const noop: Handler = () => {}
1622

17-
let hasDefaultHandler = false
23+
export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) => void): EventHookExtended<T> {
24+
const listeners = new Set<Handler>()
25+
let emitter: Handler = noop
26+
let hasEmitListeners = () => false
1827

19-
const hasListeners = () => fns.size > 0
28+
const hasListeners = () => listeners.size > 0 || hasEmitListeners()
2029

21-
if (defaultHandler) {
22-
hasDefaultHandler = true
23-
fns.add(defaultHandler)
30+
const setEmitter = (fn: Handler) => {
31+
emitter = fn
2432
}
2533

26-
const off = (fn: (param: T) => void) => {
27-
fns.delete(fn)
34+
const removeEmitter = () => {
35+
emitter = noop
2836
}
2937

30-
const on = (fn: (param: T) => void) => {
31-
if (defaultHandler && hasDefaultHandler) {
32-
fns.delete(defaultHandler)
33-
}
38+
const setHasEmitListeners = (fn: () => boolean) => {
39+
hasEmitListeners = fn
40+
}
3441

35-
fns.add(fn)
42+
const removeHasEmitListeners = () => {
43+
hasEmitListeners = () => false
44+
}
3645

37-
const offFn = () => {
38-
off(fn)
46+
const off = (fn: Handler) => {
47+
listeners.delete(fn)
48+
}
3949

40-
if (defaultHandler && hasDefaultHandler) {
41-
fns.add(defaultHandler)
42-
}
43-
}
50+
const on = (fn: Handler) => {
51+
listeners.add(fn)
4452

53+
const offFn = () => off(fn)
4554
tryOnScopeDispose(offFn)
4655

47-
return {
48-
off: offFn,
49-
}
56+
return { off: offFn }
5057
}
5158

59+
/**
60+
* Trigger order:
61+
* 1) If any user listeners OR an emitter exist -> call all of those (defaultHandler is skipped)
62+
* 2) Else (no listeners and no emitter) -> call defaultHandler (if provided)
63+
*
64+
* Errors are isolated via allSettled so one failing handler doesn't break others.
65+
*/
5266
const trigger = (param: T) => {
53-
return Promise.all(Array.from(fns).map((fn) => fn(param)))
67+
const queue: Handler[] = [emitter]
68+
69+
if (hasListeners()) {
70+
queue.push(...listeners)
71+
} else if (defaultHandler) {
72+
queue.push(defaultHandler)
73+
}
74+
75+
return Promise.allSettled(queue.map((fn) => fn(param)))
5476
}
5577

5678
return {
5779
on,
5880
off,
5981
trigger,
6082
hasListeners,
61-
fns,
83+
listeners,
84+
setEmitter,
85+
removeEmitter,
86+
setHasEmitListeners,
87+
removeHasEmitListeners,
6288
}
6389
}

0 commit comments

Comments
 (0)