Building It Step By StepAutomatically Center Node

Building It Step By Step

Automatically Center Node

Learn how to automatically center the viewport on a node when relevant changes occur.


Getting Started

Navigate to the corresponding folder:

cd step-9-automatically-center-node

Install the dependencies:

npm install

Then run the project:

npm run dev

Automatically Center Node

When you open the application, you'll notice that after certain changes, such as adding a new element, the viewport automatically centers on the relevant node.

To center the viewport on a specific node, we use the center property, which defines the id of the node to center on, as shown below.

// src/shared/kits/workflow/hooks/use-workflow.ts

// ...

function init(schema: WorkflowSchema): State {
  const workflow = toWorkflow(schema);
  return {
    history: {
      workflows: [workflow],
      pointer: 0,
      current: workflow,
    },
    center: null,
  };
}

export function useWorkflow(schema: WorkflowSchema): [State, Dispatch<Action>] {
  return useReducer(reducer, schema, init);
}

Each call to applyWorkflowChange returns a center property that, if present, specifies the id of the node to center in the viewport. It is used to update the center property in the state, which in turn triggers the viewport adjustment.

// src/shared/kits/workflow/hooks/use-workflow.ts

// ...

function reducer(state: State, action: Action): State {
  switch (action.type) {
    /**
     * Applies all incoming workflow changes. Each change may request the resulting
     * workflow to be saved in the history (`memory`), in which case a new entry
     * is added and any forward history (redo states) is discarded.
     */
    case "applyWorkflowChanges": {
      const workflows: Workflow[] = [...state.history.workflows];
      let current: Workflow = state.history.current;
      let pointer: number = state.history.pointer;
      let center: string | null = null;
      for (const change of action.changes) {
        const output = applyWorkflowChange(change, current);
        current = output.workflow;
        if (output.memory) {
          pointer++;
          workflows.splice(pointer, workflows.length - pointer, {
            ...output.workflow,
            widget: null,
            active: null,
          });
        }
        if (output.center) {
          center = output.center;
        }
      }
      return {
        history: { workflows: workflows, pointer, current },
        center: center,
      };
    }

    // ...
  }
}

// ...

The functionality to center a node in the viewport whenever the center property changes is handled by the useCenter hook, as shown below.

// src/shared/kits/workflow/components/workflow-view.tsx

// ...

export function useCenter(flows: TypedFlows, center: string | null) {
  const { setCenter } = useFlows();
  useEffect(() => {
    if (center) {
      const node = flows.nodes.get(center)!;
      const x = node.positionAbsolute.x + node.size.w / 2;
      const y = node.positionAbsolute.y + node.size.h / 2;
      setCenter(x, y, {
        zoom: constants.flows.center.zoom,
        duration: constants.flows.center.duration,
      });
    }
  }, [flows, center, setCenter]);
}

Within useCenter, we use the useFlows hook, which requires the FlowsProvider to be used higher in the component tree. In our case, it is set in the root layout.

// src/app/layout.tsx

// ...

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="bg-neutral-950">
      <body
        className={`${nunitoSans.variable} ${jetBrainsMono.variable} bg-neutral-950 font-sans`}
      >
        <TooltipProvider>
          <FlowsProvider>{children}</FlowsProvider>
        </TooltipProvider>
      </body>
    </html>
  );
}