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 messages
conn.on("message", function(data) {
console.log("Received:", data);
});
// Send a message
conn.send({ type: "chat", text: "Hello" });
// Close
conn.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.
# Python
from tina4_python.frond import live_source
@live_source("prices")
def prices(request):
return {"rows": Price.all()}// PHP
Frond::liveSource("prices", fn($request) => ["rows" => Price::all()]);# Ruby
Tina4::Frond.live_source("prices") { |request| { rows: Price.all } }// Node
Frond.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 days
frond.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 login
frond.token = "eyJhbGciOiJIUzI1NiIs...";
// Read
console.log(frond.token);
// Clear on logout
frond.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) |