The fields[] Array — Your Source of Truth
In this modern day, al most all CMS including WordPress and many page builders, form builders etc uses drag and drop interface to build the UI. Have you wondered how it work behind the scene? Today we will explore how drag and drop work in a form builder which is very common. If you understand this, then you can understand how popular form builder plugin works and how Elementor, Gutenberg block editor etc work. Everything in the form builder lives in one JavaScript array. Understanding this is the foundation.
A form builder doesn’t actually move HTML around the page. Instead, it manages a JavaScript array called fields[]. Each item in the array is a plain JavaScript object that describes one form field.
When you drag a field onto the canvas, you add an object to the array. When you delete it, you remove it from the array. The HTML on screen is always just a reflection of what’s in the array.
// Each field is just a plain JavaScript object const field = { id: "field_1", // unique ID type: "text", // what kind of input label: "Full Name", // the visible label placeholder: "e.g. John Smith", required: true, // validation helpText: "" };
Click the buttons below to add fields. Watch how the array grows:
// fields[] is empty let fields = [];
The 3 Drag Events You Need
The browser fires events as you drag. You only need to listen to three of them.
Fires when the user begins dragging. You record what is being dragged.
Fires while hovering over a drop target. You must call
e.preventDefault() here or the drop won’t work.Fires when the user releases. You use the recorded info to add a new field to the array.
// 1. On the palette item — record what's being dragged paletteItem.addEventListener('dragstart', function(e) { draggedType = 'text'; // save to a variable }); // 2. On the drop zone — allow the drop dropZone.addEventListener('dragover', function(e) { e.preventDefault(); // THIS IS REQUIRED or drop won't fire }); // 3. On the drop zone — actually add the field dropZone.addEventListener('drop', function(e) { var newField = { id: 'field_1', type: draggedType }; fields.push(newField); // add to array renderCanvas(); // update the HTML });
The 3-Panel Layout
Every form builder uses the same layout. Once you know it, you can build it with plain CSS Grid.
Field Palette
Drop Zone
Settings
/* The whole thing is just a CSS Grid */ .workspace { display: grid; grid-template-columns: 240px 1fr 260px; height: 100vh; } /* Drop zone gets a dashed border when empty */ .drop-zone { min-height: 200px; border: 2px dashed #ccc; } /* Highlight when something is dragged over it */ .drop-zone.drag-over { border-color: #4CAF50; background: #f0fff4; }
<div class="workspace"> <div class="palette"> <div draggable="true" data-type="text">Text Input</div> <div draggable="true" data-type="email">Email</div> </div> <div class="canvas"> <div class="drop-zone"> <!-- field blocks appear here --> </div> </div> <div class="settings-panel"> <!-- settings for selected field --> </div> </div>
renderCanvas() — Turning Data into HTML
One function reads the fields[] array and builds the HTML. You call it every time anything changes.
function renderCanvas() { var zone = document.getElementById('drop-zone'); zone.innerHTML = ''; // clear existing HTML // Loop through the array and build HTML for each field fields.forEach(function(field) { var block = document.createElement('div'); block.className = 'field-block'; block.innerHTML = ` <label>${field.label}</label> <input type="${field.type}" placeholder="${field.placeholder}"> `; zone.appendChild(block); }); // Show placeholder if empty if (fields.length === 0) { zone.innerHTML = '<p>Drop fields here</p>'; } }
fields[] array, then call renderCanvas(). The function rebuilds everything from scratch. This is exactly how React, Vue, and WordPress Gutenberg blocks work.
The Settings Panel — Editing a Field’s Data
When you click a field, the settings panel reads that field’s data and shows inputs to edit it.
// When a field is clicked — show its settings function selectField(id) { selectedId = id; var field = fields.find(f => f.id === id); // Populate the settings panel with this field's values document.getElementById('input-label').value = field.label; document.getElementById('input-placeholder').value = field.placeholder; document.getElementById('toggle-required').checked = field.required; } // When user types in the settings panel — update the array document.getElementById('input-label').addEventListener('input', function() { var field = fields.find(f => f.id === selectedId); field.label = this.value; // update the array renderCanvas(); // re-render the canvas });
selectField(id) fills the settings panel → user edits → event listener updates fields[] → renderCanvas() redraws the canvas instantly.
The Complete Flow — From Drag to Save
Put it all together. Here’s every step from dragging a field to saving the form.
Item
stores type
event
‘text’
event
drop zone
.push()
added to array
Canvas()
on screen
panel
edit its data
stringify()
or export
<!-- HTML --> <div class="workspace"> <div class="palette"> <div draggable="true" data-type="text">Text</div> <div draggable="true" data-type="email">Email</div> </div> <div id="drop-zone"></div> <div id="settings"></div> </div> // JavaScript var fields = []; var draggedType = null; var selectedId = null; var nextId = 1; // Step 1: Each palette item stores its type on dragstart document.querySelectorAll('.palette [draggable]').forEach(function(el) { el.ondragstart = function() { draggedType = el.dataset.type; }; }); // Step 2: Drop zone accepts the drop var zone = document.getElementById('drop-zone'); zone.ondragover = function(e) { e.preventDefault(); }; zone.ondrop = function() { fields.push({ id: 'field_' + nextId++, type: draggedType, label: draggedType + ' field', required: false }); renderCanvas(); }; // Step 3: Render the array as HTML function renderCanvas() { zone.innerHTML = fields.map(function(f) { return `<div onclick="selectField('${f.id}')"> <label>${f.label}</label> <input type="${f.type}"> </div>`; }).join(''); } // Step 4: Show settings for the clicked field function selectField(id) { selectedId = id; var field = fields.find(f => f.id === id); document.getElementById('settings').innerHTML = ` <input id="s-label" value="${field.label}" oninput="updateField('label', this.value)"> `; } // Step 5: Update the field object when settings change function updateField(key, value) { var field = fields.find(f => f.id === selectedId); field[key] = value; // update the array renderCanvas(); // re-render }
post_meta as JSON using update_post_meta()<input> tags on the front endInspectorControls$_POST and saved with update_post_meta()
Leave a Reply