Iterating Over a Visual Editor Compiler
Tags: node, typescript, react, web
Published: 2024-07-10 20:15:25

## The Visual Editor
When it comes to creating visual editors for workflows, React Flow stands out as an ideal choice. It offers robust performance, is highly customizable, and facilitates document export. With it, you can build everything from chatbots to complex backends.

## Compiling the Visual
Directly exporting the visual format is not efficient for execution. Therefore, we convert this structure into something more executable. A basic example of the structure would be:

```json
[{prev: null, data: {}, id: id, next: <some id>}]
```
This makes it easy to navigate and filter the objects.

#Data Maintenance
To reverse the "compilation" and maintain node metadata, we use:

```ts
const step: any = {
    data: node.data,
    height: node.height,
    id: node.id,
    position: node.position,
    positionAbsolute: node.positionAbsolute,
    selected: node.selected,
    type: node.type,
    width: node.width,
    prev: another step,
    next: another step
};
```

## [Format Conversion
Below is the function that converts the React Flow format to a custom format:

```ts
function convertToCustomFormat(reactFlowObject: any) {
    const customFormatObject: any = {
        steps: [],
        viewport: reactFlowObject.flow.viewport,
    };

    reactFlowObject.flow.nodes.forEach((node: any) => {
        const step: any = {
            data: node.data,
            height: node.height,
            id: node.id,
            position: node.position,
            positionAbsolute: node.positionAbsolute,
            selected: node.selected,
            type: node.type,
            width: node.width,
        };

        if (node.data.nodeType === "conditionNode" || node.data.nodeType === "pixNode") {
            const trueEdge = reactFlowObject.flow.edges.find(
                (edge: any) => edge.source === node.id && edge.sourceHandle == "a"
            );
            const falseEdge = reactFlowObject.flow.edges.find(
                (edge: any) => edge.source === node.id && edge.sourceHandle == "b"
            );

            step.true = trueEdge ? trueEdge : null;
            step.false = falseEdge ? falseEdge : null;
            step.prev = reactFlowObject.flow.edges.find(
                (edge: any) => edge.target === node.id
            );
        } else {
            step.prev = reactFlowObject.flow.edges.find(
                (edge: any) => edge.target === node.id
            );
            step.next = reactFlowObject.flow.edges.find(
                (edge: any) => edge.source === node.id
            );
        }

        customFormatObject.steps.push(step);
    });

    return customFormatObject;
}
```

## Reconversion to React Flow
To reconvert the custom format back to React Flow, we use the following function:

```ts
function convertToReactFlowFormat(customFormatObject: any) {
    const reactFlowObject: any = {
        flow: {
            nodes: [],
            edges: [],
            viewport: customFormatObject.viewport,
        },
    };

    customFormatObject.steps.forEach((step: any) => {
        const node = {
            data: step.data,
            height: step.height,
            id: step.id,
            position: step.position,
            positionAbsolute: step.positionAbsolute,
            selected: step.selected,
            type: step.type,
            width: step.width,
        };

        reactFlowObject.flow.nodes.push(node);

        if (step.type === "nodeContainer") {
            if (step.prev) {
                reactFlowObject.flow.edges.push({
                    ...step.prev,
                    target: step.id,
                });
            }

            if (step.next) {
                reactFlowObject.flow.edges.push({
                    ...step.next,
                    source: step.id,
                });
            }
        }
    });

    return reactFlowObject;
}
```

## Execution
To execute from the metadata, especially when user inputs are needed, we can use variables that change continuously and can be stored in memory or in a database like Redis. We keep the step ID, the number of steps passed, and metadata for each previous step or input.

This allows us to pause and resume execution easily, maintaining the flow logic intact. In cases where there are multiple outputs, we adapt the function to handle specific actions:

```ts
const step: any = {
    data: node.data,
    
height: node.height,
    id: node.id,
    position: node.position,
    positionAbsolute: node.positionAbsolute,
    selected: node.selected,
    type: node.type,
    width: node.width,
    prev: step,
    outputs: {
        0: step,
        1: step
    }
};
```

This allows for even more detailed and specific management of the constructed flow.

## Introducing the Visitor Pattern
For those looking for a more structured and scalable way to iterate over the elements of a visual editor, the Visitor Pattern can be an intriguing solution. The Visitor Pattern allows you to separate algorithms from the objects on which they operate, making it easier to add new operations without modifying the existing elements.

## What is the Visitor Pattern?
The Visitor Pattern involves creating a visitor interface that declares a set of visit methods for each type of element. Each element class implements an accept method that accepts a visitor, allowing the visitor to perform the required operation.

## Benefits

- Simplified Maintenance: Adding new operations only requires creating a new visitor without needing to modify existing elements.
- Separation of Responsibilities: Operations are clearly separated from the elements, making the code more modular.
- Scalability: Facilitates the addition of new types of elements and operations in an orderly and hassle-free manner.

## Basic Example
Here is a basic example of how the Visitor Pattern can be implemented:

```js
class Visitor {
  visitText(element) {}
  visitImage(element) {}
  visitShape(element) {}
}

class ConcreteVisitor extends Visitor {
  visitText(element) {
    console.log('Processing text element');
  }
  visitImage(element) {
    console.log('Processing image element');
  }
  visitShape(element) {
    console.log('Processing shape element');
  }
}

class Element {
  accept(visitor) {}
}

class TextElement extends Element {
  accept(visitor) {
    visitor.visitText(this);
  }
}

class ImageElement extends Element {
  accept(visitor) {
    visitor.visitImage(this);
  }
}

class ShapeElement extends Element {
  accept(visitor) {
    visitor.visitShape(this);
  }
}

function processElements(elements, visitor) {
  for (let element of elements) {
    element.accept(visitor);
  }
}

const elements = [new TextElement(), new ImageElement(), new ShapeElement()];
const visitor = new ConcreteVisitor();
processElements(elements, visitor);
```

Considering the use of the Visitor Pattern can help keep your code more organized and adaptable to future changes. If you are iterating over a complex set of elements and need to apply multiple operations, this pattern can be an excellent choice.

__This article was built based on the [BatAnBot](https://batanbot.vercel.app/) project and the recent technical challenge by [Vom](https://www.linkedin.com/company/vomdecision).__