WIP: migrated store and useSvelteFlow hook to signals & solved state propagation for provider

This commit is contained in:
peterkogo
2024-12-06 13:53:48 +01:00
parent ed1ead7b5a
commit f29898f0bd
49 changed files with 1616 additions and 1137 deletions
@@ -1,8 +1,8 @@
<script lang="ts">
import { onMount, hasContext } from 'svelte';
import { get } from 'svelte/store';
import { onMount, getContext, setContext } from 'svelte';
import cc from 'classcat';
import { ConnectionMode, PanOnScrollMode } from '@xyflow/system';
import { PanOnScrollMode } from '@xyflow/system';
import { Zoom } from '$lib/container/Zoom';
import { Pane } from '$lib/container/Pane';
@@ -14,78 +14,28 @@
import { KeyHandler } from '$lib/components/KeyHandler';
import { ConnectionLine } from '$lib/components/ConnectionLine';
import { Attribution } from '$lib/components/Attribution';
import { key, useStore, createStoreContext } from '$lib/store';
import { key, createStore } from '$lib/store';
import type { SvelteFlowProps } from './types';
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
import { type ProviderContext, type ContainerSignals, type StoreContext } from '$lib/store/types';
let {
id = '1',
nodes,
edges,
fitView,
fitViewOptions,
minZoom,
maxZoom,
initialViewport,
viewport,
nodeTypes,
edgeTypes,
style,
class: className,
width,
height,
proOptions,
selectionKey,
selectionMode,
deleteKey,
panActivationKey,
multiSelectionKey,
zoomActivationKey,
nodesDraggable,
nodesConnectable,
nodeDragThreshold,
elementsSelectable,
snapGrid,
deleteKey,
connectionRadius,
connectionLineType,
connectionMode = ConnectionMode.Strict,
connectionLineStyle = '',
connectionLineContainerStyle = '',
onMoveStart,
onMove,
onMoveEnd,
isValidConnection,
translateExtent,
nodeExtent,
onlyRenderVisibleElements,
panOnScrollMode = PanOnScrollMode.Free,
preventScrolling = true,
zoomOnScroll = true,
zoomOnDoubleClick = true,
zoomOnPinch = true,
panOnScroll = false,
panOnDrag = true,
selectionOnDrag,
autoPanOnConnect = true,
autoPanOnNodeDrag = true,
onerror,
ondelete,
onedgecreate,
attributionPosition,
proOptions,
defaultEdgeOptions,
width,
height,
colorMode = 'light',
onconnect,
onconnectstart,
onconnectend,
onbeforedelete,
oninit,
nodeOrigin,
paneClickDistance = 1,
nodeClickDistance = 1,
defaultMarkerColor = '#b1b1b7',
style,
class: className,
onMoveStart,
onMoveEnd,
onMove,
connectionLine,
children,
onnodeclick,
onnodecontextmenu,
onnodedrag,
@@ -102,135 +52,74 @@
onedgemouseleave,
onpaneclick,
onpanecontextmenu,
...rest
panOnScrollMode = PanOnScrollMode.Free,
preventScrolling = true,
zoomOnScroll = true,
zoomOnDoubleClick = true,
zoomOnPinch = true,
panOnScroll = false,
panOnDrag = true,
selectionOnDrag = true,
defaultEdgeOptions,
connectionLineContainerStyle = '',
connectionLineStyle = '',
attributionPosition,
children,
...props
}: SvelteFlowProps = $props();
let domNode = $state<HTMLDivElement>();
let clientWidth = $state<number>();
let clientHeight = $state<number>();
// svelte-ignore perf_avoid_inline_class
const container: ContainerSignals = new (class {
domNode = $state<HTMLDivElement>();
width = $state<number | undefined>(width);
height = $state<number | undefined>(height);
})();
const initViewport = $viewport || initialViewport;
const store = createStore({
props,
container
});
const { viewport } = store;
const initialViewport = props.initialViewport ?? get(viewport);
//TODO SVELTE5
store.syncNodeStores(props.nodes);
store.syncEdgeStores(props.edges);
store.syncViewport(props.viewport);
const store = hasContext(key)
? useStore()
: createStoreContext({
nodes: get(nodes),
edges: get(edges),
width,
height,
fitView,
nodeOrigin,
nodeExtent
});
const providerContext = getContext<ProviderContext>(key);
if (providerContext) {
providerContext.setStore(store);
}
setContext(key, {
provider: false,
getStore() {
return store;
}
} satisfies StoreContext);
onMount(() => {
store.width.set(clientWidth!);
store.height.set(clientHeight!);
store.domNode.set(domNode!);
store.syncNodeStores(nodes);
store.syncEdgeStores(edges);
store.syncViewport(viewport);
if (fitView !== undefined) {
store.fitViewOnInit.set(fitView);
}
if (fitViewOptions) {
store.fitViewOptions.set(fitViewOptions);
}
updateStore(store, {
nodeTypes,
edgeTypes,
minZoom,
maxZoom,
translateExtent,
paneClickDistance
});
return () => {
store.reset();
};
});
// Update width & height on resize
$effect.pre(() => {
if (clientWidth !== undefined && clientHeight !== undefined) {
store.width.set(clientWidth);
store.height.set(clientHeight);
}
});
// Call oninit once when flow is intialized
const { initialized } = store;
let onInitCalled = $state(false);
$effect.pre(() => {
if (!onInitCalled && $initialized) {
oninit?.();
onInitCalled = true;
}
});
// this updates the store for simple changes
// where the prop names equals the store name
$effect(() => {
const updatableProps: UpdatableStoreProps = {
flowId: id,
connectionLineType,
connectionRadius,
selectionMode,
snapGrid,
defaultMarkerColor,
nodesDraggable,
nodesConnectable,
elementsSelectable,
onlyRenderVisibleElements,
isValidConnection,
autoPanOnConnect,
autoPanOnNodeDrag,
onerror,
ondelete,
onedgecreate,
connectionMode,
nodeDragThreshold,
onconnect,
onconnectstart,
onconnectend,
onbeforedelete,
nodeOrigin
};
updateStoreByKeys(store, updatableProps);
});
$effect(() => {
updateStore(store, {
nodeTypes,
edgeTypes,
minZoom,
maxZoom,
translateExtent,
paneClickDistance
});
});
let colorModeClass = $derived(useColorModeClass(colorMode));
let colorModeClass = $derived(useColorModeClass(store.colorMode));
</script>
<div
bind:this={domNode}
bind:clientWidth
bind:clientHeight
bind:this={container.domNode}
bind:clientWidth={container.width}
bind:clientHeight={container.height}
style:width
style:height
{style}
class={cc(['svelte-flow', className, $colorModeClass])}
data-testid="svelte-flow__wrapper"
{...rest}
{...props}
role="application"
>
<KeyHandler
{store}
{selectionKey}
{deleteKey}
{panActivationKey}
@@ -238,7 +127,8 @@
{zoomActivationKey}
/>
<Zoom
initialViewport={initViewport}
{store}
{initialViewport}
{onMoveStart}
{onMove}
{onMoveEnd}
@@ -251,16 +141,17 @@
{panOnDrag}
{paneClickDistance}
>
<Pane {nodes} {edges} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
<ViewportComponent>
<Pane {store} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
<ViewportComponent viewport={$viewport}>
<EdgeRenderer
{store}
{onedgeclick}
{onedgecontextmenu}
{onedgemouseenter}
{onedgemouseleave}
{defaultEdgeOptions}
/>
<ConnectionLine
{store}
containerStyle={connectionLineContainerStyle}
style={connectionLineStyle}
{connectionLine}
@@ -268,6 +159,7 @@
<div class="svelte-flow__edgelabel-renderer"></div>
<div class="svelte-flow__viewport-portal"></div>
<NodeRenderer
{store}
{nodeClickDistance}
{onnodeclick}
{onnodecontextmenu}
@@ -279,6 +171,7 @@
{onnodedragstop}
/>
<NodeSelection
{store}
{onselectionclick}
{onselectioncontextmenu}
{onnodedrag}
@@ -188,6 +188,10 @@ export type SvelteFlowProps = NodeEvents &
* @default 'full'
*/
selectionMode?: SelectionMode;
/**
* Controls if nodes should be automatically selected when being dragged
*/
selectNodesOnDrag: boolean;
/** Grid all nodes will snap to
* @example [20, 20]
*/
@@ -1,91 +0,0 @@
import type { Writable } from 'svelte/store';
import type { CoordinateExtent } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
import type { EdgeTypes, NodeTypes } from '$lib/types';
// this is helper function for updating the store
// for props where we need to call a specific store action
export function updateStore(
store: SvelteFlowStore,
{
nodeTypes,
edgeTypes,
minZoom,
maxZoom,
translateExtent,
paneClickDistance
}: {
nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes;
minZoom?: number;
maxZoom?: number;
translateExtent?: CoordinateExtent;
paneClickDistance?: number;
}
) {
if (nodeTypes !== undefined) {
store.setNodeTypes(nodeTypes);
}
if (edgeTypes !== undefined) {
store.setEdgeTypes(edgeTypes);
}
if (minZoom !== undefined) {
store.setMinZoom(minZoom);
}
if (maxZoom !== undefined) {
store.setMaxZoom(maxZoom);
}
if (translateExtent !== undefined) {
store.setTranslateExtent(translateExtent);
}
if (paneClickDistance !== undefined) {
store.setPaneClickDistance(paneClickDistance);
}
}
const getKeys = <T extends object>(obj: T) => Object.keys(obj) as Array<keyof T>;
type UnwrapWritable<T> = T extends Writable<infer U> ? U : T;
// @todo there must be a better way to define the types here..
export type UpdatableStoreProps = {
flowId?: UnwrapWritable<SvelteFlowStore['flowId']>;
connectionLineType?: UnwrapWritable<SvelteFlowStore['connectionLineType']>;
connectionRadius?: UnwrapWritable<SvelteFlowStore['connectionRadius']>;
selectionMode?: UnwrapWritable<SvelteFlowStore['selectionMode']>;
snapGrid?: UnwrapWritable<SvelteFlowStore['snapGrid']>;
defaultMarkerColor?: UnwrapWritable<SvelteFlowStore['defaultMarkerColor']>;
nodesDraggable?: UnwrapWritable<SvelteFlowStore['nodesDraggable']>;
nodesConnectable?: UnwrapWritable<SvelteFlowStore['nodesConnectable']>;
elementsSelectable?: UnwrapWritable<SvelteFlowStore['elementsSelectable']>;
onlyRenderVisibleElements?: UnwrapWritable<SvelteFlowStore['onlyRenderVisibleElements']>;
isValidConnection?: UnwrapWritable<SvelteFlowStore['isValidConnection']>;
autoPanOnConnect?: UnwrapWritable<SvelteFlowStore['autoPanOnConnect']>;
autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>;
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
onerror?: UnwrapWritable<SvelteFlowStore['onerror']>;
ondelete?: UnwrapWritable<SvelteFlowStore['ondelete']>;
onedgecreate?: UnwrapWritable<SvelteFlowStore['onedgecreate']>;
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
onconnect?: UnwrapWritable<SvelteFlowStore['onconnect']>;
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>;
nodeOrigin?: UnwrapWritable<SvelteFlowStore['nodeOrigin']>;
};
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
getKeys(keys).forEach((prop) => {
const update = keys[prop];
if (update !== undefined) {
// @ts-expect-error @todo: how to fix this TS error?
store[prop].set(update);
}
});
}