Skip to content

feat: add support for nesting vue flows #1895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/vite/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ export const routes: RouterOptions['routes'] = [
path: '/confirm-delete',
component: () => import('./src/ConfirmDelete/ConfirmDeleteExample.vue'),
},
{
path: '/nesting-flows',
component: () => import('./src/NestingFlows/NestingFlows.vue'),
},
{
path: '/recursive-nesting-flows',
component: () => import('./src/RecursiveNestingFlows/RecursiveNestingFlows.vue'),
},
]

export const router = createRouter({
Expand Down
55 changes: 55 additions & 0 deletions examples/vite/src/NestingFlows/NestedFlow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script lang="ts" setup>
import { VueFlow, useVueFlow } from '@vue-flow/core'
import type { Elements } from '@vue-flow/core'

import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'

import { newVueFlowInstanceID } from './NestingFlows'

const { ancestorZoom } = defineProps(['ancestorZoom'])

// temporary fix to allow nested VueFlow components to pan
const NestedFlowElement = useTemplateRef('NestedFlowElement')
onUpdated(() => {
NestedFlowElement.value.vueFlowRef.parentElement?.parentElement?.parentElement?.classList.remove('nopan')
})

const elements = ref<Elements>([
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
])

const vueFlowInstanceID = `flow-${newVueFlowInstanceID()}`
const { onConnect, addEdges } = useVueFlow(vueFlowInstanceID)
onConnect(addEdges)
</script>

<template>
<VueFlow
:id="vueFlowInstanceID"
ref="NestedFlowElement"
v-model="elements"
:fit-view-on-init="true"
:auto-pan-on-connect="false"
:auto-pan-on-node-drag="false"
class="vue-flow-basic-example nested-flow"
:ancestor-zoom="ancestorZoom"
>
<Background />
<MiniMap />
<Controls />
</VueFlow>
</template>

<style scoped>
.nested-flow {
width: 100%;
height: 100%;
}
</style>
53 changes: 53 additions & 0 deletions examples/vite/src/NestingFlows/NestedFlowNode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core'
import NestedFlow from './NestedFlow.vue'

const { data, ancestorZoom } = defineProps(['data', 'ancestorZoom'])

// The purpose of this component is to avoid <Handle> and <VueFlow> being used in the same component. Otherwise useVueFlow will detect wrong instance ID.
</script>

<template>
<Handle id="source-left" type="source" :position="Position.Left" />
<Handle id="source-right" type="source" :position="Position.Right" />
<Handle id="target-left" type="target" :position="Position.Left" />
<Handle id="target-right" type="target" :position="Position.Right" />
<div class="container">
<div class="title">{{ data.label }}</div>
<div class="content nodrag">
<NestedFlow :ancestor-zoom="ancestorZoom" />
</div>
</div>
</template>

<style scoped>
.container {
border: 1px solid #ddd;
height: 400px;
width: 400px;
display: flex;
flex-direction: column;
}

.title {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 4px 4px 0 0;
background-color: black;
color: white;
font-weight: bold;
}

.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
border-radius: 0 0 4px 4px;
background: rgba(255, 255, 255, 0.75);
}
</style>
4 changes: 4 additions & 0 deletions examples/vite/src/NestingFlows/NestingFlows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
let VueFlowInstanceId = 0
export function newVueFlowInstanceID(): number {
return VueFlowInstanceId++
}
45 changes: 45 additions & 0 deletions examples/vite/src/NestingFlows/NestingFlows.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts" setup>
import type { Elements } from '@vue-flow/core'
import { VueFlow, useVueFlow } from '@vue-flow/core'

import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'

import NestedFlowNode from './NestedFlowNode.vue'
import { newVueFlowInstanceID } from './NestingFlows'

const elements = ref<Elements>([
{
id: 'A',
type: 'nestedFlow',
data: {
label: 'Flow A',
},
position: { x: 250, y: 5 },
},
{
id: 'B',
type: 'nestedFlow',
data: {
label: 'Flow B',
},
position: { x: 500, y: 100 },
},
])

const vueFlowInstanceID = `flow-${newVueFlowInstanceID()}`
const { onConnect, addEdges, viewport, ancestorZoom } = useVueFlow(vueFlowInstanceID)
onConnect(addEdges)
</script>

<template>
<VueFlow :id="vueFlowInstanceID" v-model="elements" :fit-view-on-init="true" class="vue-flow-basic-example">
<Background />
<MiniMap />
<Controls />
<template #node-nestedFlow="props">
<NestedFlowNode :data="props.data" :ancestor-zoom="ancestorZoom * viewport.zoom" />
</template>
</VueFlow>
</template>
53 changes: 53 additions & 0 deletions examples/vite/src/RecursiveNestingFlows/NestedFlowNode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core'
import RecursiveNestingFlow from './RecursiveNestingFlows.vue'

const { data, ancestorZoom } = defineProps(['data', 'ancestorZoom'])

// The purpose of this component is to avoid <Handle> and <VueFlow> being used in the same component. Otherwise useVueFlow will detect wrong instance ID.
</script>

<template>
<Handle id="source-left" type="source" :position="Position.Left" />
<Handle id="source-right" type="source" :position="Position.Right" />
<Handle id="target-left" type="target" :position="Position.Left" />
<Handle id="target-right" type="target" :position="Position.Right" />
<div class="container">
<div class="title">{{ data.label }}</div>
<div class="content nodrag">
<RecursiveNestingFlow :ancestor-zoom="ancestorZoom" />
</div>
</div>
</template>

<style scoped>
.container {
border: 1px solid #ddd;
height: 400px;
width: 400px;
display: flex;
flex-direction: column;
}

.title {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 4px 4px 0 0;
background-color: black;
color: white;
font-weight: bold;
}

.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
border-radius: 0 0 4px 4px;
background: rgba(255, 255, 255, 0.75);
}
</style>
4 changes: 4 additions & 0 deletions examples/vite/src/RecursiveNestingFlows/NestingFlows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
let VueFlowInstanceId = 0
export function newVueFlowInstanceID(): number {
return VueFlowInstanceId++
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script lang="ts" setup>
import type { Elements } from '@vue-flow/core'
import { VueFlow, useVueFlow } from '@vue-flow/core'

import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'

import NestedFlowNode from './NestedFlowNode.vue'
import { newVueFlowInstanceID } from './NestingFlows'

const id = newVueFlowInstanceID()
const elements = ref<Elements>(
id < 31
? [
{
id: 'A',
type: 'nestedFlow',
data: {
label: 'Flow A',
},
position: { x: 20, y: 5 },
},
{
id: 'B',
type: 'nestedFlow',
data: {
label: 'Flow B',
},
position: { x: 400, y: 20 },
},
]
: [],
)
const vueFlowInstanceID = `flow-${id}`
const { onConnect, addEdges, viewport, ancestorZoom } = useVueFlow(vueFlowInstanceID)
onConnect(addEdges)
</script>

<template>
<VueFlow
:id="vueFlowInstanceID"
v-model="elements"
:fit-view-on-init="true"
:auto-pan-on-connect="false"
:auto-pan-on-node-drag="false"
class="vue-flow-basic-example"
>
<Background />
<MiniMap />
<Controls />
<template #node-nestedFlow="props">
<NestedFlowNode :data="props.data" :ancestor-zoom="ancestorZoom * viewport.zoom" />
</template>
</VueFlow>
</template>
5 changes: 3 additions & 2 deletions packages/core/src/components/ConnectionLine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const ConnectionLine = defineComponent({
connectionLineOptions,
connectionStatus,
viewport,
ancestorZoom,
findNode,
} = useVueFlow()

Expand All @@ -32,8 +33,8 @@ const ConnectionLine = defineComponent({

const toXY = computed(() => {
return {
x: (connectionPosition.value.x - viewport.value.x) / viewport.value.zoom,
y: (connectionPosition.value.y - viewport.value.y) / viewport.value.zoom,
x: (connectionPosition.value.x - viewport.value.x) / viewport.value.zoom / ancestorZoom.value,
y: (connectionPosition.value.y - viewport.value.y) / viewport.value.zoom / ancestorZoom.value,
}
})

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/Nodes/NodeWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ const NodeWrapper = defineComponent({
'vue-flow__node',
`vue-flow__node-${nodeCmp.value === false ? 'default' : node.type || 'default'}`,
{
[noPanClassName.value]: isDraggable.value,
// [noPanClassName.value]: isDraggable.value,
dragging: dragging?.value,
draggable: isDraggable.value,
selected: node.selected,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/composables/useGetPointerPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import type { UseDragEvent } from './useDrag'
* @internal
*/
export function useGetPointerPosition() {
const { viewport, snapGrid, snapToGrid, vueFlowRef } = useVueFlow()
const { viewport, snapGrid, snapToGrid, vueFlowRef, ancestorZoom } = useVueFlow()

// returns the pointer position projected to the VF coordinate system
return (event: UseDragEvent | MouseTouchEvent) => {
const containerBounds = vueFlowRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }
const evt = isUseDragEvent(event) ? event.sourceEvent : event

const { x, y } = getEventPosition(evt, containerBounds as DOMRect)
const pointerPos = pointToRendererPoint({ x, y }, viewport.value)
const pointerPos = pointToRendererPoint({ x, y }, viewport.value, undefined, undefined, ancestorZoom.value)
const { x: xSnapped, y: ySnapped } = snapToGrid.value ? snapPosition(pointerPos, snapGrid.value) : pointerPos

// we need the snapped position to be able to skip unnecessary drag events
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/composables/useHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function useHandle({
endConnection,
emits,
viewport,
ancestorZoom,
edges,
nodes,
isValidConnection: isValidConnectionProp,
Expand Down Expand Up @@ -116,6 +117,8 @@ export function useHandle({

let prevActiveHandle: Element
let connectionPosition = getEventPosition(event, containerBounds)
connectionPosition.x += viewport.value.x * (1 - ancestorZoom.value)
connectionPosition.y += viewport.value.y * (1 - ancestorZoom.value)
let autoPanStarted = false

// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
Expand Down Expand Up @@ -177,9 +180,11 @@ export function useHandle({

function onPointerMove(event: MouseTouchEvent) {
connectionPosition = getEventPosition(event, containerBounds)
connectionPosition.x += viewport.value.x * (1 - ancestorZoom.value)
connectionPosition.y += viewport.value.y * (1 - ancestorZoom.value)

closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1]),
pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1], ancestorZoom.value),
connectionRadius.value,
nodeLookup.value,
fromHandle,
Expand Down Expand Up @@ -219,7 +224,7 @@ export function useHandle({
isValid,
to:
result.toHandle && isValid
? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, viewport.value)
? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, viewport.value, ancestorZoom.value)
: connectionPosition,
toHandle: result.toHandle,
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
Expand Down Expand Up @@ -250,6 +255,7 @@ export function useHandle({
y: closestHandle.y,
},
viewport.value,
ancestorZoom.value,
)
: connectionPosition,
result.toHandle,
Expand Down
Loading