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>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@vue-flow/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -56,12 +56,12 @@ const modelValue = useVModel(props, 'modelValue', emit)
|
|||||||
const modelNodes = useVModel(props, 'nodes', emit)
|
const modelNodes = useVModel(props, 'nodes', emit)
|
||||||
const modelEdges = useVModel(props, 'edges', emit)
|
const modelEdges = useVModel(props, 'edges', emit)
|
||||||
|
|
||||||
const instance = useVueFlow(props)
|
const vfInstance = useVueFlow(props)
|
||||||
|
|
||||||
// watch props and update store state
|
// watch props and update store state
|
||||||
const dispose = useWatchProps({ modelValue, nodes: modelNodes, edges: modelEdges }, props, instance)
|
const disposeWatchers = useWatchProps({ modelValue, nodes: modelNodes, edges: modelEdges }, props, vfInstance)
|
||||||
|
|
||||||
useHooks(emit, instance.hooks)
|
useHooks(emit, vfInstance.hooks)
|
||||||
|
|
||||||
useOnInitHandler()
|
useOnInitHandler()
|
||||||
|
|
||||||
@@ -72,12 +72,9 @@ useStylesLoadedWarning()
|
|||||||
// as that would require a lot of boilerplate and causes significant performance drops
|
// as that would require a lot of boilerplate and causes significant performance drops
|
||||||
provide(Slots, slots)
|
provide(Slots, slots)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(disposeWatchers)
|
||||||
// clean up watcher scope
|
|
||||||
dispose()
|
|
||||||
})
|
|
||||||
|
|
||||||
defineExpose<VueFlowStore>(instance)
|
defineExpose<VueFlowStore>(vfInstance)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -88,7 +85,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :ref="instance.vueFlowRef" class="vue-flow">
|
<div :ref="vfInstance.vueFlowRef" class="vue-flow">
|
||||||
<Viewport>
|
<Viewport>
|
||||||
<EdgeRenderer />
|
<EdgeRenderer />
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { tryOnScopeDispose } from '@vueuse/core'
|
import { tryOnScopeDispose } from '@vueuse/core'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { onBeforeMount } from 'vue'
|
import { getCurrentInstance, onBeforeMount } from 'vue'
|
||||||
import type { FlowHooks } from '../types'
|
import type { FlowEvents, FlowHooks } from '../types'
|
||||||
import { createExtendedEventHook, warn } from '../utils'
|
import { createExtendedEventHook, warn } from '../utils'
|
||||||
|
|
||||||
// flow event hooks
|
|
||||||
export function createHooks(): FlowHooks {
|
export function createHooks(): FlowHooks {
|
||||||
return {
|
return {
|
||||||
edgesChange: createExtendedEventHook(),
|
edgesChange: createExtendedEventHook(),
|
||||||
@@ -64,18 +63,40 @@ export function createHooks(): FlowHooks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useHooks(emit: (...args: any[]) => void, hooks: Ref<FlowHooks>) {
|
export function useHooks(emit: (...args: any[]) => void, hooks: Ref<FlowHooks>) {
|
||||||
|
const inst = getCurrentInstance()
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
for (const [key, value] of Object.entries(hooks.value)) {
|
for (const [key, value] of Object.entries(hooks.value)) {
|
||||||
const listener = (data: any) => {
|
const listener = (data: unknown) => {
|
||||||
emit(key, data)
|
emit(key, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// push into fns instead of using `on` to avoid overwriting default handlers - the emits should be called in addition to the default handlers
|
// push into fns instead of using `on` to avoid overwriting default handlers - the emitter should be called in addition to the default handlers
|
||||||
value.fns.add(listener)
|
value.setEmitter(listener)
|
||||||
|
tryOnScopeDispose(value.removeEmitter)
|
||||||
|
|
||||||
tryOnScopeDispose(() => {
|
value.setHasEmitListeners(() => hasVNodeListener(key as keyof FlowEvents))
|
||||||
value.off(listener)
|
tryOnScopeDispose(value.removeHasEmitListeners)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function hasVNodeListener(event: keyof FlowEvents) {
|
||||||
|
const key = toHandlerKey(event)
|
||||||
|
// listeners live on vnode.props; value can be a Function or an array of Functions
|
||||||
|
const h = inst?.vnode.props?.[key]
|
||||||
|
return !!h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an event name to the corresponding handler key.
|
||||||
|
* E.g. 'nodeClick' -> 'onNodeClick'
|
||||||
|
*
|
||||||
|
* @param event The event name to convert.
|
||||||
|
* @returns The corresponding handler key.
|
||||||
|
*/
|
||||||
|
function toHandlerKey(event: string) {
|
||||||
|
const [head, ...rest] = event.split(':')
|
||||||
|
const camel = head.replace(/(?:^|-)(\w)/g, (_, c: string) => c.toUpperCase())
|
||||||
|
return `on${camel}${rest.length ? `:${rest.join(':')}` : ''}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,78 @@
|
|||||||
import type { EventHook } from '@vueuse/core'
|
import type { EventHook } from '@vueuse/core'
|
||||||
import { tryOnScopeDispose } from '@vueuse/core'
|
import { tryOnScopeDispose } from '@vueuse/core'
|
||||||
|
|
||||||
/**
|
|
||||||
* Source code taken from https://github.com/vueuse/vueuse/blob/main/packages/shared/createEventHook/index.ts
|
|
||||||
*
|
|
||||||
* Modified to be able to check if there are any event listeners
|
|
||||||
*/
|
|
||||||
export interface EventHookExtended<T> extends EventHook<T> {
|
export interface EventHookExtended<T> extends EventHook<T> {
|
||||||
|
/** true if any user listeners are registered (emitter ignored) */
|
||||||
hasListeners: () => boolean
|
hasListeners: () => boolean
|
||||||
fns: Set<(param: T) => void>
|
/** current user listeners (read-only; do not mutate externally) */
|
||||||
|
listeners: ReadonlySet<(param: T) => void>
|
||||||
|
/** wire a single external emitter (e.g., for `emit`) */
|
||||||
|
setEmitter: (fn: (param: T) => void) => void
|
||||||
|
/** remove the external emitter */
|
||||||
|
removeEmitter: () => void
|
||||||
|
/** wire a function to detect if any emit listeners exist (e.g., for `$listeners` in Vue 2) */
|
||||||
|
setHasEmitListeners: (fn: () => boolean) => void
|
||||||
|
/** remove the emit listeners detector */
|
||||||
|
removeHasEmitListeners: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Handler<T = any> = (param: T) => any | Promise<any>
|
||||||
|
|
||||||
|
const noop: Handler = () => {}
|
||||||
|
|
||||||
export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) => void): EventHookExtended<T> {
|
export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) => void): EventHookExtended<T> {
|
||||||
const fns = new Set<(param: T) => void>()
|
const listeners = new Set<Handler>()
|
||||||
|
let emitter: Handler = noop
|
||||||
|
let hasEmitListeners = () => false
|
||||||
|
|
||||||
let hasDefaultHandler = false
|
const hasListeners = () => listeners.size > 0 || hasEmitListeners()
|
||||||
|
|
||||||
const hasListeners = () => fns.size > 0
|
const setEmitter = (fn: Handler) => {
|
||||||
|
emitter = fn
|
||||||
if (defaultHandler) {
|
|
||||||
hasDefaultHandler = true
|
|
||||||
fns.add(defaultHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const off = (fn: (param: T) => void) => {
|
const removeEmitter = () => {
|
||||||
fns.delete(fn)
|
emitter = noop
|
||||||
}
|
}
|
||||||
|
|
||||||
const on = (fn: (param: T) => void) => {
|
const setHasEmitListeners = (fn: () => boolean) => {
|
||||||
if (defaultHandler && hasDefaultHandler) {
|
hasEmitListeners = fn
|
||||||
fns.delete(defaultHandler)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fns.add(fn)
|
const removeHasEmitListeners = () => {
|
||||||
|
hasEmitListeners = () => false
|
||||||
|
}
|
||||||
|
|
||||||
const offFn = () => {
|
const off = (fn: Handler) => {
|
||||||
off(fn)
|
listeners.delete(fn)
|
||||||
|
}
|
||||||
|
|
||||||
if (defaultHandler && hasDefaultHandler) {
|
const on = (fn: Handler) => {
|
||||||
fns.add(defaultHandler)
|
listeners.add(fn)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const offFn = () => off(fn)
|
||||||
tryOnScopeDispose(offFn)
|
tryOnScopeDispose(offFn)
|
||||||
|
|
||||||
return {
|
return { off: offFn }
|
||||||
off: offFn,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger order:
|
||||||
|
* 1) If any user listeners OR an emitter exist -> call all of those (defaultHandler is skipped)
|
||||||
|
* 2) Else (no listeners and no emitter) -> call defaultHandler (if provided)
|
||||||
|
*
|
||||||
|
* Errors are isolated via allSettled so one failing handler doesn't break others.
|
||||||
|
*/
|
||||||
const trigger = (param: T) => {
|
const trigger = (param: T) => {
|
||||||
return Promise.all(Array.from(fns).map((fn) => fn(param)))
|
const queue: Handler[] = [emitter]
|
||||||
|
|
||||||
|
if (hasListeners()) {
|
||||||
|
queue.push(...listeners)
|
||||||
|
} else if (defaultHandler) {
|
||||||
|
queue.push(defaultHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.allSettled(queue.map((fn) => fn(param)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -58,6 +80,10 @@ export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) =>
|
|||||||
off,
|
off,
|
||||||
trigger,
|
trigger,
|
||||||
hasListeners,
|
hasListeners,
|
||||||
fns,
|
listeners,
|
||||||
|
setEmitter,
|
||||||
|
removeEmitter,
|
||||||
|
setHasEmitListeners,
|
||||||
|
removeHasEmitListeners,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user