To create user-interface with webflow scripts are used to enable communication between object components (buttons, sliders, input text fields etc) and the javascript backend. The main script to enable communication is added to the home page of the application.
Page > Home > Edit Page Settings > Custom Code
Inside head tag:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- prevent vertical scrolling -->
<style>
body {
height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>
Before body tag:
<script>
document.addEventListener("DOMContentLoaded", () => {
// ---------- NAVIGATION ----------
function showScreen(target) {
document.querySelectorAll('[data-nav]').forEach(div => div.style.display = 'none');
const el = document.querySelector(`[data-nav="${target}"]`);
if (el) el.style.display = 'block';
}
document.querySelectorAll('[data-target]').forEach(btn => {
btn.addEventListener('click', () => showScreen(btn.getAttribute('data-target')));
});
// ---------- SOCKET ----------
const ipEl = document.querySelector('[data-ip]');
const host = ipEl?.dataset?.ip?.trim();
const url = host ? `ws://${host}:1880/ws` : null;
function connect() {
if (!url) { console.error("❌ No [data-ip] found for backend URL"); return null; }
const s = new WebSocket(url);
s.addEventListener("open", () => console.log("✅ WebSocket connected"));
s.addEventListener("close", () => setTimeout(connect, 2000)); // simple reconnect
s.addEventListener("error", (e) => console.error("❌ WebSocket error", e));
s.onmessage = handleMessage;
window.socket = s;
return s;
}
const socket = connect();
function send(obj) {
if (socket?.readyState === 1) socket.send(JSON.stringify(obj));
}
// ---------- DIGITAL (boolean only) ----------
document.querySelectorAll('[data-type="digital"]').forEach(btn => {
// init UI state from attribute
const state = btn.dataset.state === "true";
btn.classList.toggle("high", state);
btn.setAttribute("aria-pressed", state);
btn.addEventListener("click", () => {
const id = btn.dataset.id;
if (!id) return;
// invert boolean
const next = btn.dataset.state !== "true";
btn.dataset.state = String(next);
// optimistic UI
btn.classList.toggle("high", next);
btn.setAttribute("aria-pressed", next);
// send to backend
send({ type: "digital", id, value: next });
});
});
// ---------- ANALOG (sliders) ----------
const throttleMs = 40; // limit chatter
document.querySelectorAll('[data-type="analog"]').forEach(slider => {
let last = 0;
slider.addEventListener("input", () => {
const now = performance.now();
if (now - last < throttleMs) return;
last = now;
const id = slider.dataset.id;
if (!id) return;
send({ type: "analog", id, value: Number(slider.value) });
});
});
// ---------- SERIAL (text inputs/labels) ----------
document.querySelectorAll('[data-type="serial"]').forEach(input => {
const push = () => {
const id = input.dataset.id;
if (!id) return;
send({ type: "serial", id, value: input.value });
};
input.addEventListener("change", push);
input.addEventListener("keydown", e => { if (e.key === "Enter") push(); });
});
// ---------- RECEIVE FEEDBACK ----------
function handleMessage(event) {
const data = JSON.parse(event.data);
const el = document.querySelector(`[data-id="${data.id}"]`);
if (!el) return;
switch (data.type) {
case "analog":
if (el.tagName === "INPUT" && el.type === "range") {
el.value = data.value;
}
break;
case "digital": {
const next = data.value === true || String(data.value).toLowerCase() === "true";
el.dataset.state = String(next);
el.classList.toggle("high", next);
el.setAttribute("aria-pressed", next);
break;
}
case "serial":
if (el.tagName === "INPUT" && el.type === "text") {
el.value = data.value;
} else {
el.textContent = data.value;
}
break;
}
}
});
</script>