Building It Step By StepModel View Controller

Building It Step By Step

Model View Controller

Learn how to create a basic model–view–controller architecture.


Getting Started

Navigate to the corresponding folder:

cd step-2-model-view-controller

Install the dependencies:

npm install

Then run the project:

npm run dev

Model View Controller

The workflow functionality is built around a Model View Controller architecture, where the workflow state, its visual representation, and the logic that connects them are separated into distinct layers. The diagram below illustrates how its core types and functions interact.

Architecture diagram showing the MVC data flow in Workflow Kit. The Workflow model (top-left) transforms into Flows via workflowView() (top-left to bottom-left). User interactions on Flows produce FlowsChange[] via onFlowsChange() (bottom-left to bottom-right). FlowsChange[] is converted to WorkflowChange[] via toWorkflowChanges() (bottom-right to top-right). Finally, WorkflowChange[] is applied back to Workflow via applyWorkflowChanges() (top-right to top-left), completing the cycle.

Types:

  • Workflow: The current workflow state (Model).
  • Flows: The visual representation of the workflow (View).
  • FlowsChange: An event captured from the visual representation (select, drag, or drop).
  • WorkflowChange: A change that occurred in the workflow.

Functions:

  • workflowView: Transforms a Workflow into its visual Flows representation.
  • onFlowsChange: Triggered whenever the user interacts with the Flows by selecting, dragging, or dropping a node, producing FlowsChange objects.
  • toWorkflowChanges: Converts FlowsChange[] into WorkflowChange[], acting as the Controller that bridges the View and the Model.
  • applyWorkflowChanges: Applies the provided WorkflowChange[] against the current workflow to produce an updated Workflow.

Where It All Starts

We can take a look at how this architecture is implemented by going at the following file.

// src/app/page.tsx

// ...

export function initialWorkflow(): Workflow {
  return {
    paths: { left: [], right: [] },
    items: [
      {
        id: "A",
        selected: false,
        position: { x: 200, y: 0 },
      },
    ],
  };
}

export default function Home() {
  const [workflow, setWorkflow] = useState(() => initialWorkflow());

  const onWorkflowChange = useCallback((changes: WorkflowChange[]) => {
    setWorkflow((workflow) => applyWorkflowChanges(changes, workflow));
  }, []);

  return (
    <div className="flex h-screen flex-col bg-neutral-900">
      <WorkflowView workflow={workflow} onWorkflowChange={onWorkflowChange} />
    </div>
  );
}

The useState hook returns a workflow that is passed to WorkflowView. Whenever the workflow needs to update, onWorkflowChange is called to apply the required changes.

The Workflow View

The code of the WorkflowView component can be found in the following file.

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

// ...

interface WorkflowViewProps {
  workflow: Workflow;
  onWorkflowChange: (changes: WorkflowChange[]) => void;
}

export function WorkflowView({
  workflow,
  onWorkflowChange,
}: WorkflowViewProps) {
  const flows = useMemo(
    () => workflowView(workflow, onWorkflowChange),
    [workflow, onWorkflowChange],
  );

  const onFlowsChange = useCallback(
    (changes: TypedFlowsChange[]) => {
      const array = toWorkflowChanges(changes, workflow, flows);
      if (array.length) onWorkflowChange(array);
    },
    [onWorkflowChange, workflow, flows],
  );

  return (
    <FlowsView
      flows={flows}
      onFlowsChange={onFlowsChange}
      nodeTypes={nodeTypes}
      edgeTypes={edgeTypes}
    />
  );
}

The workflow prop is the model, holding the state; workflowView serves as the view, transforming that state into the visual representation; and toWorkflowChanges functions as the controller, mapping user interactions into updates to the workflow.

Select, Drag and Drop

Whenever a node is selected, dragged, or dropped, the onFlowsChange callback is triggered with FlowsChange objects describing the events.

Below is an example of a FlowsChange object.

{
  "type": "select",
  "node": {
    "id": "A/item",
    "type": "component",
    "entity": {
      "type": "item",
      "meta": {
        "type": "item",
        "id": "A"
      },
      "data": {}
    },
    "...": "..."
  },
  "selected": true
}

As shown, the object includes the node with the data we defined, in this case its type and id.

Workflow Changes

The properties of the FlowsChange object guide a chain of dispatcher functions toward the final handler, which is responsible for creating the WorkflowChange objects.

To Workflow Changes

These dispatcher functions are built using the zet utility function, as shown in this file.

// src/shared/kits/workflow/mvc/controller/item/index.ts

// ...

interface Zet {
  object: ItemChange;
  nested: [];
  filter: ["type"];
  params: [Workflow, TypedFlows];
  return: WorkflowChange[];
}

const dispatch = zet<Zet>([], ["type"], {
  select: select,
  drag: drag,
  drop: () => [],
});

export function item(
  change: ItemChange,
  workflow: Workflow,
  flows: TypedFlows,
): WorkflowChange[] {
  return dispatch(change, workflow, flows);
}

export type SelectChange = Refine<{
  object: ItemChange;
  nested: [];
  filter: ["type"];
  narrow: "select";
}>;

export type DragChange = Refine<{
  object: ItemChange;
  nested: [];
  filter: ["type"];
  narrow: "drag";
}>;

For an in-depth explanation of how this utility works, visit the following link.