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();
}
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>
The connector is available at:
<script src="/assets/dotapp/connector.js"></script>
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>
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>
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:
- Building a Reusable Users Module with DotApp PHP Framework (+ 2FA)
- How to Send Emails and Save Them in the Sent Folder with 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)