Skip to content

Commit e94611a

Browse files
authored
chore(docs): simplify animated layout example (#1747)
* chore(examples): update ts example of layouting Signed-off-by: braks <[email protected]> * chore(docs): cleanup animated layout example Signed-off-by: braks <[email protected]> --------- Signed-off-by: braks <[email protected]>
1 parent 25f9dd1 commit e94611a

25 files changed

+1062
-687
lines changed

docs/examples/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { IntersectionApp, IntersectionCSS } from './intersection'
1818
import { SnapToHandleApp, SnappableConnectionLine } from './connection-radius'
1919
import { NodeResizerApp, ResizableNode } from './node-resizer'
2020
import { ToolbarApp, ToolbarNode } from './node-toolbar'
21-
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess, useShuffle } from './layout'
21+
import { LayoutApp, LayoutEdge, LayoutElements, LayoutIcon, LayoutNode, useLayout, useRunProcess } from './layout'
2222
import { SimpleLayoutApp, SimpleLayoutElements, SimpleLayoutIcon, useSimpleLayout } from './layout-simple'
2323
import { LoopbackApp, LoopbackCSS, LoopbackEdge } from './loopback'
2424
import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
@@ -136,7 +136,6 @@ export const exampleImports = {
136136
'ProcessNode.vue': LayoutNode,
137137
'AnimationEdge.vue': LayoutEdge,
138138
'useRunProcess.js': useRunProcess,
139-
'useShuffle.js': useShuffle,
140139
'useLayout.js': useLayout,
141140
'Icon.vue': LayoutIcon,
142141
'additionalImports': {

docs/examples/layout/AnimationEdge.vue

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup>
2-
import { computed, ref, toRef, watch } from 'vue'
2+
import { computed, nextTick, ref, watch } from 'vue'
33
import { BaseEdge, EdgeLabelRenderer, Position, getSmoothStepPath, useNodesData, useVueFlow } from '@vue-flow/core'
4+
import { ProcessStatus } from './useRunProcess'
45
56
const props = defineProps({
67
id: {
@@ -39,95 +40,124 @@ const props = defineProps({
3940
type: String,
4041
default: Position.Left,
4142
},
43+
data: {
44+
type: Object,
45+
required: false,
46+
},
4247
})
4348
4449
const { updateEdgeData } = useVueFlow()
4550
46-
const nodesData = useNodesData([props.target, props.source])
51+
/**
52+
* We call `useNodesData` to get the data of the source and target nodes, which
53+
* contain the information about the status of each nodes' process.
54+
*/
55+
const nodesData = useNodesData(() => [props.target, props.source])
4756
4857
const labelRef = ref()
4958
5059
const edgeRef = ref()
5160
61+
/**
62+
* We extract the source and target node data from the nodes data.
63+
* We only need the first element of the array since we are only connecting two nodes.
64+
*/
5265
const targetNodeData = computed(() => nodesData.value[0].data)
5366
5467
const sourceNodeData = computed(() => nodesData.value[1].data)
5568
56-
const isFinished = toRef(() => sourceNodeData.value.isFinished)
57-
58-
const isCancelled = toRef(() => targetNodeData.value.isCancelled)
59-
60-
const isAnimating = ref(false)
69+
const isAnimating = computed({
70+
get: () => props.data.isAnimating || false,
71+
set: (value) => {
72+
updateEdgeData(props.id, { isAnimating: value })
73+
},
74+
})
6175
6276
let animation = null
6377
6478
const path = computed(() => getSmoothStepPath(props))
6579
6680
const edgeColor = computed(() => {
67-
if (targetNodeData.value.hasError) {
68-
return '#f87171'
69-
}
70-
71-
if (targetNodeData.value.isFinished) {
72-
return '#42B983'
73-
}
74-
75-
if (targetNodeData.value.isCancelled || targetNodeData.value.isSkipped) {
76-
return '#fbbf24'
77-
}
78-
79-
if (targetNodeData.value.isRunning || isAnimating.value) {
80-
return '#2563eb'
81-
}
82-
83-
return '#6b7280'
84-
})
85-
86-
watch(isCancelled, (isCancelled) => {
87-
if (isCancelled) {
88-
animation?.cancel()
81+
switch (targetNodeData.value.status) {
82+
case ProcessStatus.ERROR:
83+
return '#f87171'
84+
case ProcessStatus.FINISHED:
85+
return '#42B983'
86+
case ProcessStatus.CANCELLED:
87+
case ProcessStatus.SKIPPED:
88+
return '#fbbf24'
89+
case ProcessStatus.RUNNING:
90+
return '#2563eb'
91+
default:
92+
return '#6b7280'
8993
}
9094
})
9195
92-
watch(isAnimating, (isAnimating) => {
93-
updateEdgeData(props.id, { isAnimating })
94-
})
95-
96-
watch(isFinished, (isFinished) => {
97-
if (isFinished) {
98-
runAnimation()
99-
}
100-
})
96+
// Cancel the animation if the target nodes' process was cancelled
97+
watch(
98+
() => targetNodeData.value.status === ProcessStatus.CANCELLED,
99+
(isCancelled) => {
100+
if (isCancelled) {
101+
animation?.cancel()
102+
}
103+
},
104+
)
105+
106+
// Run the animation when the source nodes' process is finished
107+
watch(
108+
() => sourceNodeData.value.status === ProcessStatus.FINISHED,
109+
(isFinished) => {
110+
if (isFinished) {
111+
runAnimation()
112+
}
113+
},
114+
)
101115
102116
function runAnimation() {
103117
const pathEl = edgeRef.value?.pathEl
118+
const labelEl = labelRef.value
104119
105-
if (!pathEl) {
120+
if (!pathEl || !labelEl) {
121+
console.warn('Path or label element not found')
106122
return
107123
}
108124
109125
const totalLength = pathEl.getTotalLength()
110126
111127
isAnimating.value = true
112128
113-
const keyframes = [{ offsetDistance: '0%' }, { offsetDistance: '100%' }]
114-
115-
// use path length as a possible measure for the animation duration
116-
const pathLengthDuration = totalLength * 10
117-
118-
animation = labelRef.value.animate(keyframes, {
119-
duration: Math.min(Math.max(pathLengthDuration, 1500), 3000), // clamp duration between 1.5s and 3s
120-
direction: 'normal',
121-
easing: 'ease-in-out',
122-
iterations: 1,
129+
// We need to wait for the next tick to ensure that the label element is rendered
130+
nextTick(() => {
131+
const keyframes = [{ offsetDistance: '0%' }, { offsetDistance: '100%' }]
132+
133+
// use path length as a possible measure for the animation duration
134+
const pathLengthDuration = totalLength * 10
135+
136+
/**
137+
* We animate the label element along the path of the edge using the `offsetDistance` property and
138+
* the Web Animations API.
139+
*
140+
* The `animate` method returns an `Animation` object that we can use to listen to events like `finish` or `cancel`.
141+
*
142+
* The animation duration is calculated based on the total length of the path and clamped between 1.5s and 3s.
143+
*
144+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
145+
*/
146+
const labelAnimation = labelEl.animate(keyframes, {
147+
duration: Math.min(Math.max(pathLengthDuration, 1500), 3000), // clamp duration between 1.5s and 3s
148+
direction: 'normal',
149+
easing: 'ease-in-out',
150+
iterations: 1,
151+
})
152+
153+
const handleAnimationEnd = () => {
154+
isAnimating.value = false
155+
}
156+
157+
labelAnimation.onfinish = handleAnimationEnd
158+
labelAnimation.oncancel = handleAnimationEnd
159+
animation = labelAnimation
123160
})
124-
125-
animation.onfinish = handleAnimationEnd
126-
animation.oncancel = handleAnimationEnd
127-
}
128-
129-
function handleAnimationEnd() {
130-
isAnimating.value = false
131161
}
132162
</script>
133163
@@ -152,7 +182,6 @@ export default {
152182
offsetRotate: '0deg',
153183
offsetAnchor: 'center',
154184
}"
155-
class="animated-edge-label"
156185
>
157186
<span class="truck">
158187
<span class="box">📦</span>

docs/examples/layout/App.vue

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import AnimationEdge from './AnimationEdge.vue'
88
99
import { initialEdges, initialNodes } from './initial-elements.js'
1010
import { useRunProcess } from './useRunProcess'
11-
import { useShuffle } from './useShuffle'
1211
import { useLayout } from './useLayout'
1312
1413
const nodes = ref(initialNodes)
@@ -17,26 +16,12 @@ const edges = ref(initialEdges)
1716
1817
const cancelOnError = ref(true)
1918
20-
const shuffle = useShuffle()
21-
22-
const { graph, layout, previousDirection } = useLayout()
19+
const { graph, layout } = useLayout()
2320
2421
const { run, stop, reset, isRunning } = useRunProcess({ graph, cancelOnError })
2522
2623
const { fitView } = useVueFlow()
2724
28-
async function shuffleGraph() {
29-
await stop()
30-
31-
reset(nodes.value)
32-
33-
edges.value = shuffle(nodes.value)
34-
35-
nextTick(() => {
36-
layoutGraph(previousDirection.value)
37-
})
38-
}
39-
4025
async function layoutGraph(direction) {
4126
await stop()
4227
@@ -53,8 +38,8 @@ async function layoutGraph(direction) {
5338
<template>
5439
<div class="layout-flow">
5540
<VueFlow
56-
:nodes="nodes"
57-
:edges="edges"
41+
v-model:nodes="nodes"
42+
v-model:edges="edges"
5843
:default-edge-options="{ type: 'animation', animated: true }"
5944
@nodes-initialized="layoutGraph('LR')"
6045
>
@@ -73,6 +58,7 @@ async function layoutGraph(direction) {
7358
:targetY="edgeProps.targetY"
7459
:source-position="edgeProps.sourcePosition"
7560
:target-position="edgeProps.targetPosition"
61+
:data="edgeProps.data"
7662
/>
7763
</template>
7864

@@ -95,10 +81,6 @@ async function layoutGraph(direction) {
9581
<button title="set vertical layout" @click="layoutGraph('TB')">
9682
<Icon name="vertical" />
9783
</button>
98-
99-
<button title="shuffle graph" @click="shuffleGraph">
100-
<Icon name="shuffle" />
101-
</button>
10284
</div>
10385

10486
<div class="checkbox-panel">

docs/examples/layout/Icon.vue

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,4 @@ defineProps({
3030
<path d="M7,7 L12,2 L17,7" stroke="currentColor" stroke-width="2" fill="none" />
3131
<path d="M7,17 L12,22 L17,17" stroke="currentColor" stroke-width="2" fill="none" />
3232
</svg>
33-
34-
<svg v-else-if="name === 'shuffle'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
35-
<path
36-
fill="currentColor"
37-
d="M14 20v-2h2.6l-3.175-3.175L14.85 13.4L18 16.55V14h2v6zm-8.6 0L4 18.6L16.6 6H14V4h6v6h-2V7.4zm3.775-9.425L4 5.4L5.4 4l5.175 5.175z"
38-
/>
39-
</svg>
4033
</template>

docs/examples/layout/ProcessNode.vue

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup>
22
import { computed, toRef } from 'vue'
33
import { Handle, useNodeConnections } from '@vue-flow/core'
4+
import { ProcessStatus } from './useRunProcess'
45
56
const props = defineProps({
67
data: {
@@ -23,64 +24,61 @@ const targetConnections = useNodeConnections({
2324
handleType: 'source',
2425
})
2526
26-
const isSender = toRef(() => sourceConnections.value.length <= 0)
27+
const isStartNode = toRef(() => sourceConnections.value.length <= 0)
2728
28-
const isReceiver = toRef(() => targetConnections.value.length <= 0)
29+
const isEndNode = toRef(() => targetConnections.value.length <= 0)
30+
31+
const status = toRef(() => props.data.status)
2932
3033
const bgColor = computed(() => {
31-
if (isSender.value) {
34+
if (isStartNode.value) {
3235
return '#2563eb'
3336
}
3437
35-
if (props.data.hasError) {
36-
return '#f87171'
37-
}
38-
39-
if (props.data.isFinished) {
40-
return '#42B983'
38+
switch (status.value) {
39+
case ProcessStatus.ERROR:
40+
return '#f87171'
41+
case ProcessStatus.FINISHED:
42+
return '#42B983'
43+
case ProcessStatus.CANCELLED:
44+
return '#fbbf24'
45+
default:
46+
return '#4b5563'
4147
}
42-
43-
if (props.data.isCancelled) {
44-
return '#fbbf24'
45-
}
46-
47-
return '#4b5563'
4848
})
4949
5050
const processLabel = computed(() => {
51-
if (props.data.hasError) {
52-
return ''
53-
}
54-
55-
if (props.data.isSkipped) {
56-
return '🚧'
57-
}
58-
59-
if (props.data.isCancelled) {
60-
return '🚫'
61-
}
62-
63-
if (isSender.value) {
51+
if (isStartNode.value) {
6452
return '📦'
6553
}
6654
67-
if (props.data.isFinished) {
68-
return '😎'
55+
switch (status.value) {
56+
case ProcessStatus.ERROR:
57+
return ''
58+
case ProcessStatus.SKIPPED:
59+
return '🚧'
60+
case ProcessStatus.CANCELLED:
61+
return '🚫'
62+
case ProcessStatus.FINISHED:
63+
return '😎'
64+
default:
65+
return '🏠'
6966
}
70-
71-
return '🏠'
7267
})
7368
</script>
7469

7570
<template>
76-
<div class="process-node" :style="{ backgroundColor: bgColor, boxShadow: data.isRunning ? '0 0 10px rgba(0, 0, 0, 0.5)' : '' }">
77-
<Handle v-if="!isSender" type="target" :position="targetPosition">
78-
<span v-if="!data.isRunning && !data.isFinished && !data.isCancelled && !data.isSkipped && !data.hasError">📥 </span>
71+
<div
72+
class="process-node"
73+
:style="{ backgroundColor: bgColor, boxShadow: status === ProcessStatus.RUNNING ? '0 0 10px rgba(0, 0, 0, 0.5)' : '' }"
74+
>
75+
<Handle v-if="!isStartNode" type="target" :position="targetPosition">
76+
<span v-if="status === null">📥 </span>
7977
</Handle>
8078

81-
<Handle v-if="!isReceiver" type="source" :position="sourcePosition" />
79+
<Handle v-if="!isEndNode" type="source" :position="sourcePosition" />
8280

83-
<div v-if="!isSender && data.isRunning" class="spinner" />
81+
<div v-if="status === ProcessStatus.RUNNING" class="spinner" />
8482
<span v-else>
8583
{{ processLabel }}
8684
</span>

0 commit comments

Comments
 (0)