DEV Community

Cover image for Connector Extension for the Frontend Part of DotApp PHP framework (dotapp.js): Seamlessly Connect Nodes in Just a Few Steps
DotApp PHP Framework
DotApp PHP Framework

Posted on

Connector Extension for the Frontend Part of DotApp PHP framework (dotapp.js): Seamlessly Connect Nodes in Just a Few Steps

Better to see it once than hear about it a hundred times. Here's the video:

This article assumes you are already familiar with the DotApp framework. If not, you can explore it at https://dotapp.dev, where you'll find documentation and usage examples. To avoid explaining the framework setup in every article, I’ll assume you’ve checked it out. So, let’s dive right in.

Since I assume you’ve already worked with the framework, all these concepts should be familiar. Let’s get started. Create a new module—name it whatever you like. Then, create a controller.

In the module’s init file, add a route that points to your controller and a method, let’s say index(). In this method, use the renderer to create a view in your module’s /view/ folder, for example, index.view.php. Render this view in the index() method.

Here’s an example:

public static function index($request, Renderer $renderer) {
    return $renderer->module(self::modulename())->setView("index")->renderView();
}
Enter fullscreen mode Exit fullscreen mode

Now, here comes the magic.

The core frontend library is built into the framework and accessible via .htaccess at:

<script src="/assets/dotapp/dotapp.js"></script>
Enter fullscreen mode Exit fullscreen mode

The connector is available at:

<script src="/assets/dotapp/connector.js"></script>
Enter fullscreen mode Exit fullscreen mode

Once the connector is successfully registered, it triggers the dotapp-connector event. This ensures no issues arise if a user incorrectly orders scripts or if a script loads slowly.

Now, a bit of theory on how to load libraries robustly, preparing for potential errors:

<script>
    (function() {
        function runMe($dotapp) {
            // Your code here
        }

        if (window.$dotapp && window.$dotapp().isSet(window.$dotapp().connector)) {
            runMe(window.$dotapp);
        } else {
            window.addEventListener('dotapp-connector', function() {
                runMe(window.$dotapp);
            }, { once: true });
        }
    })();
</script>
Enter fullscreen mode Exit fullscreen mode

As you can see, the runMe function only runs if dotapp is loaded and the connector extension is initialized. If not, it sets a listener to wait for initialization. This is a good practice I recommend following when working with the connector. If you only need the dotapp library, replace the listener with the dotapp event.

That’s enough on best practices. Now, the exciting part. Copy the following code into the index view of your module. This is the code shown in the video demonstrating how it works.

Here’s the code:

