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:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useHandle, useHooks, useStore } from '../../composables'
|
||||
import { Position, ValidConnectionFunc } from '../../types'
|
||||
import { NodeIdContextKey } from '~/context'
|
||||
import { NodeId } from '~/context'
|
||||
|
||||
interface HandleProps {
|
||||
id?: string
|
||||
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<HandleProps>(), {
|
||||
|
||||
const store = useStore()
|
||||
const hooks = useHooks()
|
||||
const nodeId = inject(NodeIdContextKey)!
|
||||
const nodeId = inject(NodeId)!
|
||||
|
||||
const handler = useHandle()
|
||||
const onMouseDownHandler = (event: MouseEvent) =>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable'
|
||||
import { useHooks, useStore } from '../../composables'
|
||||
import { Node, NodeType, SnapGrid } from '../../types'
|
||||
import { NodeIdContextKey } from '~/context'
|
||||
import { NodeId } from '~/context'
|
||||
|
||||
interface NodeProps {
|
||||
node: Node
|
||||
@@ -19,7 +19,7 @@ const props = withDefaults(defineProps<NodeProps>(), {
|
||||
|
||||
const store = useStore()
|
||||
const hooks = useHooks()
|
||||
provide(NodeIdContextKey, props.node.id)
|
||||
provide(NodeId, props.node.id)
|
||||
|
||||
const nodeElement = templateRef<HTMLDivElement>('node-element', null)
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ export { default as useKeyPress } from './useKeyPress'
|
||||
export { default as useZoomPanHelper } from './useZoomPanHelper'
|
||||
export { default as useStore } from './useStore'
|
||||
export { default as useWindow } from './useWindow'
|
||||
export { default as useVueFlow } from './useVueFlow'
|
||||
export { default as useVueFlow, initFlow } from './useVueFlow'
|
||||
|
||||
+23
-21
@@ -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'
|
||||
|
||||
// flow event hooks
|
||||
const hooks = (): FlowHooks => {
|
||||
export const createHooks = (): FlowHooks => {
|
||||
return {
|
||||
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
|
||||
elementsRemove: createEventHook<Elements>(),
|
||||
@@ -45,33 +57,23 @@ const hooks = (): FlowHooks => {
|
||||
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
|
||||
}
|
||||
|
||||
return {
|
||||
...eventHooks,
|
||||
bind,
|
||||
const bind = (emit: EmitFunc, hooks: FlowHooks) => {
|
||||
for (const [key, value] of Object.entries(hooks)) {
|
||||
value.on((data: FlowEvents[keyof FlowHooks]) => {
|
||||
emit(key as keyof FlowHooks, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default (emit?: EmitFunc) => {
|
||||
let hooks = inject(Hooks)!
|
||||
if (!hooks) {
|
||||
const store = useStore()
|
||||
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.')
|
||||
else {
|
||||
hooks = createHooks().bind(emit)
|
||||
provide(Hooks, hooks)
|
||||
}
|
||||
hooks = store.hooks
|
||||
if (typeof emit === 'function') bind(emit, hooks)
|
||||
provide(Hooks, hooks)
|
||||
}
|
||||
|
||||
return hooks
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Store } from '~/context'
|
||||
|
||||
export default (options?: Partial<FlowOptions>) => {
|
||||
let store = inject(Store)!
|
||||
|
||||
if (!store) {
|
||||
if (import.meta.env.DEV) console.warn('store context not found; creating default store')
|
||||
store = useFlowStore({
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import useHooks from './useHooks'
|
||||
import useStore from './useStore'
|
||||
import { FlowInstance } from '~/types'
|
||||
import { EmitFunc } from '~/types'
|
||||
|
||||
export default () => {
|
||||
export const initFlow = (emit: EmitFunc) => {
|
||||
const store = useStore()
|
||||
const hooks = useHooks()
|
||||
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.`)
|
||||
})
|
||||
const hooks = useHooks(emit)
|
||||
|
||||
return {
|
||||
isReady,
|
||||
instance,
|
||||
store,
|
||||
hooks,
|
||||
}
|
||||
}
|
||||
|
||||
export default () => useStore()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties, onBeforeUnmount } from 'vue'
|
||||
import { invoke } from '@vueuse/core'
|
||||
import diff from 'microdiff'
|
||||
import {
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
@@ -12,14 +13,15 @@ import {
|
||||
TranslateExtent,
|
||||
NodeExtent,
|
||||
FlowOptions,
|
||||
FlowEvents,
|
||||
} from '../../types'
|
||||
import { useHooks, useStore, createHooks } from '../../composables'
|
||||
import ZoomPane from '../../container/ZoomPane/ZoomPane.vue'
|
||||
import SelectionPane from '../../container/SelectionPane/SelectionPane.vue'
|
||||
import NodeRenderer from '../../container/NodeRenderer/NodeRenderer.vue'
|
||||
import EdgeRenderer from '../../container/EdgeRenderer/EdgeRenderer.vue'
|
||||
import { DefaultNode, InputNode, OutputNode } from '../../components/Nodes'
|
||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '../../components/Edges'
|
||||
import { initFlow } from '~/composables'
|
||||
|
||||
export interface FlowProps extends FlowOptions {
|
||||
elements: Elements
|
||||
@@ -98,7 +100,9 @@ const props = withDefaults(defineProps<FlowProps>(), {
|
||||
edgeTypesId: '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> = {
|
||||
input: InputNode as NodeType,
|
||||
@@ -113,8 +117,7 @@ const defaultEdgeTypes: Record<string, EdgeType> = {
|
||||
smoothstep: SmoothStepEdge as EdgeType,
|
||||
}
|
||||
|
||||
const store = useStore(props)
|
||||
const hooks = useHooks(emit)
|
||||
const { store, hooks } = initFlow(emit)
|
||||
|
||||
const init = (opts: typeof props) => {
|
||||
store.$state = { ...store.$state, ...opts }
|
||||
@@ -124,12 +127,16 @@ const init = (opts: typeof props) => {
|
||||
store.setTranslateExtent(opts.translateExtent)
|
||||
store.setNodeExtent(opts.nodeExtent)
|
||||
}
|
||||
const elements = useVModel(props, 'elements', emit)
|
||||
|
||||
onBeforeUnmount(() => store?.$dispose())
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
(val) => init(val),
|
||||
(val, oldVal) => {
|
||||
const hasDiff = diff(val, oldVal)
|
||||
if (hasDiff.length > 0) init(val)
|
||||
},
|
||||
{ flush: 'pre', deep: true },
|
||||
)
|
||||
invoke(async () => {
|
||||
@@ -137,16 +144,27 @@ invoke(async () => {
|
||||
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(() => {
|
||||
let types = defaultNodeTypes
|
||||
if (Array.isArray(props.nodeTypes)) props.nodeTypes.forEach((type) => (types[type] = true))
|
||||
else types = { ...types, ...props.nodeTypes }
|
||||
store.$state.nodeTypes = types
|
||||
return types
|
||||
})
|
||||
const edgeTypes = computed(() => {
|
||||
let types = defaultEdgeTypes
|
||||
if (Array.isArray(props.edgeTypes)) props.edgeTypes.forEach((type) => (types[type] = true))
|
||||
else types = { ...types, ...props.edgeTypes }
|
||||
store.$state.edgeTypes = types
|
||||
return types
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
|
||||
import { get, invoke } from '@vueuse/core'
|
||||
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 { clamp, onLoadGetElements, onLoadProject, onLoadToObject } from '../../utils'
|
||||
|
||||
@@ -186,7 +186,7 @@ store.dimensions = {
|
||||
|
||||
invoke(async () => {
|
||||
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),
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
@@ -195,7 +195,8 @@ invoke(async () => {
|
||||
project: onLoadProject(store),
|
||||
getElements: onLoadGetElements(store),
|
||||
toObject: onLoadToObject(store),
|
||||
})
|
||||
}
|
||||
hooks.load.trigger(instance)
|
||||
})
|
||||
|
||||
watch(
|
||||
|
||||
@@ -3,4 +3,4 @@ import { ElementId, FlowHooks, FlowStore } from '~/types'
|
||||
|
||||
export const Store: InjectionKey<FlowStore> = Symbol('store')
|
||||
export const Hooks: InjectionKey<FlowHooks> = Symbol('hooks')
|
||||
export const NodeIdContextKey: InjectionKey<ElementId> = Symbol('NodeIdContext')
|
||||
export const NodeId: InjectionKey<ElementId> = Symbol('nodeId')
|
||||
|
||||
+1
-3
@@ -19,11 +19,9 @@ export {
|
||||
graphPosToZoomedPos,
|
||||
} from './utils/graph'
|
||||
export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
|
||||
export { default as useStore } from './composables/useStore'
|
||||
export { default as useHooks } from './composables/useHooks'
|
||||
export { default as useVueFlow } from './composables/useVueFlow'
|
||||
export { default as useHandle } from './composables/useHandle'
|
||||
export { default as useKeyPress } from './composables/useKeyPress'
|
||||
export { default as useVueFlow } from './composables/useVueFlow'
|
||||
|
||||
export * from './additional-components'
|
||||
export * from './types'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { setActivePinia, createPinia, defineStore, StoreDefinition } from 'pinia'
|
||||
import diff from 'microdiff'
|
||||
import { Edge, FlowState, Node, FlowActions } from '~/types'
|
||||
import { Edge, FlowState, Node, FlowActions, Elements, NodeExtent } from '~/types'
|
||||
import {
|
||||
clampPosition,
|
||||
getDimensions,
|
||||
@@ -21,7 +21,54 @@ type NextElements = {
|
||||
const pinia = createPinia()
|
||||
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)
|
||||
|
||||
return defineStore({
|
||||
@@ -32,51 +79,7 @@ export default function useFlowStore(preloadedState: FlowState): StoreDefinition
|
||||
getters: {},
|
||||
actions: {
|
||||
setElements(elements) {
|
||||
const nextElements: NextElements = {
|
||||
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)
|
||||
|
||||
const { nextNodes, nextEdges } = parseElements(elements, this.nodes, this.edges, this.nodeExtent)
|
||||
this.nodes = nextNodes
|
||||
this.edges = nextEdges
|
||||
},
|
||||
@@ -257,6 +260,11 @@ export default function useFlowStore(preloadedState: FlowState): StoreDefinition
|
||||
this.nodesConnectable = 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
@@ -1,4 +1,5 @@
|
||||
import { ConnectionMode, FlowState } from '~/types'
|
||||
import { createHooks } from '~/composables'
|
||||
|
||||
export const initialState = (): FlowState => ({
|
||||
dimensions: {
|
||||
@@ -6,6 +7,8 @@ export const initialState = (): FlowState => ({
|
||||
height: 0,
|
||||
},
|
||||
transform: [0, 0, 1],
|
||||
nodeTypes: [],
|
||||
edgeTypes: [],
|
||||
elements: [],
|
||||
nodes: [],
|
||||
edges: [],
|
||||
@@ -54,7 +57,10 @@ export const initialState = (): FlowState => ({
|
||||
|
||||
multiSelectionActive: false,
|
||||
|
||||
isReady: false,
|
||||
hooks: createHooks(),
|
||||
|
||||
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-',
|
||||
})
|
||||
|
||||
export { default as useFlowStore } from './useFlowStore'
|
||||
export { default as useFlowStore } from './flowStore'
|
||||
|
||||
@@ -22,4 +22,5 @@ export interface FlowActions {
|
||||
updateSize: (size: Dimensions) => void
|
||||
setConnectionNodeId: (payload: SetConnectionId) => void
|
||||
setInteractive: (isInteractive: boolean) => void
|
||||
addElements: (elements: Elements) => void
|
||||
}
|
||||
|
||||
+8
-2
@@ -2,17 +2,20 @@ import { Store } from 'pinia'
|
||||
import { Dimensions, ElementId, Elements, Rect, SelectionRect, SnapGrid, Transform, XYPosition } from './types'
|
||||
import { HandleType } from './handle'
|
||||
import { ConnectionMode, OnConnectEndFunc, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc } from './connection'
|
||||
import { Edge } from './edge'
|
||||
import { Node, NodeExtent, TranslateExtent } from './node'
|
||||
import { Edge, EdgeType } from './edge'
|
||||
import { Node, NodeExtent, NodeType, TranslateExtent } from './node'
|
||||
import { FlowActions } from './actions'
|
||||
import { D3Selection, D3Zoom, D3ZoomHandler } from './panel'
|
||||
import { FlowHooks } from '~/types/hooks'
|
||||
|
||||
export interface FlowState {
|
||||
dimensions: Dimensions
|
||||
transform: Transform
|
||||
elements: Elements
|
||||
nodes: Node[]
|
||||
nodeTypes: Record<string, NodeType> | string[]
|
||||
edges: Edge[]
|
||||
edgeTypes: Record<string, EdgeType> | string[]
|
||||
selectedElements?: Elements
|
||||
selectedNodesBbox: Rect
|
||||
|
||||
@@ -50,6 +53,9 @@ export interface FlowState {
|
||||
onConnectStart?: OnConnectStartFunc
|
||||
onConnectStop?: OnConnectStopFunc
|
||||
onConnectEnd?: OnConnectEndFunc
|
||||
|
||||
isReady: boolean
|
||||
hooks: FlowHooks
|
||||
}
|
||||
|
||||
export type FlowStore = Store<string, FlowState, any, FlowActions>
|
||||
|
||||
Reference in New Issue
Block a user