Tina4

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:

html
<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.

js
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:

js
frond.request("/api/users", function(data) {    console.log(data);});

Options:

OptionTypeDescription
methodstringHTTP method (default: "GET")
bodyobject\|FormData\|stringRequest body: objects become JSON, FormData stays multipart
headersobjectExtra headers
onSuccessfunction(data, status, xhr)Success callback (2xx/3xx)
onErrorfunction(status, xhr)Error callback (4xx/5xx)

Automatic features:

  • Sends Authorization: Bearer <token> when frond.token is set
  • Reads FreshToken response header and updates frond.token automatically
  • 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.

js
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.

js
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.

js
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.

js
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.

js
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:

html
<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:

ActionHTTP Method
"create"GET
"edit"GET
"delete"DELETE
js
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.

js
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:

OptionTypeDefaultDescription
reconnectbooleantrueAuto-reconnect on disconnect
reconnectDelaynumber1000Initial reconnect delay (ms)
maxReconnectDelaynumber30000Max reconnect delay (exponential backoff)
maxReconnectAttemptsnumberInfinityStop trying after N attempts
protocolsstring[][]WebSocket sub-protocols
onOpenfunction-Connection opened
onClosefunction(code, reason)-Connection closed
onErrorfunction(error)-Connection error

Connection object:

Property/MethodDescription
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.

js
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:

OptionTypeDefaultDescription
reconnectbooleantrueAuto-reconnect
eventsstring[][]Named events to listen for
jsonbooleantrueAuto-parse JSON data
onOpenfunction-Stream opened
onClosefunction-Stream closed
onErrorfunction(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.

twig
{% 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:

html
<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.

DirectiveRefresh
{% 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
# Pythonfrom tina4_python.frond import live_sourceโ€‹@live_source("prices")def prices(request):    return {"rows": Price.all()}
php
// PHPFrond::liveSource("prices", fn($request) => ["rows" => Price::all()]);
ruby
# RubyTina4::Frond.live_source("prices") { |request| { rows: Price.all } }
javascript
// 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:

python
await push_live("prices", {"rows": Price.all()})   # Python
ruby
Tina4::Frond.push_live("prices", { rows: Price.all })   # Ruby
javascript
Frond.pushLive("prices", { rows: Price.all() });   // Node   (PHP: Frond::pushLive)

Rules#

  • A block's src attribute 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#

js
frond.cookie.set("theme", "dark", 30);    // Set cookie, expires in 30 daysfrond.cookie.get("theme");                 // "dark"frond.cookie.remove("theme");              // Delete cookie

Utility#

frond.message(text, type)#

Display a Bootstrap-style alert in the #message element.

js
frond.message("User created!", "success");frond.message("Something went wrong", "danger");

frond.popup(url, title, width, height)#

Open a centred popup window.

js
frond.popup("/preview", "Preview", 800, 600);

frond.report(url)#

Open a URL (typically a PDF) in a new browser tab.

js
frond.report("/api/reports/monthly.pdf");

GraphQL#

frond.graphql(url, query, variables, options)#

Execute a GraphQL query or mutation.

js
frond.graphql("/api/graphql", "{ products { id name price } }", {}, {    onSuccess: function(result) {        console.log(result.data);        if (result.errors) console.warn(result.errors);    }});

With variables:

js
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>.

js
// 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.

html
<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:

html
<input type="file" name="files[]" multiple>

Quick Reference#

MethodDescription
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/removeCookie 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.tokenBearer token (read/write)