Hyperspan HTML Templates
HTML templates are the backbone of Hyperspan. They are the preferred way to render HTML content to the screen. They support streaming and aysnc content, and escape HTML by default in variables.
Hyperspan HTML Templates...
- Are pure JavaScript. No special syntax to learn. No compile step required.
- Are extrememly lightweight. Source code with types is less than 300 lines.
- Work in all JavaScript runtimes that support Template Literals and AsyncGenerator (for streaming) (all major browsers, Bun, Deno, Node.js, Workers, Edge, etc.).
- Instantly separate static content from async content.
-
Can render static template content immediately, with placeholders for async content with
render
. - Can resolve async content out of order and then render it into the correct placeholders when it resolves.
- Run all async work in the same template concurrently, not one by one or blocking.
- Support unlimited levels of nested templates and nested async content.
-
Can stream render with
renderStream
(returnsAsyncGenerator
) -
Can wait for all async work to finish before rendering with
renderAsync
(returnsstring
)
Installation
Install the @hyperspan/html
package to get started.
npm install @hyperspan/html
bun add @hyperspan/html
Note: The Hyperspan framework requires Bun to use, but the HTML templates package is separate, and can be used in any JavaScript runtime that supports Template Literals and Async Generators.
Example Template
A Hyperspan template is a
Tagged Template Literal
that starts with html`...`
. It's just JavaScript. Nothing to compile!
import { html } from '@hyperspan/html';
// Template syntax
// Values can be any scalar value, Promise, nested template, or object with a 'render' method
function getContent() {
return html`<div>
<p>Some static content here first</p>
<p>${sleep(600, (resolve) => resolve("Resolves second"))}</p>
<p>A bit more static content here as well</p>
<p>
${Promise.resolve(
html`Resolves first... (Nested:
${sleep(800, (resolve) => resolve(html`Resolves third`))})`
)}
</p>
<hr />
${new AsyncValue()}
</div>`;
}
Templates can include any HTML markup you want. Your markup does not have to start with a root node or a fragment.
Async Work in Templates
Got some data to fetch before you can render your template? Just make your template function
async
and await
any data you need. No special knowledge, edge
cases, or new syntax to learn.
import { html } from '@hyperspan/html';
async function MyTemplate() {
const posts = await fetchBlogPosts({ page: 1 });
return html`
<main>
<h1>Blog Posts</h1>
<ul>
${posts.map(post => html`<li><a href="/blog/${post.id}">${post.title}</a></li>`)}
</ul>
</main>
`;
}
When stream rendering is used, async work at the top level template will hold up the initial page response. Any subsequent async work in nested templates will be render a loading placeholder and the content will be replaced later when it is done streaming in.
Custom Loading Placeholders
By default, Hyperspan will render <span>Loading...</span>
as a
placeholder for all async values rendered in templates. If you want to customize this with
loading skeletons, spinners, or other custom content, you can do so with the
placeholder(tmpl, promise)
function in the
@hyperspan/html
package.
import { html, placeholder } from '@hyperspan/html';
const content = html`<div>
${placeholder(
html`<div class="blog-posts-skeleton">Loading blog posts...</div>`,
sleep(600, (resolve) => resolve("Blog posts HTML chunk here..."))
)}
</div>`;
Custom Async Values
If you want a more structured way to wrap up custom async logic with custom loading
placeholders, you can do so by creating an object that has both render
and
renderAsync
methods that return an HTML template.
The render
method will be called immediately for the loading placeholder, and
the
renderAsync
method will be the content that will replace the loading placeholder once resolved.
import { html } from '@hyperspan/html';
// Custom value class
class RemoteCMSContentBlock {
id: number;
constructor(id: number) {
this.id = id;
}
// Loading placeholder (rendered immediately)
render() {
return html`<div>CMS Content Loading...</div>`;
}
// Actual content - replaces the loading placeholder when Promise is resolved
async renderAsync() {
const response = await fetch(`https://api.mycompanycms.com/contentblocks/${this.id}`);
return html.raw(await response.text());
}
}
// Use it in a template
const content = html`<h1>Remote CMS Content:</h1>${new RemoteCMSContentBlock(123)}`;
HTML Escaping
Hyperspan HTML templates escape HTML by default. This means that any variables you pass into the template will be sanitized to prevent XSS attacks.
import { html } from '@hyperspan/html';
const userName = '<script>alert("XSS")</script>';
const content = html`<div>${userName}</div>`;
// content is now: <div><script>alert("XSS")</script></div>
Rendering Raw HTML
Sometimes chunks of content are already formatted with HTML — like if they are coming from an internal or headless CMS — and you need to render them as-is.
When you need to render HTML inside your template, you can use the
html.raw()
function. Just make sure it is trusted content, because it
could be a potential security risk if not.
import { html } from '@hyperspan/html';
const userName = '<script>alert("XSS")</script>';
const content = html`<div>${html.raw(userName)}</div>`; // html.raw() around userName
// content is now: <div><script>alert("XSS")</script></div>
Rendering Options
The main options you have to render Hyperspan templates are streaming or async. Which one you use depends on your needs. The Hyperspan framework defaults to stream rendering for users, and async rendering for bots and crawlers.
Regardless of which rendering method you use, the template syntax and semantics are always the same.
render(tmpl): string
The render
method only renders static content and will render placeholders for
all async content. This is useful when you need to get some initial content to the screen
immediately.
import { render } from '@hyperspan/html';
const content = render(tmpl);
renderAsync(tmpl): Promise<string>
The renderAsync
method renders the whole template
and all nested templates as a single Promise. This rendering method is
useful if you need the full complete page response as a single chunk without streaming.
Recommended for bots and crawlers.
import { renderAsync } from '@hyperspan/html';
const content = await renderAsync(tmpl);
renderStream(tmpl): AsyncGenerator<string>
The renderStream
method renders initial content immediately with placeholders,
and then continues to stream in async chunks of content as they resolve, one by one. Loading
placeholders are rendered as a
<slot>
element. Async content chunks are appended as a
<template>
element
with a matching slot name.
Note: This rendering method requires a small JavaScript shim on the client for maximum cross-browser compatability. This is automatically added when used in the Hyperspan framework.
import { renderStream } from '@hyperspan/html';
const root = document.getElementById('root');
for await (const chunk of renderStream(tmpl)) {
root.insertAdjacentHTML("beforeend", chunk);
}