<!DOCTYPE html>
<html lang="sk">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Drag-and-Drop Connectable Elements with Multiple Ports</title>
  <style>
    #vysledok {
        color:rgb(72, 255, 0);
        font-size: 2rem;
    }

    :root {
      --primary-color: #6B48FF;
      --secondary-color: #00DDEB;
      --background-color: #1A1A2E;
      --node-bg: rgba(255, 255, 255, 0.1);
      --node-border: rgba(255, 255, 255, 0.2);
      --port-color: #00DDEB;
      --port-hover: #FF6B6B;
      --connection-color: #FFFFFF;
      --temp-connection-color: rgba(255, 255, 255, 0.5);
      --text-color: #E0E0E0;
      --shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
      --glow: 0 0 10px rgba(107, 72, 255, 0.5);
    }

    body {
      background: linear-gradient(135deg, #1A1A2E 0%, #16213E 100%);
      font-family: 'Inter', sans-serif;
      color: var(--text-color);
      margin: 0;
      padding: 20px;
      display: flex;
      flex-direction: row;
      gap: 20px;
      min-height: 100vh;
    }

    #container {
      display: flex;
      flex-direction: row;
      gap: 20px;
      width: 100%;
      /*max-width: 1200px;*/
    }

    #sidebar .node {
      position: relative;
      width: 20%;
      height: 130px;
      margin: 10px auto;
      cursor: grab;
      background: var(--node-bg);
      border: 1px solid var(--node-border);
      border-radius: 12px;
      backdrop-filter: blur(8px);
      box-shadow: var(--shadow);
      transition: transform 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease;
    }

    #sidebar .node:active {
      cursor: grabbing;
    }

    #sidebar .node:hover {
      transform: translateY(-4px);
      box-shadow: var(--shadow), var(--glow);
      opacity: 1;
    }

    #canvas {
      position: relative;
      width: 78%;
      height: 600px;
      background: rgba(255, 255, 255, 0.05);
      border: 1px solid var(--node-border);
      border-radius: 16px;
      overflow: auto;
      resize: both;
      min-width: 400px;
      min-height: 300px;
      box-shadow: var(--shadow);
      backdrop-filter: blur(10px);
      transition: all 0.3s ease;
    }

    #canvas:hover {
      box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4);
    }

    .node {
      position: absolute;
      width: 160px;
      height: 130px;
      background: var(--node-bg);
      border: 1px solid var(--node-border);
      border-radius: 12px;
      backdrop-filter: blur(8px);
      user-select: none;
      padding: 0;
      box-shadow: var(--shadow);
      transition: transform 0.3s ease, box-shadow 0.3s ease;
    }

    .node:hover {
      transform: translateY(-4px);
      box-shadow: var(--shadow), var(--glow);
    }

    .node-header {
      background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
      color: var(--text-color);
      padding: 10px;
      font-weight: 600;
      border-top-left-radius: 12px;
      border-top-right-radius: 12px;
      cursor: move;
      text-align: center;
      font-size: 14px;
      letter-spacing: 0.5px;
      text-transform: uppercase;
    }

    .content {
      padding: 12px;
      font-size: 12px;
      color: var(--text-color);
      overflow: auto;
      height: calc(100% - 34px);
      display: flex;
      flex-direction: column;
      gap: 8px;
    }

    .content input,
    .content select,
    .content button {
      background: rgba(255, 255, 255, 0.1);
      border: 1px solid var(--node-border);
      border-radius: 8px;
      color: var(--text-color);
      padding: 6px;
      font-size: 12px;
      transition: all 0.3s ease;
    }

    .content input:focus,
    .content select:focus,
    .content button:hover {
      background: rgba(255, 255, 255, 0.2);
      border-color: var(--secondary-color);
      outline: none;
    }

    .port {
      position: absolute;
      width: 14px;
      height: 14px;
      background: var(--port-color);
      border-radius: 50%;
      cursor: pointer;
      transition: transform 0.3s ease, background 0.3s ease;
      box-shadow: 0 0 8px rgba(0, 221, 235, 0.5);
    }

    .port:hover {
      background: var(--port-hover);
      transform: scale(1.3);
      box-shadow: 0 0 12px rgba(255, 107, 107, 0.7);
    }

    .port.input {
      left: -7px;
      transform: translateY(-50%);
    }

    .port.output {
      right: -7px;
      transform: translateY(-50%);
    }

    svg {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
    }

    .connection {
      stroke: var(--connection-color);
      stroke-width: 3;
      fill: none;
      stroke-linecap: round;
      transition: stroke 0.3s ease;
    }

    .connection:hover {
      stroke: var(--secondary-color);
    }

    .temp-connection {
      stroke: var(--temp-connection-color);
      stroke-width: 3;
      stroke-dasharray: 6, 6;
      fill: none;
      stroke-linecap: round;
    }

    button {
      margin: 12px;
      padding: 10px 20px;
      background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
      color: var(--text-color);
      border: none;
      border-radius: 8px;
      cursor: pointer;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      transitions: all 0.3s ease;
      box-shadow: var(--shadow);
    }

    button:hover {
      background: linear-gradient(90deg, var(--secondary-color), var(--primary-color));
      transform: translateY(-2px);
      box-shadow: var(--shadow), var(--glow);
    }

    button:active {
      transform: translateY(0);
      box-shadow: var(--shadow);
    }

    #output {
      margin: 12px;
      padding: 12px;
      background: rgba(255, 255, 255, 0.1);
      border: 1px solid var(--node-border);
      border-radius: 8px;
      max-height: 200px;
      overflow: auto;
      color: var(--text-color);
      font-size: 12px;
      line-height: 1.5;
      backdrop-filter: blur(8px);
      box-shadow: var(--shadow);
    }

    #sidebar {
      width: 200px;
      background: rgba(255, 255, 255, 0.05);
      border: 1px solid var(--node-border);
      border-radius: 16px;
      padding: 10px;
      box-shadow: var(--shadow);
      backdrop-filter: blur(10px);
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    #sidebar .node {
      width: 160px;
      height: 130px;
      margin: 10px auto;
      cursor: grab;
      background: var(--node-bg);
      border: 1px solid var(--node-border);
      border-radius: 12px;
      backdrop-filter: blur(8px);
      box-shadow: var(--shadow);
      transition: transform 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease;
      position: relative;
    }

    #sidebar .node:active {
      cursor: grabbing;
    }

    #sidebar .node:hover {
      transform: translateY(-4px);
      box-shadow: var(--shadow), var(--glow);
      opacity: 1;
    }

    .close-button {
      position: absolute;
      right: 8px;
      top: 8px;
      cursor: pointer;
      font-size: 16px;
      color: var(--port-hover);
      transition: transform 0.3s ease, color 0.3s ease;
    }

    .close-button:hover {
      transform: scale(1.2);
      color: var(--secondary-color);
    }

    #grid {
        opacity: 1;
        transition: opacity 0.3s ease-in-out;
        pointer-events: none;
    }

    #grid.hidden {
        opacity: 0;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id="sidebar"></div>
    <div id="canvas"></div>
  </div>
  <script src="/assets/dotapp/dotapp.js"></script>
  <script src="/assets/dotapp/connector.js"></script>
  <script>
    (function() {
        function runMe($dotapp) {
            const connector = $dotapp('#canvas').connector({
                grid: true,
                gridX: 20,
                gridY: 20
                /*rules: {
                    'out-number-1': ['in-sum-1', 'in-sum-2', 'in-sum-3', 'in-sum2-1', 'in-sum2-2', 'in-sum2-3'],
                    'out-number-2': ['in-sum-1', 'in-sum-2', 'in-sum-3', 'in-sum2-1', 'in-sum2-2', 'in-sum2-3'],
                    'out-number-3': ['in-sum-1', 'in-sum-2', 'in-sum-3', 'in-sum2-1', 'in-sum2-2', 'in-sum2-3'],
                    'out-sum-1': ['in-display-1','in-sum2-1', 'in-sum2-2', 'in-sum2-3']
                }*/
            });

            const predefinedNodes = [
                {
                    title: 'Number 1',
                    id: 'node-number-1',
                    content: 'Output: 5',
                    inputs: [],
                    outputs: [{ id: 'out-number-1' }],
                    callback: (inputs, outputId) => {
                        console.log('Number 1:', inputs, outputId);
                        return 5;
                    }
                },
                {
                    title: 'Number 2',
                    id: 'node-number-2',
                    content: 'Output: 6',
                    inputs: [],
                    outputs: [{ id: 'out-number-2' }],
                    callback: (inputs, outputId) => {
                        console.log('Number 2:', inputs, outputId);
                        return 6;
                    }
                },
                {
                    title: 'Number 3',
                    id: 'node-number-3',
                    content: 'Output: 10',
                    inputs: [],
                    outputs: [{ id: 'out-number-3' }],
                    callback: (inputs, outputId) => {
                        console.log('Number 3:', inputs, outputId);
                        return 10;
                    }
                },
                {
                    title: 'Number 4',
                    id: 'node-number-4',
                    content: '<input type="text" value="8">',
                    inputs: [],
                    outputs: [{ id: 'out-number-4' }],
                    callback: (inputs, outputId) => {
                        const input = $dotapp("#node-number-4 INPUT");
                        const element = $dotapp(input).first(false);
                        if (!element.__listenerAdded) {
                            element.__listenerAdded = true;
                            $dotapp(input).on("change", function() {
                                connector.trigger("connectionchange", {});
                            });
                        }                        
                        const result = input.val();
                        if (result === "" || result === null || result === undefined) {
                            return connector.halt('Empty input');
                        }

                        // Verify if the result is a valid number (integer or decimal)
                        const isValidNumber = !isNaN(result) && /^-?\d*\.?\d+$/.test(result);
                        if (!isValidNumber) {
                            return connector.halt('Invalid number or contains letters');
                        }
                        return parseFloat(result);
                    }
                },
                {
                    title: 'Adder',
                    id: 'node-sum',
                    content: 'Adds inputs',
                    inputs: [
                        { id: 'in-sum-1', required: true },
                        { id: 'in-sum-2', required: true },
                        { id: 'in-sum-3', required: false }
                    ],
                    outputs: [{ id: 'out-sum-1' }],
                    callback: (inputs, outputId) => {
                        console.log('Adder:', inputs, outputId);
                        // Check if any required inputs (in-sum-1, in-sum-2) are missing or halted
                        const requiredInputs = inputs.slice(0, 2); // First two inputs are required
                        if (requiredInputs.some(input => input.value === null || connector.isConnectorHalt(input.value))) {
                            return connector.halt('missing_required_input');
                        }
                        // Sum all provided inputs (including optional in-sum-3 if connected)
                        const validInputs = inputs.filter(input => input.value !== null && !connector.isConnectorHalt(input.value));
                        return validInputs.reduce((sum, input) => sum + input.value, 0);
                    }
                },
                {
                    title: 'Adder2',
                    id: 'node-sum2',
                    content: 'Adds inputs',
                    inputs: [
                        { id: 'in-sum2-1', required: true },
                        { id: 'in-sum2-2', required: true },
                        { id: 'in-sum2-3', required: false }
                    ],
                    outputs: [{ id: 'out-sum2-1' }],
                    callback: (inputs, outputId) => {
                        console.log('Adder:', inputs, outputId);
                        // Check if any required inputs (in-sum-1, in-sum-2) are missing or halted
                        const requiredInputs = inputs.slice(0, 2); // First two inputs are required
                        if (requiredInputs.some(input => input.value === null || connector.isHalted(input.value))) {
                            return connector.halt('missing_required_input');
                        }
                        // Sum all provided inputs (including optional in-sum-3 if connected)
                        const validInputs = inputs.filter(input => input.value !== null && !connector.isConnectorHalt(input.value));
                        return validInputs.reduce((sum, input) => sum + input.value, 0);
                    }
                },
                {
                    title: 'Multiplier',
                    id: 'node-adder',
                    content: 'Multiplies inputs',
                    inputs: [
                        { id: 'in-adder-1', required: true },
                        { id: 'in-adder-2', required: true }
                    ],
                    outputs: [{ id: 'out-adder-1' }],
                    callback: (inputs, outputId) => {
                        console.log('Multiplier:', inputs, outputId);
                        // Check if any required inputs (in-multiplier-1, in-multiplier-2) are missing or halted
                        const requiredInputs = inputs.slice(0, 2); // First two inputs are required
                        if (requiredInputs.some(input => input.value === null || connector.isHalted(input.value))) {
                            return connector.halt('missing_required_input');
                        }
                        // Multiply all provided inputs
                        const validInputs = inputs.filter(input => input.value !== null && !connector.isConnectorHalt(input.value));
                        return validInputs.reduce((product, input) => product * input.value, 1);
                    }
                },
                {
                    title: 'Display LIVE',
                    id: 'node-display2',
                    content: 'Result: <b id="vysledok">X</b>',
                    inputs: [{ id: 'in-display-2', required: true }],
                    outputs: [],
                    callback: (inputs, outputId) => {
                        if (inputs[0].value === null || connector.isHalted(inputs[0].value)) {
                            $dotapp('#node-display2 #vysledok').html("X");
                            return connector.halt('missing_required_input');
                        }
                        if (inputs[0].value === null) inputs[0].value = "X";
                        $dotapp('#node-display2 #vysledok').html(inputs[0].value);
                        return inputs[0].value;
                    }
                }
            ];

            connector.nodes({
                sidebar: '#sidebar',
                nodes: predefinedNodes,
                autoLoad: false // Changed to false to prevent auto-loading nodes onto canvas
            });

            // Example of how to store state
            function saveState() {
                const state = connector.save();
                document.getElementById('output').textContent = JSON.stringify(state, null, 2);
                localStorage.setItem('connectorState', JSON.stringify(state));
            }

            // Example of how to laod saved state
            function loadState() {
                const savedState = localStorage.getItem('connectorState');
                let state;
                if (savedState) {
                    state = JSON.parse(savedState);
                } 
                state.nodes.forEach(node => {
                    node.callback = predefinedNodes.find(n => n.id === node.originalConfig.id)?.callback || (() => null);
                });
                connector.load(state);
                document.getElementById('output').textContent = 'State loaded';
            }

            // Example of how to get route if you want somehow store it or pass it to backend for further processing
            function getRoute() {
                console.log('Connections:', connector.getConnections());
                console.log('Nodes:', connector.getNodes());
                const result = connector.route('node-display');
                document.getElementById('output').textContent = result ? JSON.stringify(result.path, null, 2) : 'Route does not exist';
            }

            // Example of how to run route in this example it is route of node-display2 
            function runRoute() {
                console.log('Running route...');
                const result = connector.route('node-display2');
                if (result) {
                    const output = result.run();
                    console.log('Route output:', output);
                    document.getElementById('output').textContent = connector.isConnectorHalt(output) ? `Halted: ${output.reason}` : `Result: ${output}`;
                } else {
                    document.getElementById('output').textContent = 'Route does not exist';
                }
            }

            // Example of how to make autocalculation on routes changes
            connector.on("connectionchange", (detail) => {
                console.log('SPUSTENY CONNECTION CHANGE:', detail);
                const result = connector.route('node-display2');
                if (result) {
                    const output = result.run();
                }
            });
        }

        if (window.$dotapp && window.$dotapp().isSet(window.$dotapp().connector)) {
            runMe(window.$dotapp);
        } else {
            window.addEventListener('dotapp-connector', function() {
                runMe(window.$dotapp);
            }, { once: true });
        }
    })();
</script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The code provides enough examples to understand how to create nodes, their types, and how to define them easily. Experiment with this code and modify it to better understand how it works.

That’s all for today’s article, aimed at advanced users already familiar with the DotApp framework.

Previous Articles on the DotApp PHP Framework:

As you can see, the DotApp Framework is a truly high-quality, secure, and robust framework, easily extensible and built on the philosophy of modularity—create once, reuse in any DotApp project.

Top comments (0)