feat!: Add hooks to store

* For easier access and to avoid context issues with injections hooks are put into the store
* hooks can be accessed and will emit events when the flow has bound its emit function to the hooks
* useHooks and useStore are not exported anymore! Instead, useVueFlow is provided, which will return an instance of the current vue flow store
* Rename NodeIdContextKey to NodeId
* Add addElements function to store to add elements without having to re-set the whole store
* use v-model for elements, re-set elements only if there's an actual change

Signed-off-by: Braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Braks
2021-11-18 21:24:20 +01:00
parent 0cca54e7e2
commit 99dbd4b2cc
14 changed files with 134 additions and 100 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useHandle, useHooks, useStore } from '../../composables' import { useHandle, useHooks, useStore } from '../../composables'
import { Position, ValidConnectionFunc } from '../../types' import { Position, ValidConnectionFunc } from '../../types'
import { NodeIdContextKey } from '~/context' import { NodeId } from '~/context'
interface HandleProps { interface HandleProps {
id?: string id?: string
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<HandleProps>(), {
const store = useStore() const store = useStore()
const hooks = useHooks() const hooks = useHooks()
const nodeId = inject(NodeIdContextKey)! const nodeId = inject(NodeId)!
const handler = useHandle() const handler = useHandle()
const onMouseDownHandler = (event: MouseEvent) => const onMouseDownHandler = (event: MouseEvent) =>
+2 -2
View File
@@ -2,7 +2,7 @@
import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable' import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable'
import { useHooks, useStore } from '../../composables' import { useHooks, useStore } from '../../composables'
import { Node, NodeType, SnapGrid } from '../../types' import { Node, NodeType, SnapGrid } from '../../types'
import { NodeIdContextKey } from '~/context' import { NodeId } from '~/context'
interface NodeProps { interface NodeProps {
node: Node node: Node
@@ -19,7 +19,7 @@ const props = withDefaults(defineProps<NodeProps>(), {
const store = useStore() const store = useStore()
const hooks = useHooks() const hooks = useHooks()
provide(NodeIdContextKey, props.node.id) provide(NodeId, props.node.id)
const nodeElement = templateRef<HTMLDivElement>('node-element', null) const nodeElement = templateRef<HTMLDivElement>('node-element', null)
+1 -1
View File
@@ -5,4 +5,4 @@ export { default as useKeyPress } from './useKeyPress'
export { default as useZoomPanHelper } from './useZoomPanHelper' export { default as useZoomPanHelper } from './useZoomPanHelper'
export { default as useStore } from './useStore' export { default as useStore } from './useStore'
export { default as useWindow } from './useWindow' export { default as useWindow } from './useWindow'
export { default as useVueFlow } from './useVueFlow' export { default as useVueFlow, initFlow } from './useVueFlow'
+23 -21
View File
@@ -1,8 +1,20 @@
import { Connection, Edge, Elements, EmitFunc, FlowHooks, FlowTransform, Node, OnConnectStartParams, FlowInstance } from '~/types' import useStore from './useStore'
import {
Connection,
Edge,
Elements,
EmitFunc,
FlowHooks,
FlowTransform,
Node,
OnConnectStartParams,
FlowInstance,
FlowEvents,
} from '~/types'
import { Hooks } from '~/context' import { Hooks } from '~/context'
// flow event hooks // flow event hooks
const hooks = (): FlowHooks => { export const createHooks = (): FlowHooks => {
return { return {
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(), elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
elementsRemove: createEventHook<Elements>(), elementsRemove: createEventHook<Elements>(),
@@ -45,33 +57,23 @@ const hooks = (): FlowHooks => {
edgeUpdateEnd: createEventHook<MouseEvent>(), edgeUpdateEnd: createEventHook<MouseEvent>(),
} }
} }
export const createHooks = (): FlowHooks & { bind: (emit: EmitFunc) => FlowHooks } => {
const eventHooks = hooks()
const bind = (emit: EmitFunc) => {
for (const [key, value] of Object.entries(eventHooks)) {
value.on((data: unknown) => {
emit(key as keyof FlowHooks, data)
})
}
return eventHooks const bind = (emit: EmitFunc, hooks: FlowHooks) => {
} for (const [key, value] of Object.entries(hooks)) {
value.on((data: FlowEvents[keyof FlowHooks]) => {
return { emit(key as keyof FlowHooks, data)
...eventHooks, })
bind,
} }
} }
export default (emit?: EmitFunc) => { export default (emit?: EmitFunc) => {
let hooks = inject(Hooks)! let hooks = inject(Hooks)!
if (!hooks) { if (!hooks) {
const store = useStore()
if (import.meta.env.DEV) console.warn('hooks context not found; creating default hooks') if (import.meta.env.DEV) console.warn('hooks context not found; creating default hooks')
if (!emit) console.error('no emit function found for hook context.') hooks = store.hooks
else { if (typeof emit === 'function') bind(emit, hooks)
hooks = createHooks().bind(emit) provide(Hooks, hooks)
provide(Hooks, hooks)
}
} }
return hooks return hooks
+1
View File
@@ -4,6 +4,7 @@ import { Store } from '~/context'
export default (options?: Partial<FlowOptions>) => { export default (options?: Partial<FlowOptions>) => {
let store = inject(Store)! let store = inject(Store)!
if (!store) { if (!store) {
if (import.meta.env.DEV) console.warn('store context not found; creating default store') if (import.meta.env.DEV) console.warn('store context not found; creating default store')
store = useFlowStore({ store = useFlowStore({
+5 -12
View File
@@ -1,22 +1,15 @@
import useHooks from './useHooks' import useHooks from './useHooks'
import useStore from './useStore' import useStore from './useStore'
import { FlowInstance } from '~/types' import { EmitFunc } from '~/types'
export default () => { export const initFlow = (emit: EmitFunc) => {
const store = useStore() const store = useStore()
const hooks = useHooks() const hooks = useHooks(emit)
const isReady = ref(false)
const instance = ref<FlowInstance>()
hooks.load.on((flow) => {
isReady.value = true
instance.value = flow
if (import.meta.env.DEV) console.log(`vue flow ${store.$id} ready.`)
})
return { return {
isReady,
instance,
store, store,
hooks, hooks,
} }
} }
export default () => useStore()
+23 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { CSSProperties, onBeforeUnmount } from 'vue' import { CSSProperties, onBeforeUnmount } from 'vue'
import { invoke } from '@vueuse/core' import { invoke } from '@vueuse/core'
import diff from 'microdiff'
import { import {
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
@@ -12,14 +13,15 @@ import {
TranslateExtent, TranslateExtent,
NodeExtent, NodeExtent,
FlowOptions, FlowOptions,
FlowEvents,
} from '../../types' } from '../../types'
import { useHooks, useStore, createHooks } from '../../composables'
import ZoomPane from '../../container/ZoomPane/ZoomPane.vue' import ZoomPane from '../../container/ZoomPane/ZoomPane.vue'
import SelectionPane from '../../container/SelectionPane/SelectionPane.vue' import SelectionPane from '../../container/SelectionPane/SelectionPane.vue'
import NodeRenderer from '../../container/NodeRenderer/NodeRenderer.vue' import NodeRenderer from '../../container/NodeRenderer/NodeRenderer.vue'
import EdgeRenderer from '../../container/EdgeRenderer/EdgeRenderer.vue' import EdgeRenderer from '../../container/EdgeRenderer/EdgeRenderer.vue'
import { DefaultNode, InputNode, OutputNode } from '../../components/Nodes' import { DefaultNode, InputNode, OutputNode } from '../../components/Nodes'
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '../../components/Edges' import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '../../components/Edges'
import { initFlow } from '~/composables'
export interface FlowProps extends FlowOptions { export interface FlowProps extends FlowOptions {
elements: Elements elements: Elements
@@ -98,7 +100,9 @@ const props = withDefaults(defineProps<FlowProps>(), {
edgeTypesId: '1', edgeTypesId: '1',
nodeTypesId: '1', nodeTypesId: '1',
}) })
const emit = defineEmits(Object.keys(createHooks())) export type DefineFlowEvents = { (event: keyof FlowEvents, flowEvent: FlowEvents[keyof FlowEvents]): void }
const emit = defineEmits<DefineFlowEvents>()
const defaultNodeTypes: Record<string, NodeType> = { const defaultNodeTypes: Record<string, NodeType> = {
input: InputNode as NodeType, input: InputNode as NodeType,
@@ -113,8 +117,7 @@ const defaultEdgeTypes: Record<string, EdgeType> = {
smoothstep: SmoothStepEdge as EdgeType, smoothstep: SmoothStepEdge as EdgeType,
} }
const store = useStore(props) const { store, hooks } = initFlow(emit)
const hooks = useHooks(emit)
const init = (opts: typeof props) => { const init = (opts: typeof props) => {
store.$state = { ...store.$state, ...opts } store.$state = { ...store.$state, ...opts }
@@ -124,12 +127,16 @@ const init = (opts: typeof props) => {
store.setTranslateExtent(opts.translateExtent) store.setTranslateExtent(opts.translateExtent)
store.setNodeExtent(opts.nodeExtent) store.setNodeExtent(opts.nodeExtent)
} }
const elements = useVModel(props, 'elements', emit)
onBeforeUnmount(() => store?.$dispose()) onBeforeUnmount(() => store?.$dispose())
watch( watch(
() => props, () => props,
(val) => init(val), (val, oldVal) => {
const hasDiff = diff(val, oldVal)
if (hasDiff.length > 0) init(val)
},
{ flush: 'pre', deep: true }, { flush: 'pre', deep: true },
) )
invoke(async () => { invoke(async () => {
@@ -137,16 +144,27 @@ invoke(async () => {
init(props) init(props)
}) })
watch(
elements,
(val, oldVal) => {
const hasDiff = diff(val, oldVal)
if (hasDiff.length > 0) store.setElements(val)
},
{ flush: 'pre', deep: true },
)
const nodeTypes = computed(() => { const nodeTypes = computed(() => {
let types = defaultNodeTypes let types = defaultNodeTypes
if (Array.isArray(props.nodeTypes)) props.nodeTypes.forEach((type) => (types[type] = true)) if (Array.isArray(props.nodeTypes)) props.nodeTypes.forEach((type) => (types[type] = true))
else types = { ...types, ...props.nodeTypes } else types = { ...types, ...props.nodeTypes }
store.$state.nodeTypes = types
return types return types
}) })
const edgeTypes = computed(() => { const edgeTypes = computed(() => {
let types = defaultEdgeTypes let types = defaultEdgeTypes
if (Array.isArray(props.edgeTypes)) props.edgeTypes.forEach((type) => (types[type] = true)) if (Array.isArray(props.edgeTypes)) props.edgeTypes.forEach((type) => (types[type] = true))
else types = { ...types, ...props.edgeTypes } else types = { ...types, ...props.edgeTypes }
store.$state.edgeTypes = types
return types return types
}) })
</script> </script>
+4 -3
View File
@@ -2,7 +2,7 @@
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom' import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
import { get, invoke } from '@vueuse/core' import { get, invoke } from '@vueuse/core'
import { pointer, select } from 'd3-selection' import { pointer, select } from 'd3-selection'
import { FlowTransform, KeyCode, PanOnScrollMode } from '../../types' import { FlowInstance, FlowTransform, KeyCode, PanOnScrollMode } from '../../types'
import { useHooks, useKeyPress, useStore, useZoomPanHelper } from '../../composables' import { useHooks, useKeyPress, useStore, useZoomPanHelper } from '../../composables'
import { clamp, onLoadGetElements, onLoadProject, onLoadToObject } from '../../utils' import { clamp, onLoadGetElements, onLoadProject, onLoadToObject } from '../../utils'
@@ -186,7 +186,7 @@ store.dimensions = {
invoke(async () => { invoke(async () => {
await until(() => !isNaN(width.value) && width.value > 0 && !isNaN(height.value) && height.value > 0).toBeTruthy() await until(() => !isNaN(width.value) && width.value > 0 && !isNaN(height.value) && height.value > 0).toBeTruthy()
hooks.load.trigger({ const instance: FlowInstance = {
fitView: (params = { padding: 0.1 }) => fitView(params), fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn, zoomIn,
zoomOut, zoomOut,
@@ -195,7 +195,8 @@ invoke(async () => {
project: onLoadProject(store), project: onLoadProject(store),
getElements: onLoadGetElements(store), getElements: onLoadGetElements(store),
toObject: onLoadToObject(store), toObject: onLoadToObject(store),
}) }
hooks.load.trigger(instance)
}) })
watch( watch(
+1 -1
View File
@@ -3,4 +3,4 @@ import { ElementId, FlowHooks, FlowStore } from '~/types'
export const Store: InjectionKey<FlowStore> = Symbol('store') export const Store: InjectionKey<FlowStore> = Symbol('store')
export const Hooks: InjectionKey<FlowHooks> = Symbol('hooks') export const Hooks: InjectionKey<FlowHooks> = Symbol('hooks')
export const NodeIdContextKey: InjectionKey<ElementId> = Symbol('NodeIdContext') export const NodeId: InjectionKey<ElementId> = Symbol('nodeId')
+1 -3
View File
@@ -19,11 +19,9 @@ export {
graphPosToZoomedPos, graphPosToZoomedPos,
} from './utils/graph' } from './utils/graph'
export { default as useZoomPanHelper } from './composables/useZoomPanHelper' export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
export { default as useStore } from './composables/useStore' export { default as useVueFlow } from './composables/useVueFlow'
export { default as useHooks } from './composables/useHooks'
export { default as useHandle } from './composables/useHandle' export { default as useHandle } from './composables/useHandle'
export { default as useKeyPress } from './composables/useKeyPress' export { default as useKeyPress } from './composables/useKeyPress'
export { default as useVueFlow } from './composables/useVueFlow'
export * from './additional-components' export * from './additional-components'
export * from './types' export * from './types'
@@ -1,6 +1,6 @@
import { setActivePinia, createPinia, defineStore, StoreDefinition } from 'pinia' import { setActivePinia, createPinia, defineStore, StoreDefinition } from 'pinia'
import diff from 'microdiff' import diff from 'microdiff'
import { Edge, FlowState, Node, FlowActions } from '~/types' import { Edge, FlowState, Node, FlowActions, Elements, NodeExtent } from '~/types'
import { import {
clampPosition, clampPosition,
getDimensions, getDimensions,
@@ -21,7 +21,54 @@ type NextElements = {
const pinia = createPinia() const pinia = createPinia()
let id = 0 let id = 0
export default function useFlowStore(preloadedState: FlowState): StoreDefinition<string, FlowState, any, FlowActions> { const parseElements = (elements: Elements, nodes: Node[], edges: Edge[], nodeExtent: NodeExtent) => {
const nextElements: NextElements = {
nextNodes: [],
nextEdges: [],
}
for (const element of elements) {
if (isNode(element)) {
const storeNode = nodes.find((node) => node.id === element.id)
if (storeNode) {
const updatedNode: Node = {
...storeNode,
...element,
}
if (!updatedNode.__rf) updatedNode.__rf = {}
if (storeNode.position.x !== element.position.x || storeNode.position.y !== element.position.y) {
updatedNode.__rf.position = element.position
}
if (typeof element.type !== 'undefined' && element.type !== storeNode.type) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__rf.width = undefined
}
nextElements.nextNodes.push(updatedNode)
} else {
nextElements.nextNodes.push(parseNode(element, nodeExtent))
}
} else if (isEdge(element)) {
const storeEdge = edges.find((se) => se.id === element.id)
if (storeEdge) {
nextElements.nextEdges.push({
...storeEdge,
...element,
})
} else {
nextElements.nextEdges.push(parseEdge(element))
}
}
}
return nextElements
}
export default function flowStore(preloadedState: FlowState): StoreDefinition<string, FlowState, any, FlowActions> {
setActivePinia(pinia) setActivePinia(pinia)
return defineStore({ return defineStore({
@@ -32,51 +79,7 @@ export default function useFlowStore(preloadedState: FlowState): StoreDefinition
getters: {}, getters: {},
actions: { actions: {
setElements(elements) { setElements(elements) {
const nextElements: NextElements = { const { nextNodes, nextEdges } = parseElements(elements, this.nodes, this.edges, this.nodeExtent)
nextNodes: [],
nextEdges: [],
}
const { nextNodes, nextEdges } = elements.reduce((res, propElement): NextElements => {
if (isNode(propElement)) {
const storeNode = this.nodes.find((node) => node.id === propElement.id)
if (storeNode) {
const updatedNode: Node = {
...storeNode,
...propElement,
}
if (!updatedNode.__rf) updatedNode.__rf = {}
if (storeNode.position.x !== propElement.position.x || storeNode.position.y !== propElement.position.y) {
updatedNode.__rf.position = propElement.position
}
if (typeof propElement.type !== 'undefined' && propElement.type !== storeNode.type) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__rf.width = undefined
}
res.nextNodes.push(updatedNode)
} else {
res.nextNodes.push(parseNode(propElement, this.nodeExtent))
}
} else if (isEdge(propElement)) {
const storeEdge = this.edges.find((se) => se.id === propElement.id)
if (storeEdge) {
res.nextEdges.push({
...storeEdge,
...propElement,
})
} else {
res.nextEdges.push(parseEdge(propElement))
}
}
return res
}, nextElements)
this.nodes = nextNodes this.nodes = nextNodes
this.edges = nextEdges this.edges = nextEdges
}, },
@@ -257,6 +260,11 @@ export default function useFlowStore(preloadedState: FlowState): StoreDefinition
this.nodesConnectable = isInteractive this.nodesConnectable = isInteractive
this.elementsSelectable = isInteractive this.elementsSelectable = isInteractive
}, },
addElements(elements: Elements) {
const { nextNodes, nextEdges } = parseElements(elements, this.nodes, this.edges, this.nodeExtent)
this.nodes = [...this.nodes, ...nextNodes]
this.edges = [...this.edges, ...nextEdges]
},
}, },
}) })
} }
+7 -1
View File
@@ -1,4 +1,5 @@
import { ConnectionMode, FlowState } from '~/types' import { ConnectionMode, FlowState } from '~/types'
import { createHooks } from '~/composables'
export const initialState = (): FlowState => ({ export const initialState = (): FlowState => ({
dimensions: { dimensions: {
@@ -6,6 +7,8 @@ export const initialState = (): FlowState => ({
height: 0, height: 0,
}, },
transform: [0, 0, 1], transform: [0, 0, 1],
nodeTypes: [],
edgeTypes: [],
elements: [], elements: [],
nodes: [], nodes: [],
edges: [], edges: [],
@@ -54,7 +57,10 @@ export const initialState = (): FlowState => ({
multiSelectionActive: false, multiSelectionActive: false,
isReady: false,
hooks: createHooks(),
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-', vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-',
}) })
export { default as useFlowStore } from './useFlowStore' export { default as useFlowStore } from './flowStore'
+1
View File
@@ -22,4 +22,5 @@ export interface FlowActions {
updateSize: (size: Dimensions) => void updateSize: (size: Dimensions) => void
setConnectionNodeId: (payload: SetConnectionId) => void setConnectionNodeId: (payload: SetConnectionId) => void
setInteractive: (isInteractive: boolean) => void setInteractive: (isInteractive: boolean) => void
addElements: (elements: Elements) => void
} }
+8 -2
View File
@@ -2,17 +2,20 @@ import { Store } from 'pinia'
import { Dimensions, ElementId, Elements, Rect, SelectionRect, SnapGrid, Transform, XYPosition } from './types' import { Dimensions, ElementId, Elements, Rect, SelectionRect, SnapGrid, Transform, XYPosition } from './types'
import { HandleType } from './handle' import { HandleType } from './handle'
import { ConnectionMode, OnConnectEndFunc, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc } from './connection' import { ConnectionMode, OnConnectEndFunc, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc } from './connection'
import { Edge } from './edge' import { Edge, EdgeType } from './edge'
import { Node, NodeExtent, TranslateExtent } from './node' import { Node, NodeExtent, NodeType, TranslateExtent } from './node'
import { FlowActions } from './actions' import { FlowActions } from './actions'
import { D3Selection, D3Zoom, D3ZoomHandler } from './panel' import { D3Selection, D3Zoom, D3ZoomHandler } from './panel'
import { FlowHooks } from '~/types/hooks'
export interface FlowState { export interface FlowState {
dimensions: Dimensions dimensions: Dimensions
transform: Transform transform: Transform
elements: Elements elements: Elements
nodes: Node[] nodes: Node[]
nodeTypes: Record<string, NodeType> | string[]
edges: Edge[] edges: Edge[]
edgeTypes: Record<string, EdgeType> | string[]
selectedElements?: Elements selectedElements?: Elements
selectedNodesBbox: Rect selectedNodesBbox: Rect
@@ -50,6 +53,9 @@ export interface FlowState {
onConnectStart?: OnConnectStartFunc onConnectStart?: OnConnectStartFunc
onConnectStop?: OnConnectStopFunc onConnectStop?: OnConnectStopFunc
onConnectEnd?: OnConnectEndFunc onConnectEnd?: OnConnectEndFunc
isReady: boolean
hooks: FlowHooks
} }
export type FlowStore = Store<string, FlowState, any, FlowActions> export type FlowStore = Store<string, FlowState, any, FlowActions>