feat(svelte): add SvelteFlowProvider and useSvelteFlow hook

This commit is contained in:
moklick
2023-03-08 20:17:33 +01:00
parent 71cdc0ff02
commit 7bd8032ea4
19 changed files with 202 additions and 46 deletions

View File

@@ -3,7 +3,8 @@
const routes = [
'overview',
'stress'
'stress',
'usesvelteflow'
];
const onChange = (event: Event) => {

View File

@@ -24,7 +24,7 @@
export let selected: $$Props['selected'] = false;
export let label: $$Props['label'] = undefined;
const { edgeTypes, edges } = useStore();
const { edges, edgeTypes } = useStore();
const dispatch = createEventDispatcher();
const edgeComponent: typeof SvelteComponentTyped<EdgeProps> = $edgeTypes[type!] || BezierEdge;

View File

@@ -5,13 +5,13 @@
import type { KeyHandlerProps } from './types';
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
const { selectionKeyPressed, deleteKeyPressed } = useStore();
type $$Props = KeyHandlerProps;
export let selectionKey: $$Props['selectionKey'] = 'Shift';
export let deleteKey: $$Props['deleteKey'] = 'Backspace';
const { selectionKeyPressed, deleteKeyPressed } = useStore();
function isKeyObject(key?: KeyDefinition): key is KeyDefinitionObject {
return typeof key === 'object';
}

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import { setContext } from 'svelte';
import { createStore, key } from '$lib/store';
const store = createStore();
setContext(key, {
getStore: () => store
});
</script>
<slot />

View File

@@ -0,0 +1,2 @@
export { default as SvelteFlowProvider } from './SvelteFlowProvider.svelte';
export type { SvelteFlowProviderProps } from './types';

View File

@@ -0,0 +1,6 @@
import type { SvelteFlowProps } from '$lib/container/SvelteFlow/types';
export type SvelteFlowProviderProps = Pick<
SvelteFlowProps,
'nodes' | 'edges' | 'fitView' | 'nodeTypes'
>;

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { getPositionWithOrigin } from '@reactflow/utils';
import { NodeWrapper } from '$lib/components/NodeWrapper';
import { useStore } from '$lib/store';
import { getPositionWithOrigin } from '@reactflow/utils';
import { Position } from '@reactflow/system';
const { nodes, nodeOrigin, updateNodeDimensions } = useStore();
const resizeObserver: ResizeObserver | null =
typeof ResizeObserver === 'undefined'
? null
@@ -16,7 +16,6 @@
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true
}));
updateNodeDimensions(updates);
});

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { setContext, onMount } from 'svelte';
import { onMount, setContext } from 'svelte';
import cc from 'classcat';
import { key, createStore } from '$lib/store';
import { Zoom } from '$lib/container/Zoom';
import { Pane } from '$lib/container/Pane';
import { Viewport } from '$lib/container/Viewport';
@@ -12,6 +11,7 @@
import { NodeSelection } from '$lib/components/NodeSelection';
import { KeyHandler } from '$lib/components/KeyHandler';
import { ConnectionLine } from '$lib/components/ConnectionLine';
import { createStore, key, useStore } from '$lib/store';
import type { SvelteFlowProps, SvelteFlowEvents } from './types';
type $$Props = SvelteFlowProps;
@@ -40,9 +40,9 @@
nodes,
edges,
fitView,
nodeTypes
});
// we overwrite the context to be able to use the nodes and edges stores passed by the user
setContext(key, {
getStore: () => store
});
@@ -72,7 +72,7 @@
$: {
if (nodeTypes !== undefined) {
store.setNodeTypes(nodeTypes);
store.setNodeTypes(nodeTypes);
}
if (edgeTypes !== undefined) {

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { hasContext } from 'svelte';
import { key } from '$lib/store';
import SvelteFlow from './SvelteFlow.svelte';
import { SvelteFlowProvider } from '$lib/components/SvelteFlowProvider';
import type { SvelteFlowProps } from './types';
type $$Props = SvelteFlowProps;
// @todo how to do this typing right?
const props = $$props as SvelteFlowProps;
</script>
{#if hasContext(key)}
<SvelteFlow {...props}>
<slot />
</SvelteFlow>
{:else}
<SvelteFlowProvider>
<SvelteFlow {...props}>
<slot />
</SvelteFlow>
</SvelteFlowProvider>
{/if}

View File

@@ -1,2 +1,2 @@
export { default as SvelteFlow } from './SvelteFlow.svelte';
export { default as SvelteFlow } from './Wrapper.svelte';
export * from './types';

View File

@@ -0,0 +1,19 @@
import type { ZoomInOut } from '@reactflow/system';
import { useStore } from '$lib/store';
import type { FitViewOptions } from '$lib/types';
export function useSvelteFlow(): {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
fitView: (options?: FitViewOptions) => void;
} {
// how to get the new context here? fit view doesn't work, because the store is not updated (uses old nodes store)
const { zoomIn, zoomOut, fitView } = useStore();
return {
zoomIn,
zoomOut,
fitView
};
}

View File

@@ -3,6 +3,8 @@ import { SvelteFlow } from '$lib/container/SvelteFlow';
export * from '$lib/container/SvelteFlow';
export * from '$lib/container/Panel';
export * from '$lib/components/SvelteFlowProvider';
export * from '$lib/plugins/Controls';
export * from '$lib/plugins/Background';
export * from '$lib/plugins/Minimap';
@@ -10,4 +12,6 @@ export * from '$lib/plugins/Minimap';
export * from '$lib/types';
export * from '$lib/utils';
export * from '$lib/hooks/useSvelteFlow';
export default SvelteFlow;

View File

@@ -2,11 +2,9 @@ import { getContext } from 'svelte';
import { get } from 'svelte/store';
import { zoomIdentity } from 'd3-zoom';
import {
type Transform,
type NodeDragItem,
type NodeDimensionUpdate,
internalsSymbol,
type NodeOrigin,
type ViewportHelperFunctionOptions,
type Connection,
type XYPosition,
@@ -30,30 +28,15 @@ import {
initialStoreState
} from './initial-store';
import type { SvelteFlowStore } from './types';
import type { SvelteFlowProps } from '$lib/container/SvelteFlow/types';
export const key = Symbol();
type CreateStoreProps = {
nodes: SvelteFlowProps['nodes'];
edges: SvelteFlowProps['edges'];
fitView?: boolean;
nodeOrigin?: NodeOrigin;
transform?: Transform;
nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes;
id?: string;
};
type CreateStoreParams = Pick<SvelteFlowStore, 'nodes' | 'edges'> & { fitView?: boolean };
export function createStore({
fitView: fitViewOnInit = false,
nodes,
edges
}: CreateStoreProps): SvelteFlowStore {
export function createStore(params?: CreateStoreParams): SvelteFlowStore {
const store = {
...initialStoreState,
nodes,
edges
...(params !== undefined ? params : {})
};
let fitViewOnInitDone = false;
@@ -105,7 +88,6 @@ export function createStore({
const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
const nextNodes = get(store.nodes).map((node) => {
const update = updates.find((u) => u.id === node.id);
@@ -139,7 +121,7 @@ export function createStore({
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
fitViewOnInitDone =
fitViewOnInitDone || (fitViewOnInit && !!d3Zoom && !!d3Selection && fitView());
fitViewOnInitDone || (!!params?.fitView && !!d3Zoom && !!d3Selection && fitView());
store.nodes.set(nextNodes);
}
@@ -333,7 +315,13 @@ export function createStore({
}
export function useStore(): SvelteFlowStore {
const { getStore } = getContext<{ getStore: () => SvelteFlowStore }>(key);
const store = getContext<{ getStore: () => SvelteFlowStore }>(key);
return getStore();
if (!store) {
throw new Error(
'In order to use useStore you need to wrap your component in a <SvelteFlowProvider />'
);
}
return store.getStore();
}

View File

@@ -16,7 +16,7 @@ import OutputNode from '$lib/components/nodes/OutputNode.svelte';
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
import type { ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted } from '$lib/types';
import type { ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted, Edge, Node } from '$lib/types';
export const initConnectionData = {
nodeId: null,
@@ -40,11 +40,14 @@ export const initialEdgeTypes = {
export const initialStoreState = {
id: writable<string | null>(null),
nodes: writable<Node[]>([]),
edges: writable<Edge[]>([]),
edgesLayouted: readable<EdgeLayouted[]>([]),
height: writable<number>(500),
width: writable<number>(500),
minZoom: writable<number>(0.5),
maxZoom: writable<number>(2),
fitViewOnInit: false,
nodeOrigin: writable<NodeOrigin>([0, 0]),
d3: writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({
zoom: null,

View File

@@ -1,5 +1,7 @@
import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type { HandleProps, HandleType, XYPosition } from '@reactflow/system';
import type { FitViewOptionsBase, HandleProps, HandleType, XYPosition } from '@reactflow/system';
import type { Node } from './nodes';
export type KeyModifier = ShortcutModifierDefinition;
export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
@@ -17,3 +19,5 @@ export type HandleComponentProps = HandleProps & {
class?: string;
style?: string;
};
export type FitViewOptions = FitViewOptionsBase<Node>;

View File

@@ -2,5 +2,21 @@
import { Header } from '../example-components/Header'
</script>
<Header />
<slot />
<div class="app">
<Header />
<div class="flow">
<slot />
</div>
</div>
<style>
.app {
display: flex;
flex-direction: column;
height: 100%;
}
.flow {
flex: 1;
}
</style>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import {
SvelteFlowProvider,
} from '../../lib/index';
import Flow from './Flow.svelte';
</script>
<SvelteFlowProvider>
<Flow />
</SvelteFlowProvider>

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import SvelteFlow, {
Controls,
Background,
BackgroundVariant,
Minimap,
Panel,
createNodes,
createEdges,
useSvelteFlow
} from '../../lib/index';
const nodes = createNodes([
{
id: '1',
type: 'input',
data: { label: 'Input Node' },
position: { x: 150, y: 5 }
},
{
id: '2',
type: 'default',
data: { label: 'Node' },
position: { x: 0, y: 150 }
},
{
id: '3',
type: 'output',
data: { label: 'Output Node' },
position: { x: 300, y: 150 }
}
]);
const edges = createEdges([
{
id: '1-2',
type: 'default',
source: '1',
target: '2',
label: 'Edge Text'
},
{
id: '1-3',
type: 'smoothstep',
source: '1',
target: '3'
}
]);
const svelteFlow = useSvelteFlow();
</script>
<SvelteFlow
{nodes}
{edges}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<Minimap />
<Panel position="top-right">
<button on:click={() => svelteFlow.zoomIn()}>zoom in</button>
<button on:click={() => svelteFlow.zoomOut({ duration: 1000 })}>zoom out transition</button>
<button on:click={() => svelteFlow.fitView()}>fitView</button>
</Panel>
</SvelteFlow>

View File

@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Selection as D3Selection } from 'd3';
import { zoomIdentity } from 'd3-zoom';
import { boxToRect, clamp, devWarn, getBoundsOfBoxes, getOverlappingArea, rectToBox } from './utils';
@@ -11,10 +10,11 @@ import {
type XYPosition,
type Rect,
type NodeOrigin,
BaseNode,
BaseEdge,
FitViewParamsBase,
FitViewOptionsBase,
type BaseNode,
type BaseEdge,
type FitViewParamsBase,
type FitViewOptionsBase,
type D3SelectionInstance,
} from '@reactflow/system';
export const isEdgeBase = <NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>(
@@ -304,7 +304,7 @@ export const getTransformForBounds = (
return [x, y, clampedZoom];
};
export const getD3Transition = (selection: D3Selection<Element, unknown, null, undefined>, duration = 0) => {
export const getD3Transition = (selection: D3SelectionInstance, duration = 0) => {
return selection.transition().duration(duration);
};