Don't stress! You don't need to be an HTML or JavaScript expert to use this golden feature. Just copy this entire guide page (the text you're reading right now) and paste it into an AI assistant like DeepSeek or any other AI. Then, simply describe what you want in plain English, for example: "Create an HTML page with 4 directional buttons to control a robot, and display ultrasonic sensor data received via Bluetooth." The AI will generate the complete code for you!
📋 How to use it in SoifGo:
🎯 That's it! Ask the AI to build any app you want and run it inside SoifGo instantly.
SoifGo turns your HTML pages into a real Android app. Inside its WebView, your page can talk to Bluetooth, read phone sensors, request microphone access, connect to MQTT brokers, send/receive API data, and store data — all with plain HTML, CSS, and JavaScript.
This guide shows you how to:
Ready-to-use controllers. Each example includes: HTML live demo, HTML Code copyable code, and where available Arduino ↓ sketch.
📥 Download all examples as ZIP
Inside SoifGo's WebView, a JavaScript object called window.soifgo
is automatically injected. Use it to call native functions from JavaScript.
// Basic call pattern
window.soifgo.CallSub('subName', true, arg1, arg2, arg3);
// Real example — send text to Bluetooth
window.soifgo.CallSub('html_bluetooth_rx', true, txt);
// Real example — connect to MQTT broker (step 1)
window.soifgo.CallSub('connecte_mqtt_ai', true, address, id);
// Real example — send data to API
window.soifgo.CallSub('api_send', true, address, key, value);
// Real example — receive data from API
window.soifgo.CallSub('api_recive', true, address, key);
window.soifgo.CallSub(...) directly.true or false.'' — never omit them.WebView → SoifGo → Bluetooth
This page allows the user to enter text and send it via Bluetooth. The data goes from the WebView to SoifGo, then to the connected Bluetooth device.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Send Command</title>
</head>
<body style="text-align:center; margin-top:50px; font-family:sans-serif;">
<h3>📤 Send Command to Bluetooth</h3>
<input type="text" id="txt" placeholder="Enter text...">
<br><br>
<button onclick="sendBluetooth()"
style="padding:10px 30px; font-size:16px; background:#2196f3;
color:white; border:none; border-radius:6px; cursor:pointer;">
Send
</button>
<script>
function sendBluetooth() {
const txt = document.getElementById('txt').value;
window.soifgo.CallSub('html_bluetooth_rx', true, txt);
}
</script>
</body>
</html>
window.soifgo.CallSub('html_bluetooth_rx', true, txt).Bluetooth → SoifGo → WebView
This page displays data received from Bluetooth. SoifGo forwards incoming data to the WebView using JavaScript functions.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BT Receive</title>
</head>
<body>
<div id="output"
style="font-size:20px;color:white;background:black;
padding:20px;min-height:100vh;">
waiting for data...
</div>
<script>
// This function is called by SoifGo when Bluetooth data arrives
function html_bluetooth_tx(data) {
document.getElementById('output').innerHTML = data;
}
// Expose to window so SoifGo can find it
window.html_bluetooth_tx = html_bluetooth_tx;
</script>
</body>
</html>
html_bluetooth_tx(data) in the WebView.Sensors → SoifGo → WebView
This page displays data from 3 sensors:
window.soifgo.CallSub('sensor_light', true); // Activate light sensor
window.soifgo.CallSub('sensor_move', true); // Activate movement sensor
window.soifgo.CallSub('sensor_magno', true); // Activate magneto sensor
These 3 commands are called inside window.onload to auto-start sensors when the page loads.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sensor Receive</title>
</head>
<body>
<div id="light" style="font-size:20px;color:white;background:#333;padding:15px;margin:5px;border-radius:8px;">
Light: waiting...
</div>
<div id="movex" style="font-size:20px;color:white;background:#444;padding:15px;margin:5px;border-radius:8px;">
Move X: waiting...
</div>
<div id="movey" style="font-size:20px;color:white;background:#555;padding:15px;margin:5px;border-radius:8px;">
Move Y: waiting...
</div>
<div id="magno" style="font-size:20px;color:white;background:#666;padding:15px;margin:5px;border-radius:8px;">
Magneto: waiting...
</div>
<script>
// ---- Receiver functions (called by SoifGo) ----
function html_sensor_light(data) {
document.getElementById('light').innerHTML = 'Light: ' + data;
}
function html_sensor_movex(data) {
document.getElementById('movex').innerHTML = 'Move X: ' + data;
}
function html_sensor_movey(data) {
document.getElementById('movey').innerHTML = 'Move Y: ' + data;
}
function html_sensor_magno(data) {
document.getElementById('magno').innerHTML = 'Magneto: ' + data;
}
// Expose them
window.html_sensor_light = html_sensor_light;
window.html_sensor_movex = html_sensor_movex;
window.html_sensor_movey = html_sensor_movey;
window.html_sensor_magno = html_sensor_magno;
// ---- Auto-start sensors on page load ----
window.onload = function() {
window.soifgo.CallSub('sensor_light', true);
window.soifgo.CallSub('sensor_move', true);
window.soifgo.CallSub('sensor_magno', true);
};
</script>
</body>
</html>
If your project needs microphone access — for example, to record audio, do speech-to-text conversion, or build a voice → Bluetooth pipeline — you must request permission the first time. After the user allows it once, Android remembers the choice and no dialog appears again.
request_mic_perm once,
before you start using the microphone. That is all you need.
// Call this once, before starting speech recognition or audio recording
window.soifgo.CallSub('request_mic_perm', true, null);
request_mic_perm in your code,
the user can grant microphone permission manually in
Android Settings → Apps → SoifGo → Permissions → Microphone.<button id="micBtn">🎤 Start</button>
<script>
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.lang = 'en-US';
document.getElementById('micBtn').addEventListener('click', () => {
// 1. Ask for microphone permission (first time only)
window.soifgo.CallSub('request_mic_perm', true, null);
// 2. Start speech recognition
recognition.start();
});
recognition.onresult = (event) => {
const text = event.results[0][0].transcript;
if (text.trim()) {
// 3. Send recognized text to Bluetooth
window.soifgo.CallSub('html_bluetooth_rx', true, text);
}
};
</script>
WebView ↔ Broker
SoifGo has a built-in MQTT client. Your HTML page can connect to any MQTT broker, publish messages, and receive them — all through simple JavaScript calls. No WebSocket, no CORS, no external library needed.
// ============================================================
// 1. CONNECT — step 1: address + client id
// ============================================================
// Native Sub: connecte_mqtt_ai(address, id)
//
// address : "tcp://host:1883" or "ssl://host:8883"
// Empty → defaults to "tcp://test.mosquitto.org:1883"
// id : any unique string. Empty → auto-generated "soifgo_XXXX"
//
window.soifgo.CallSub('connecte_mqtt_ai', true, address, id);
// ============================================================
// 2. CONNECT — step 2 (optional): username + password
// ============================================================
// Native Sub: connecte_mqtt_up(username, password)
//
// Only call this if the broker requires authentication.
// If both are empty, nothing happens.
//
window.soifgo.CallSub('connecte_mqtt_up', true, username, password);
// ============================================================
// 3. PUBLISH a message (also subscribes to the same topic!)
// ============================================================
// Native Sub: mqtt_publish(topic, message, qos)
//
// topic : topic string (e.g. "soifgo/led")
// message : text payload
// qos : "0", "1", or "2" (as a string)
//
// 💡 Publishing to a topic also subscribes you to it,
// so you receive messages published by other clients.
//
window.soifgo.CallSub('mqtt_publish', true, topic, message, qos);
// ============================================================
// 4. DISCONNECT from the broker
// ============================================================
// Native Sub: mqtt_Disconnect (no arguments)
//
window.soifgo.CallSub('mqtt_Disconnect', true);
// ============================================================
// 5. CHECK connection status
// ============================================================
// Native Sub: mqtt_status (no arguments)
// SoifGo replies by calling html_mqtt_status('true'/'false')
//
window.soifgo.CallSub('mqtt_status', true);
// ============================================================
// RECEIVERS — functions SoifGo calls on your page
// ============================================================
// Incoming message on a subscribed topic
function mqtt_rx(topic, message) {
// topic : string
// message : string
}
window.mqtt_rx = mqtt_rx;
// Connection status changed
// ⚠️ The value is sent as a STRING ('true' or 'false'),
// NOT as a real boolean. Convert it before using it!
function html_mqtt_status(connected) {
const isConnected = (connected === true || connected === 'true');
// use isConnected (true/false)
}
window.html_mqtt_status = html_mqtt_status;
connecte_mqtt_ai(address, id) to start the connection.connecte_mqtt_up(username, password) right after.mqtt_rx(topic, message).mqtt_publish also auto-subscribes you to that topic — so you receive messages from other clients on it.mqtt_status — SoifGo answers with html_mqtt_status('true') or ('false').mqtt_Disconnect.Complete working example — connect, publish, receive, and check status from a single page.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MQTT Control</title>
<style>
body { font-family: sans-serif; background:#1e1e1e; color:#e0e0e0; padding:16px; }
input, button { padding:10px; border-radius:6px; border:1px solid #444;
background:#2b2b2b; color:#fff; font-size:16px; margin:4px 0; width:100%; }
button { background:#9b59b6; border:none; cursor:pointer; font-weight:600; }
button:hover { background:#8e44ad; }
button.on { background:#27ae60; }
button.off { background:#c0392b; }
button.check { background:#f39c12; color:#10131a; }
.status { padding:10px; border-radius:6px; margin:10px 0; font-weight:600; }
.status.connected { background:#1a2a1a; color:#4caf50; border:1px solid #4caf50; }
.status.disconnected { background:#2a1a1a; color:#e74c3c; border:1px solid #e74c3c; }
#log { background:#111; padding:12px; border-radius:8px; height:220px;
overflow-y:auto; font-family:monospace; font-size:0.85rem; margin-top:12px; }
#log div { margin:2px 0; }
.row { display:flex; gap:8px; }
.row button { flex:1; }
label { font-size:0.8rem; color:#888; display:block; margin-top:6px; }
</style>
</head>
<body>
<h3>🟣 MQTT Control Panel</h3>
<!-- Connection -->
<label>Broker URL</label>
<input type="text" id="broker" value="tcp://test.mosquitto.org:1883">
<label>Client ID (empty = auto)</label>
<input type="text" id="clientId" placeholder="myTest_001">
<label>Username (optional)</label>
<input type="text" id="username" placeholder="">
<label>Password (optional)</label>
<input type="password" id="password" placeholder="">
<label>QoS (0, 1, 2)</label>
<input type="text" id="qos" value="0">
<div class="row">
<button onclick="connectMQTT()">Connect</button>
<button class="check" onclick="checkStatus()">Check</button>
<button onclick="disconnectMQTT()">Disconnect</button>
</div>
<div id="status" class="status disconnected">Disconnected</div>
<label>Topic</label>
<input type="text" id="pubTopic" value="soifgo/demo/tx">
<label>Message</label>
<input type="text" id="pubMsg" value="ON">
<div class="row">
<button class="on" onclick="publishMsg('ON')">Publish ON</button>
<button class="off" onclick="publishMsg('OFF')">Publish OFF</button>
</div>
<div class="row">
<button onclick="publishMsg()">Send Custom</button>
</div>
<div id="log"></div>
<script>
function log(msg) {
const el = document.getElementById('log');
const t = new Date().toLocaleTimeString();
el.innerHTML += `<div>[${t}] ${msg}</div>`;
el.scrollTop = el.scrollHeight;
}
function setStatus(connected) {
const el = document.getElementById('status');
el.className = 'status ' + (connected ? 'connected' : 'disconnected');
el.textContent = connected ? 'Connected' : 'Disconnected';
}
/* ---- Connect (two calls: address+id, then user+pass) ---- */
function connectMQTT() {
const broker = document.getElementById('broker').value.trim();
const cid = document.getElementById('clientId').value.trim();
const user = document.getElementById('username').value.trim();
const pass = document.getElementById('password').value;
log('Connect → ' + broker + (cid ? ' as ' + cid : ' (auto ID)'));
window.soifgo.CallSub('connecte_mqtt_ai', true, broker, cid);
if (user || pass) {
window.soifgo.CallSub('connecte_mqtt_up', true, user, pass);
}
}
function disconnectMQTT() {
log('Disconnect');
window.soifgo.CallSub('mqtt_Disconnect', true);
setStatus(false);
}
function checkStatus() {
log('Check status');
window.soifgo.CallSub('mqtt_status', true);
}
function publishMsg(preset) {
const topic = document.getElementById('pubTopic').value.trim();
const qos = document.getElementById('qos').value.trim() || '0';
const text = (preset !== undefined) ? preset : document.getElementById('pubMsg').value;
if (!topic) { alert('Enter a topic'); return; }
log('TX → ' + topic + ' : ' + text + ' (QoS ' + qos + ')');
window.soifgo.CallSub('mqtt_publish', true, topic, text, qos);
}
/* ---- Receivers (called by SoifGo) ---- */
function mqtt_rx(topic, message) {
log('RX ← ' + topic + ' : ' + message);
}
window.mqtt_rx = mqtt_rx;
function html_mqtt_status(connected) {
const isConnected = (connected === true || connected === 'true');
setStatus(isConnected);
log('Status: ' + (isConnected ? 'CONNECTED' : 'DISCONNECTED'));
}
window.html_mqtt_status = html_mqtt_status;
</script>
</body>
</html>
connecte_mqtt_ai, then connecte_mqtt_up if auth needed.=== 'true'.Combine MQTT and Bluetooth on the same page. For example: forward incoming Bluetooth data to MQTT, or the other way around.
/* ---- Bluetooth → MQTT ---- */
function html_bluetooth_tx(data) {
window.soifgo.CallSub('mqtt_publish', true, 'soifgo/relay/from_bt', data, '0');
}
window.html_bluetooth_tx = html_bluetooth_tx;
/* ---- MQTT → Bluetooth ---- */
function mqtt_rx(topic, message) {
window.soifgo.CallSub('html_bluetooth_rx', true, message);
}
window.mqtt_rx = mqtt_rx;
tcp:// for plain, ssl://
for TLS. Default ports: 1883 (plain), 8883 (TLS).
'true' / 'false').
In JavaScript the string 'false' is truthy — always compare with
=== 'true'.
window.soifgo is undefined, you're running in a normal
browser — the bridge only exists inside SoifGo.
WebView ↔ Server
SoifGo has a built-in API bridge. Your HTML page can send
key/value data to a server and request data back — no fetch,
no CORS, no external library.
// ============================================================
// 1. SEND — POST a key/value pair to a server
// ============================================================
// Native Sub: api_send(address, key, value)
//
// Sends: POST
// Body: {"":""}
// Header: Content-Type: application/json
//
window.soifgo.CallSub('api_send', true, address, key, value);
// ============================================================
// 2. RECEIVE — GET data from a server
// ============================================================
// Native Sub: api_recive(address, key)
//
// Sends: GET
// Returns: the "" branch of the JSON response,
// passed to api_rx() as a string
//
// ⚠️ The second parameter "key" is REQUIRED — it tells
// SoifGo which branch of the JSON you want.
//
window.soifgo.CallSub('api_recive', true, address, key);
// ============================================================
// 3. RECEIVER — SoifGo calls this with the response
// ============================================================
// SoifGo calls: api_rx('')
//
// ⚠️ The response format depends on the API provider.
// See the section below — "Two Response Formats".
//
function api_rx(number) {
// number : string — either a plain value or a wrapped one
}
window.api_rx = api_rx;
api_send(address, key, value) → POST with JSON body.api_recive(address, key) → GET request. The key tells SoifGo which branch of the JSON to extract.api_rx(number) as a string.
Different API providers return their data in different shapes.
When SoifGo extracts the value at the given key, it can arrive
at api_rx in two very different forms:
The API returns a flat JSON where the key points directly to a primitive value.
Example API: DIA
Raw JSON:
{
"Symbol": "BTC",
"Price": 86846.51694166221
}
What api_rx receives:
86846.51694166221
Just the raw value. No braces, no key name.
The API returns a nested JSON where the key points to another object.
Example API: CoinGecko
Raw JSON:
{
"bitcoin": {
"usd": 86750
}
}
What api_rx receives:
{usd=86750}
The inner object, wrapped in B4A Map format.
DIA is used here because it has no strict rate limit and returns a simple, flat JSON. This makes it perfect for testing.
setInterval) lets the page refresh the price automatically
without the user tapping a button. But polling too fast will trigger rate
limits and the API will start rejecting your requests.
Choose a safe interval for the API you're using
(see the Rate Limits section below).
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Price</title>
<style>
body { font-family: sans-serif; background:#1e1e1e; color:#e0e0e0;
padding:20px; text-align:center; }
h2 { color:#f7931a; }
.price { font-size:2.5rem; font-weight:bold; color:#f7931a;
margin:20px 0; min-height:3rem; }
.status { font-size:0.85rem; color:#888; }
button { background:#f7931a; color:#fff; border:none;
padding:14px 30px; border-radius:8px; font-size:16px;
font-weight:600; cursor:pointer; margin:6px; }
button.stop { background:#c0392b; }
#raw { background:#111; padding:12px; border-radius:8px;
font-family:monospace; font-size:0.75rem;
text-align:left; margin-top:12px;
word-break:break-all; max-height:120px; overflow-y:auto; }
</style>
</head>
<body>
<h2>₿ Bitcoin Price</h2>
<div class="price" id="price">--</div>
<div class="status" id="status">Stopped</div>
<div style="margin-top:14px;">
<button onclick="startBTC()">▶ Start</button>
<button class="stop" onclick="stopBTC()">■ Stop</button>
</div>
<div id="raw"></div>
<script>
// DIA endpoint — flat JSON, no rate limit issues
const URL = 'https://api.diadata.org/v1/assetQuotation/Bitcoin/0x0000000000000000000000000000000000000000';
const KEY = 'Price';
const INTERVAL = 5000; // 5 seconds — safe for DIA
let timer = null;
function startBTC() {
stopBTC();
fetchBTC();
timer = setInterval(fetchBTC, INTERVAL);
document.getElementById('status').textContent = 'Live · every ' + (INTERVAL/1000) + 's';
}
function stopBTC() {
if (timer) { clearInterval(timer); timer = null; }
document.getElementById('status').textContent = 'Stopped';
}
function fetchBTC() {
window.soifgo.CallSub('api_recive', true, URL, KEY);
}
/* ============================================================
Robust extractor — handles BOTH response formats
============================================================ */
function extractNumber(raw) {
const s = String(raw).trim();
// Format A — plain number: "86846.51694166221"
if (/^-?\d+(\.\d+)?$/.test(s)) {
return parseFloat(s);
}
// Format B — B4A Map: "{usd=86750}"
const mapMatch = s.match(/[a-zA-Z_]\w*[=:]\s*(-?[\d.]+)/);
if (mapMatch) {
return parseFloat(mapMatch[1]);
}
// Fallback — any number in the string
const anyNum = s.match(/-?\d+(\.\d+)?/);
return anyNum ? parseFloat(anyNum[0]) : null;
}
function api_rx(number) {
document.getElementById('raw').textContent = String(number);
const price = extractNumber(number);
if (price !== null && !isNaN(price)) {
document.getElementById('price').textContent =
'$' + price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
} else {
document.getElementById('price').textContent = '❌ Parse error';
}
}
window.api_rx = api_rx;
</script>
</body>
</html>
fetchBTC() manually without setInterval.Different providers return different shapes. Use this single helper to handle both cases — plain numbers, Map-wrapped values, and JSON.
function extractValue(raw, fieldName) {
const s = String(raw).trim();
// 1. Plain number: "86846.51694166221"
if (/^-?\d+(\.\d+)?$/.test(s)) return s;
// 2. B4A Map format: {usd=86750} or {Price=86846.51}
const mapMatch = s.match(
new RegExp(fieldName + '[=:]\\s*([^,}\\s]+)', 'i')
);
if (mapMatch) return mapMatch[1];
// 3. Valid JSON: {"usd":86750}
try {
const obj = JSON.parse(s);
if (obj[fieldName] !== undefined) return obj[fieldName];
} catch (e) {}
// 4. Fallback: any number
const numMatch = s.match(/-?\d+(\.\d+)?/);
return numMatch ? numMatch[0] : null;
}
// Usage in api_rx:
function api_rx(number) {
const price = extractValue(number, 'Price'); // or 'usd'
if (price) {
document.getElementById('out').textContent = '$' + price;
}
}
window.api_rx = api_rx;
{"status":{"error_code":429,"error_message":"You've exceeded the Rate Limit"}}
SoifGo supports two ways of talking to an API. The Classic method works entirely inside SoifGo's native UI; the WebView method uses the HTML bridge you've been reading about.
api_recive(address, key) and handle the
response in api_rx.api_recive(address, key). Missing the key will silently fail.
api_rx is registered on window:
window.api_rx = api_rx;
{usd=86750}) or as a plain value (86846.51) —
neither is valid JSON. Use the extractValue() helper above.
api_rx receives
the raw value (86846.51) with no wrapper. Handle this case
with the plain-number branch of the extractor.
window.soifgo is undefined, you're in a normal browser.
The bridge only exists inside SoifGo.
alert(), confirm(),
and prompt() all work natively in SoifGo's WebView.
You can use standard browser dialogs in your code.
Use localStorage to save and reload data. It persists across page
reloads and WebView restarts, so your users never lose their settings.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Save & Reload</title>
<style>
body { font-family: sans-serif; background:#1e1e1e; color:#e0e0e0; padding:16px; }
input, button { padding:10px; border-radius:6px; border:1px solid #444;
background:#2b2b2b; color:#fff; font-size:16px; margin:4px 0; }
button { background:#3498db; border:none; cursor:pointer; }
button.danger { background:#c0392b; }
.row { display:flex; gap:8px; align-items:center; margin:4px 0; }
.row button { flex:0 0 auto; }
#output { margin-top:16px; }
.item { padding:8px 12px; background:#2b2b2b; border-radius:6px;
margin:4px 0; display:flex; justify-content:space-between; }
.item .time { color:#888; font-size:0.85rem; }
</style>
</head>
<body>
<h3>💾 Save & Reload Demo</h3>
<div class="row">
<input type="text" id="nameInput" placeholder="Enter name...">
<button onclick="saveItem()">Save</button>
<button onclick="loadItems()">Reload</button>
<button class="danger" onclick="clearAll()">Clear</button>
</div>
<div id="output"></div>
<script>
const KEY = 'my_data';
/* ---- Save ---- */
function saveItem() {
const name = document.getElementById('nameInput').value.trim();
if (!name) {
alert('Please enter a name.');
return;
}
let list = load();
list.push({ name: name, time: Date.now() });
persist(list);
render(list);
document.getElementById('nameInput').value = '';
}
/* ---- Delete one item ---- */
function removeItem(index) {
const list = load();
list.splice(index, 1);
persist(list);
render(list);
}
/* ---- Clear all ---- */
function clearAll() {
if (!confirm('Delete all saved items?')) return;
localStorage.removeItem(KEY);
render([]);
}
/* ---- Storage helpers ---- */
function load() {
try {
return JSON.parse(localStorage.getItem(KEY)) || [];
} catch (e) {
console.error('Load failed:', e);
return [];
}
}
function persist(list) {
try {
localStorage.setItem(KEY, JSON.stringify(list));
} catch (e) {
console.error('Save failed (quota?):', e);
alert('Could not save. Storage may be full.');
}
}
/* ---- Render ---- */
function render(list) {
const out = document.getElementById('output');
if (!list.length) {
out.innerHTML = '<p style="color:#888;">No items saved yet.</p>';
return;
}
out.innerHTML = list.map((x, i) => `
<div class="item">
<span>${i + 1}. ${escapeHtml(x.name)}</span>
<span>
<span class="time">${new Date(x.time).toLocaleTimeString()}</span>
<button class="danger" onclick="removeItem(${i})">×</button>
</span>
</div>`).join('');
}
/* ---- XSS guard ---- */
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/* ---- Auto-load on page open ---- */
window.onload = function() {
render(load());
};
</script>
</body>
</html>
alert(), confirm(), prompt() work — no custom modal needed.localStorage calls in try/catch to handle quota errors gracefully.render(load()) inside window.onload so data reloads every time the page opens.'my_data' per page to avoid clashes with other pages.innerHTML (XSS guard).