Connection Status
A page that loses its connection looks completely normal, but is frozen. If the user fills in a form over a dead connection, everything is gone when she presses send.
// main.ts, runs once when the app startsimport { defineLtConnectionStatus } from "@lo-tek/interfaces/connection-status"import "@lo-tek/interfaces/tokens.css"import "@lo-tek/interfaces/connection-status.css"
defineLtConnectionStatus()How to use it
Section titled “How to use it”<!-- The stylesheets the page needs. Without a build step it is `<link>`, not `import`: a browser does not resolve a package name. --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/standalone/connection-status/connection-status.css"><!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css --><lt-connection-status class="lt-connection-status" data-ignore-morph></lt-connection-status>
<!-- Once for the whole app, in the page template. Not once per component. --><script type="module"> import { defineLtConnectionStatus } from "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/standalone/connection-status/lt-connection-status.js"
defineLtConnectionStatus()</script>
<script type="module"> const connection = document.querySelector("lt-connection-status") if (!connection) throw new Error("element not found")
async function save(body) { try { const response = await fetch("/api/save", { method: "POST", body }) if (!response.ok) throw new Error(response.statusText) connection.reportSuccess() } catch { connection.reportFailure() } }</script>// src/lotek.d.ts, gives <lt-connection-status> types in JSXimport "@lo-tek/interfaces/react-jsx"import "@lo-tek/interfaces/connection-status.css"import { lt } from "@lo-tek/interfaces/react"import { defineLtConnectionStatus, reportFailure, reportSuccess,} from "@lo-tek/interfaces/connection-status"import "@lo-tek/interfaces/tokens.css"
defineLtConnectionStatus()
export function Connection() { return <lt-connection-status {...lt.connectionStatus()} />}
export async function save(body: BodyInit) { const connection = document.querySelector("lt-connection-status") if (!connection) throw new Error("element not found") try { const response = await fetch("/api/save", { method: "POST", body }) if (!response.ok) throw new Error(response.statusText) await reportSuccess(connection) } catch { await reportFailure(connection) }}reportFailure(element) and reportSuccess(element) wait until registration has run before they call the method. A call that comes before defineLtConnectionStatus(), such as the first message from an event stream, is then carried out as soon as the element is ready, instead of failing.
<!-- Datastar itself. Without it the `data-` attributes below do nothing. --><script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js"></script>
<!-- The stylesheets the page needs. Without a build step it is `<link>`, not `import`: a browser does not resolve a package name. --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/standalone/connection-status/connection-status.css"><!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->
<!-- Once for the whole app, in the page template. Not once per component. --><script type="module"> import { defineLtConnectionStatus } from "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/standalone/connection-status/lt-connection-status.js"
defineLtConnectionStatus()</script>
<lt-connection-status class="lt-connection-status" data-ignore-morph></lt-connection-status>
<script type="module"> import { reportFailure, reportSuccess } from "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/standalone/connection-status/lt-connection-status.js"
const bar = document.querySelector("lt-connection-status")
// Datastar reports every request with the `datastar-fetch` event. document.addEventListener("datastar-fetch", (event) => { const type = event.detail.type
// `retrying`: the connection dropped. `error`: the server answered with an error. if (type === "retrying" || type === "error") reportFailure(bar)
// A patch arrived, so the stream is alive. `finished` is no good as a // sign: it also comes after an error. if (type.startsWith("datastar-patch")) reportSuccess(bar) })</script>Datastar keeps a long-lived stream open to the server. If it drops, the page looks right but stops updating. The browser only reports when the machine loses the network, not when the server stops answering, so the script at the bottom is what makes the bar appear in that case too.
Why it creates the content itself
Section titled “Why it creates the content itself”The server by definition cannot tell you that it is unavailable. If it is down, nothing comes from it to render. So the component creates the content itself, and the builder function sets data-ignore-morph so that a later patch does not tear the bar away the moment the connection is back. See Markup and behaviour.
navigator.onLine is not enough
Section titled “navigator.onLine is not enough”The browser only knows whether the machine has a network, not whether the server answers. An office network with no route to the internet, a server that is down, or a stream that has dropped all count as “online”.
So the app reports it itself:
const connection = document.querySelector("lt-connection-status")if (!connection) throw new Error("element not found")
try { const response = await fetch("/api/save", { method: "POST", body }) if (!response.ok) throw new Error(response.statusText) connection.reportSuccess()} catch { connection.reportFailure()}| Method | When you call it |
|---|---|
reportFailure() | A request to the server failed |
reportSuccess() | A request succeeded again |
reportFailure(element) and reportSuccess(element) from @lo-tek/interfaces/connection-status do the same, but first wait until the element is registered.
The component also listens to the browser’s offline and online, so a network that disappears is caught without you doing anything.
Attributes and events
Section titled “Attributes and events”| Attribute | Default |
|---|---|
offline-text | ”No connection. What you type is not being saved.” |
online-text | ”Connection restored.” |
| Event | When |
|---|---|
connection-lost | The bar appears. Once per outage, however many offline events the browser sends |
connection-restored | The connection is back |
Classes you can style
Section titled “Classes you can style”| Class | What it is |
|---|---|
.lt-connection-status | The host element. It is display: contents and takes up no space |
.lt-connection-status__bar | The bar itself. Has data-state="offline" or data-state="online" |
| Variable | Fallback |
|---|---|
--lt-connection-status-layer | 1000 |
--lt-connection-status-padding | var(--lt-spacing-2) var(--lt-spacing-4) |
.lt-connection-status__bar { --lt-connection-status-layer: 9999;}Accessibility
Section titled “Accessibility”The bar is role="status" with aria-live="polite", not alert. The message is important, but should not interrupt in the middle of a word. It stays until the connection is back, so someone who reads slowly has time to read it.
While a modal dialog is open, the bar is inert and sits under the backdrop, like everything else outside the dialog. If the connection drops then, the user learns about it only when the dialog closes.
Colour alone does not carry the message: the text says what is wrong. In Windows high contrast mode, custom colours and backgrounds are removed, so the bar gets a border in CanvasText; otherwise it would be invisible.