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:
5
.changeset/neat-days-learn.md
Normal file
5
.changeset/neat-days-learn.md
Normal file
@@ -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 modelEdges = useVModel(props, 'edges', emit)
|
||||
|
||||
const instance = useVueFlow(props)
|
||||
const vfInstance = useVueFlow(props)
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -72,12 +72,9 @@ useStylesLoadedWarning()
|
||||
// as that would require a lot of boilerplate and causes significant performance drops
|
||||
provide(Slots, slots)
|
||||
|
||||
onUnmounted(() => {
|
||||
// clean up watcher scope
|
||||
dispose()
|
||||
})
|
||||
onUnmounted(disposeWatchers)
|
||||
|
||||
defineExpose<VueFlowStore>(instance)
|
||||
defineExpose<VueFlowStore>(vfInstance)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -88,7 +85,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :ref="instance.vueFlowRef" class="vue-flow">
|
||||
<div :ref="vfInstance.vueFlowRef" class="vue-flow">
|
||||
<Viewport>
|
||||
<EdgeRenderer />
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { tryOnScopeDispose } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { onBeforeMount } from 'vue'
|
||||
import type { FlowHooks } from '../types'
|
||||
import { getCurrentInstance, onBeforeMount } from 'vue'
|
||||
import type { FlowEvents, FlowHooks } from '../types'
|
||||
import { createExtendedEventHook, warn } from '../utils'
|
||||
|
||||
// flow event hooks
|
||||
export function createHooks(): FlowHooks {
|
||||
return {
|
||||
edgesChange: createExtendedEventHook(),
|
||||
@@ -64,18 +63,40 @@ export function createHooks(): FlowHooks {
|
||||
}
|
||||
|
||||
export function useHooks(emit: (...args: any[]) => void, hooks: Ref<FlowHooks>) {
|
||||
const inst = getCurrentInstance()
|
||||
|
||||
onBeforeMount(() => {
|
||||
for (const [key, value] of Object.entries(hooks.value)) {
|
||||
const listener = (data: any) => {
|
||||
const listener = (data: unknown) => {
|
||||
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
|
||||
value.fns.add(listener)
|
||||
// push into fns instead of using `on` to avoid overwriting default handlers - the emitter should be called in addition to the default handlers
|
||||
value.setEmitter(listener)
|
||||
tryOnScopeDispose(value.removeEmitter)
|
||||
|
||||
tryOnScopeDispose(() => {
|
||||
value.off(listener)
|
||||
})
|
||||
value.setHasEmitListeners(() => hasVNodeListener(key as keyof FlowEvents))
|
||||
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 { 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> {
|
||||
/** true if any user listeners are registered (emitter ignored) */
|
||||
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> {
|
||||
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
|
||||
|
||||
if (defaultHandler) {
|
||||
hasDefaultHandler = true
|
||||
fns.add(defaultHandler)
|
||||
const setEmitter = (fn: Handler) => {
|
||||
emitter = fn
|
||||
}
|
||||
|
||||
const off = (fn: (param: T) => void) => {
|
||||
fns.delete(fn)
|
||||
const removeEmitter = () => {
|
||||
emitter = noop
|
||||
}
|
||||
|
||||
const on = (fn: (param: T) => void) => {
|
||||
if (defaultHandler && hasDefaultHandler) {
|
||||
fns.delete(defaultHandler)
|
||||
}
|
||||
const setHasEmitListeners = (fn: () => boolean) => {
|
||||
hasEmitListeners = fn
|
||||
}
|
||||
|
||||
fns.add(fn)
|
||||
const removeHasEmitListeners = () => {
|
||||
hasEmitListeners = () => false
|
||||
}
|
||||
|
||||
const offFn = () => {
|
||||
off(fn)
|
||||
const off = (fn: Handler) => {
|
||||
listeners.delete(fn)
|
||||
}
|
||||
|
||||
if (defaultHandler && hasDefaultHandler) {
|
||||
fns.add(defaultHandler)
|
||||
}
|
||||
}
|
||||
const on = (fn: Handler) => {
|
||||
listeners.add(fn)
|
||||
|
||||
const offFn = () => off(fn)
|
||||
tryOnScopeDispose(offFn)
|
||||
|
||||
return {
|
||||
off: offFn,
|
||||
}
|
||||
return { 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) => {
|
||||
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 {
|
||||
@@ -58,6 +80,10 @@ export function createExtendedEventHook<T = any>(defaultHandler?: (param: T) =>
|
||||
off,
|
||||
trigger,
|
||||
hasListeners,
|
||||
fns,
|
||||
listeners,
|
||||
setEmitter,
|
||||
removeEmitter,
|
||||
setHasEmitListeners,
|
||||
removeHasEmitListeners,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user