frond.js: Client-Side Helper#
A zero-dependency DOM helper for AJAX, forms, WebSocket, SSE, cookies, and GraphQL. Ships with every Tina4 backend and tina4-css. Under 3KB gzipped.
Include it once in your base template:
<script src="/js/frond.min.js"></script>Every method is available on the global window.frond object.
API Reference#
frond.request(url, options)#
Core HTTP request with automatic Bearer token and CSRF token handling.
frond.request("/api/users", { method: "GET", onSuccess: function(data, status, xhr) { console.log(data); }, onError: function(status, xhr) { console.error("Failed:", status); }});Shorthand with callback:
frond.request("/api/users", function(data) { console.log(data);});Options:
| Option | Type | Description |
|---|---|---|
method | string | HTTP method (default: "GET") |
body | object\|FormData\|string | Request body: objects become JSON, FormData stays multipart |
headers | object | Extra headers |
onSuccess | function(data, status, xhr) | Success callback (2xx/3xx) |
onError | function(status, xhr) | Error callback (4xx/5xx) |
Automatic features:
- Sends
Authorization: Bearer <token>whenfrond.tokenis set - Reads
FreshTokenresponse header and updatesfrond.tokenautomatically - Detects XHR-followed redirects (3xx) and navigates the browser
frond.load(url, target, callback)#
GET a URL and inject the HTML response into a target element.
frond.load("/dashboard", "content");frond.load("/profile", "main", function(html, raw) { console.log("Loaded");});frond.post(url, data, target, callback)#
POST data and inject the HTML response into a target element.
frond.post("/api/save", { name: "Alice" }, "message", function(html, raw) { console.log("Saved");});frond.inject(html, targetId)#
Parse an HTML string, inject it into an element, and execute any <script> tags found in the content.
frond.inject('<div>Hello</div><script>console.log("injected")</script>', "content");Returns the innerHTML if no target is specified.
Forms#
frond.form.collect(formId)#
Collect all form fields into a FormData object. Handles text inputs, selects, textareas, checkboxes, radio buttons, and file uploads.
var data = frond.form.collect("myForm");If frond.token is set and the form contains a formToken field, the token value is updated automatically.
frond.form.submit(formId, url, target, callback)#
Collect form data and POST it. The response is injected into the target element.
frond.form.submit("loginForm", "/login", "message", function(html) { window.location = "/dashboard";});This is the primary way to submit forms in Tina4 applications. The button should use type="button" with an onclick handler:
<form id="loginForm" method="POST" action="/login"> {{ form_token() }} <input type="email" name="email"> <input type="password" name="password"> <button type="button" onclick="frond.form.submit('loginForm', '/login', null, function(){ window.location='/dashboard'; })"> Login </button></form>frond.form.show(action, url, target, callback)#
Load a form by action type. Maps actions to HTTP methods:
| Action | HTTP Method |
|---|---|
"create" | GET |
"edit" | GET |
"delete" | DELETE |
frond.form.show("create", "/products/new", "form");frond.form.show("edit", "/products/42/edit", "form");frond.form.show("delete", "/products/42", "form");WebSocket#
frond.ws(url, options)#
Connect to a WebSocket endpoint with automatic reconnection.
var conn = frond.ws("ws://localhost:7146/ws/chat", { reconnect: true, reconnectDelay: 1000, maxReconnectDelay: 30000, onOpen: function() { console.log("Connected"); }, onClose: function(code, reason) { console.log("Closed"); }, onError: function(err) { console.error(err); }});โ// Listen for messagesconn.on("message", function(data) { console.log("Received:", data);});โ// Send a messageconn.send({ type: "chat", text: "Hello" });โ// Closeconn.close();Options:
| Option | Type | Default | Description |
|---|---|---|---|
reconnect | boolean | true | Auto-reconnect on disconnect |
reconnectDelay | number | 1000 | Initial reconnect delay (ms) |
maxReconnectDelay | number | 30000 | Max reconnect delay (exponential backoff) |
maxReconnectAttempts | number | Infinity | Stop trying after N attempts |
protocols | string[] | [] | WebSocket sub-protocols |
onOpen | function | - | Connection opened |
onClose | function(code, reason) | - | Connection closed |
onError | function(error) | - | Connection error |
Connection object:
| Property/Method | Description |
|---|---|
conn.status | "connecting" / "open" / "reconnecting" / "closed" |
conn.send(data) | Send string or object (auto-stringified) |
conn.on(event, fn) | Listen for "message", "open", "close", "error" |
conn.close(code?, reason?) | Close the connection |
Server-Sent Events (SSE)#
frond.sse(url, options)#
Connect to an SSE endpoint with automatic reconnection.
var stream = frond.sse("/api/sse/sales", { events: ["order", "stock"], json: true, onOpen: function() { console.log("Stream open"); }});โstream.on("message", function(data, eventName) { console.log(eventName, data);});โstream.close();Options:
| Option | Type | Default | Description |
|---|---|---|---|
reconnect | boolean | true | Auto-reconnect |
events | string[] | [] | Named events to listen for |
json | boolean | true | Auto-parse JSON data |
onOpen | function | - | Stream opened |
onClose | function | - | Stream closed |
onError | function(error) | - | Stream error |
Live Blocks#
A live block is a region of a server-rendered page that keeps itself current. You write it in a Frond template with the {% live %} tag. Frond renders it on the server with the first request, so the page ships with real content, and frond.js refreshes that one region on its own.
{% live "prices" poll 5 %} <ul> {% for row in rows %}<li>{{ row.name }}: {{ row.price }}</li>{% endfor %} </ul>{% endlive %}Frond wraps the block in a marker element:
<div data-frond-live="prices" id="live-prices" data-mode="poll" data-interval="5" data-src="/__frond/live/prices"> ...first-paint HTML...</div>frond.js finds every [data-frond-live] marker on the page and drives it. You load nothing extra. The same client that powers frond.request and frond.ws hydrates live blocks.
Transports#
Name the transport right after the block name.
| Directive | Refresh |
|---|---|
{% live "name" poll 5 %} | Re-fetch GET /__frond/live/name every 5 seconds |
{% live "name" sse %} | Stream refreshes over Server-Sent Events |
{% live "name" ws "/ws/path" %} | Refresh when the server pushes over a WebSocket you own |
Poll and SSE blocks read from one always-on endpoint, GET /__frond/live/{name}, that Frond mounts for you. A WebSocket block refreshes when your code pushes to it.
The data provider#
Register a provider so each refresh renders fresh data. The provider runs again with the live request, so an authenticated block re-checks auth on every refresh and a block that reads the current user keeps reading the right one.
# Pythonfrom tina4_python.frond import live_sourceโ@live_source("prices")def prices(request): return {"rows": Price.all()}// PHPFrond::liveSource("prices", fn($request) => ["rows" => Price::all()]);# RubyTina4::Frond.live_source("prices") { |request| { rows: Price.all } }// NodeFrond.liveSource("prices", (request) => ({ rows: Price.all() }));Pushing over a WebSocket#
For a ws block, re-render and broadcast the fresh HTML to every connected client with one call:
await push_live("prices", {"rows": Price.all()}) # PythonTina4::Frond.push_live("prices", { rows: Price.all }) # RubyFrond.pushLive("prices", { rows: Price.all() }); // Node (PHP: Frond::pushLive)Rules#
- A block's
srcattribute is same-origin only. A live block cannot pull from another host. - Nested live blocks are rejected. Keep each block flat.
- The provider is the only place a live block reads data, so put your auth check there.
Cookies#
frond.cookie#
frond.cookie.set("theme", "dark", 30); // Set cookie, expires in 30 daysfrond.cookie.get("theme"); // "dark"frond.cookie.remove("theme"); // Delete cookieUtility#
frond.message(text, type)#
Display a Bootstrap-style alert in the #message element.
frond.message("User created!", "success");frond.message("Something went wrong", "danger");frond.popup(url, title, width, height)#
Open a centred popup window.
frond.popup("/preview", "Preview", 800, 600);frond.report(url)#
Open a URL (typically a PDF) in a new browser tab.
frond.report("/api/reports/monthly.pdf");GraphQL#
frond.graphql(url, query, variables, options)#
Execute a GraphQL query or mutation.
frond.graphql("/api/graphql", "{ products { id name price } }", {}, { onSuccess: function(result) { console.log(result.data); if (result.errors) console.warn(result.errors); }});With variables:
frond.graphql("/api/graphql", 'query ($term: String!) { search(term: $term) { id name } }', { term: "widget" }, function(result) { console.log(result.data.search); });Token Management#
frond.token#
Read or write the Bearer token. When set, every frond.request() call includes Authorization: Bearer <token>.
// Set after loginfrond.token = "eyJhbGciOiJIUzI1NiIs...";โ// Readconsole.log(frond.token);โ// Clear on logoutfrond.token = null;Token rotation is automatic: if a response includes a FreshToken header, frond.token updates to the new value.
File Uploads#
Use frond.form.submit() with a form containing file inputs. FormData handles multipart encoding automatically.
<form id="uploadForm"> {{ form_token() }} <input type="file" name="avatar"> <button type="button" onclick="frond.form.submit('uploadForm', '/api/upload', 'message')"> Upload </button></form>Multiple files:
<input type="file" name="files[]" multiple>Quick Reference#
| Method | Description |
|---|---|
frond.request(url, opts) | Core HTTP with auth + token rotation |
frond.load(url, target, cb) | GET + inject HTML |
frond.post(url, data, target, cb) | POST + inject HTML |
frond.inject(html, target) | Parse HTML + run scripts |
frond.form.collect(formId) | Collect FormData |
frond.form.submit(formId, url, target, cb) | POST form |
frond.form.show(action, url, target, cb) | Load form by CRUD action |
frond.ws(url, opts) | WebSocket with reconnect |
frond.sse(url, opts) | SSE with reconnect |
frond.cookie.set/get/remove | Cookie helpers |
frond.message(text, type) | Alert display |
frond.popup(url, title, w, h) | Centred popup |
frond.report(url) | Open PDF |
frond.graphql(url, query, vars, opts) | GraphQL query/mutation |
frond.token | Bearer token (read/write) |