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>
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 -2
View File
@@ -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)
+1 -1
View File
@@ -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
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'
// 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
+1
View File
@@ -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({
+5 -12
View File
@@ -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()
+23 -5
View File
@@ -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>
+4 -3
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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'
+1
View File
@@ -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
View File
@@ -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>