Code Generation for BuilDubs Website Builder

Code Generation for BuilDubs Website Builder

If you have used our BuilDubs Website Builder, you might have wondered how we generate code from the drag-and-drop interface. In this blog post, we will take you through the process of code generation for our website builder.

When you drag and drop elements on the BuilDubs interface, we generate code in real-time. This code can then be copied by opening the Code Sheet.


Image
JSX code generated by BuilDubs

But before we delve into the code generation process, let's understand the structure of the BuilDubs website builder.

BuilDubs Website Builder

The BuilDubs website builder is a drag-and-drop interface that allows you to create websites without writing a single line of code. You can drag elements like text, images, buttons, and more onto the canvas and arrange them as per your requirements.

All of these are rendered as Nodes internally in the builder. These Nodes then make up the NodeTree structure of the website which is used to create the live-preview as well as generate code.

Nodes all the way down


Image
Nodes Everywhere

Everything, from the root container to the smallest text element, is a Node. Each Node has a type which determines the kind of element it represents. Even the props are stored inside the Node object.

Although we can directly access the NodeTree, we retrieve it as a "flattened" json object. This is done for sake of readability and to avoid the complexity of nested objects.

Here's how we access the Flattened NodeTree:

const data = nodeTree.getSerializedNodes()

Here's how the data looks:

{
    "ROOT": {..},
    "LbV6VnYuov": {...},
    "D4GJsWkTyi": {...},
    "zcJt8GYlLO": {...},
    "t7GcNdReUF": {...},
}

We can access eact node by its id like this:

const node = data["LbV6VnYuov"]

Here's the ROOT node as an example from data[ROOT]:

ROOT: {
    "type": {
        "resolvedName": "Container"
    },
    "isCanvas": true,
    "props": {
        "flexDirection": "column",
        "alignItems": "flex-start",
        "justifyContent": "flex-start",
        "fillSpace": "no",
        "padding": [
            "40",
            "40",
            "40",
            "40"
        ],
        "margin": [
            "0",
            "0",
            "0",
            "0"
        ],
        "background": {
            "r": 255,
            "g": 255,
            "b": 255,
            "a": 1
        },
        "color": {
            "r": 0,
            "g": 0,
            "b": 0,
            "a": 1
        },
        "shadow": 0,
        "radius": 0,
        "width": "800px",
        "height": "auto"
    },
    "displayName": "Container",
    "custom": {
        "displayName": "App"
    },
    "hidden": false,
    "nodes": [
        "LbV6VnYuov",
        "D4GJsWkTyi",
        "zcJt8GYlLO",
        "t7GcNdReUF",
        "yFl7gm9I2F",
        "v7FKrL6rw5",
        "a14M2Fy1WY"
    ],
    "linkedNodes": {}
}

As you might have guess, the ROOT node is the root container of the website. It has a type of Container and contains other nodes as its children which are in the nodes array.

This is the APP container that you see when creating website in DubsUI. New lets get started on code generation.

Code Generation

We recursively traverse the NodeTree and if a node has children, we generate code for the children recursively or return the code for the current node.

Here's the code for recursive traversal of the tree:

const generateJSX = (node, data, level = 0, imports = new Set()) => {
    const { type, props, nodes, linkedNodes, displayName } = node
    let children = ""
  
    if (nodes && nodes.length > 0) {
      children = nodes
        .map((childId) => generateJSX(data[childId], data, level + 1, imports))
        .join("\n")
    }
}

imports is used to keep track of the components that are used in the code. This is used to generate the import statements at the top of the code.

level is used to keep track of the indentation level of the code.

The function returns the code in form of a string. We then join the code for the children and return the code for the current node.

Here's how we generate code for a Container node:

const generateJSX = (node, data, level = 0, imports = new Set()) => {
    const { type, props, nodes, linkedNodes, displayName } = node
    let children = ""
  
    if (nodes && nodes.length > 0) {
      children = nodes
        .map((childId) => generateJSX(data[childId], data, level + 1, imports))
        .join("\n")
    }
  
    switch (type.resolvedName) {
        case "Container":
          return ` ${indent(level)} <div style={{${containerStyle(
            props,
            level
          )}}} ${indent(level)} > \n${children}\n ${indent(level)} </div>`
        default:
          return ""
    }
}

generateProps is a helper function that generates the props for the node. Its used to provide inline styling for now.

Now, we can just keep extending definitions for different types of nodes, and keep adding them to the Swich-Case statement. Here's how we generate code for a Text node:

case "Text":
    return ` ${indent(level)} <p style={{${textStyle(props, level)}}} ${indent(level)} >${props.text}</p>`

Annnndddd....That's it:

It's that simple...

Ok, its not, I've abstracted away the process of how the styles are generated, how the imports are generated, how the code is formatted, or even how the NodeTree itself is generated. But the core idea is the same.

Future plans

We are planning to add more features to the BuilDubs website builder, like the ability to add custom components, animations, and more. We will keep you updated on our progress.

Hope you enjoyed this blog post. If you have any questions or feedback, feel free to reach out to us.

Happy coding!



The Three Dubs,
DevsTomorrow,
@jayshiai


    Theme

    Presets

    Background

    Custom:

    Primary

    Custom:

    Secondary

    Custom:

    Border

    Custom:

    Mode

    Light
    Dark