Git Product home page Git Product logo

html's Introduction

Worker HTML

HTML templating and streaming response library for Worker Runtimes such as Cloudflare Workers.

HTML Templating

Templating is done purely in JavaScript using tagged template strings, inspired by hyperHTML and lit-html.

This library is using tagged template strings to create streaming response bodies on the fly, using no special template syntax and giving you the full power of JS for composition.

Examples

String interpolation works just like regular template strings, but all content is sanitized by default:

const helloWorld = 'Hello World!';
const h1El = html`<h1>${helloWorld}</h1>`;

What is known as "partials" in string-based templating libraries are just functions here:

const timeEl = (ts = new Date()) => html`
  <time datetime="${ts.toISOString()}">${ts.toLocalString()}</time>
`;

What is known as "layouts" are just functions as well:

const baseLayout = (title: string, content: HTMLContent) => html`
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>${title}</title>
    </head>
    <body>${content}</body>
  </html>
`;

Layouts can "inherit" from each other, again using just functions:

const pageLayout = (title: string, content: HTMLContent) => baseLayout(title, html`
  <main>
    ${content}
    <footer>Powered by @worker-tools/html</footer>
  </main>
`);

Many more features of string-based templating libraries can be replicated using functions. Most satisfying should be the use of map to replace a whole host of custom looping syntax:

html`<ul>${['Foo', 'Bar', 'Baz'].map(x => html`<li>${x}</li>`)}</ul>`;

Putting it all together:

function handleRequest(event: FetchEvent) {
  const page = pageLayout(helloWorld, html`
    ${h1El}
    <p>The current time is ${timeEl()}.</p>
    <ul>${['Foo', 'Bar', 'Baz'].map(x => html`<li>${x}</li>`)}</ul>
  `));

  return new HTMLResponse(page);
}

self.addEventListener('fetch', ev => ev.respondWith(handleRequest(ev)));

Note that this works regardless of worker runtime: Cloudflare Workers, Service Workers in the browser, and hopefully other Worker Runtimes that have yet to be implemented.

Tooling

Since the use of tagged string literals for HTML is not new (see hyperHTML, lit-html), there exists tooling for syntax highlighting, such as lit-html in VSCode.

Streaming Responses

As a side effect of this approach, responses are streams by default. This means you can use async data, without delaying sending the headers and HTML content.

In the example below, everything up to and including <p>The current time is will be sent immediately, with the reset sent after the API request completes:

function handleRequest(event: FetchEvent) {
  // NOTE: No `await` here!
  const timeElPromise = fetch('https://time.api/now')
    .then(r => r.text())
    .then(t => timeEl(new Date(t)));

  return new HTMLResponse(pageLayout('Hello World!', html`
    <h1>Hello World!</h1>
    <p>The current time is ${timeElPromise}.</p>
  `));
}

While there's ways around the lack of async/await in the above example (namely IIAFEs), @worker-tools/html supports passing async functions as html content directly:

function handleRequest(event: FetchEvent) {
  return new HTMLResponse(pageLayout('Hello World!', html`
    <h1>Hello World!</h1>
    ${async () => {
      const timeStamp = new Date(
        await fetch('https://time.api/now').then(r => r.text())
      );
      return html`<p>The current time is ${timeEl(timeStamp)}.</p>`
    }}
  `));
}

Note that there are some subtle differences compared to the earlier examples.

  • The initial response will include headers and html up to and including <h1>Hello World!</h1>
  • The time API request will not be sent until the headers and html up to and including <h1>Hello World!</h1> have hit the wire.

These follow from the way async/await works, so shouldn't be too surprising to those already familiar with common async/await pitfalls.

If for any reason you don't want to use streaming response bodies, you can use the BufferedHTMLResponse instead, which will buffer the entire body before releasing it to the network.

See Other

You can combine this library with tools from the Worker Tools family such as @worker-tools/response-creators:

import { internalServerError } from '@worker-tools/response-creators';

function handleRequest(event: FetchEvent) {
  return new HTMLResponse(
    pageLayout('Ooops', html`<h1>Something went wrong</h1>`), 
    internalServerError(),
  );
}

You can also see the Worker News source code for an example of how to build an entire web app on the edge using Worker HTML.

Finally, you can read The Joys and Perils of Writing Plain Old Web Apps for a personal account of building web apps in a Web 2.0 way.




This module is part of the Worker Tools collection
⁕

Worker Tools are a collection of TypeScript libraries for writing web servers in Worker Runtimes such as Cloudflare Workers, Deno Deploy and Service Workers in the browser.

If you liked this module, you might also like:

  • 🧭 Worker Router --- Complete routing solution that works across CF Workers, Deno and Service Workers
  • πŸ”‹ Worker Middleware --- A suite of standalone HTTP server-side middleware with TypeScript support
  • πŸ“„ Worker HTML --- HTML templating and streaming response library
  • πŸ“¦ Storage Area --- Key-value store abstraction across Cloudflare KV, Deno and browsers.
  • πŸ†— Response Creators --- Factory functions for responses with pre-filled status and status text
  • 🎏 Stream Response --- Use async generators to build streaming responses for SSE, etc...
  • πŸ₯ JSON Fetch --- Drop-in replacements for Fetch API classes with first class support for JSON.
  • πŸ¦‘ JSON Stream --- Streaming JSON parser/stingifier with first class support for web streams.

Worker Tools also includes a number of polyfills that help bridge the gap between Worker Runtimes:

  • ✏️ HTML Rewriter --- Cloudflare's HTML Rewriter for use in Deno, browsers, etc...
  • πŸ“ Location Polyfill --- A Location polyfill for Cloudflare Workers.
  • πŸ¦• Deno Fetch Event Adapter --- Dispatches global fetch events using Deno’s native HTTP server.

Fore more visit workers.tools.

html's People

Contributors

cn-traveler avatar jakechampion avatar qwtel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

html's Issues

Empty string instead of space for falsy elements

Hello, thanks for this library! I'm enjoying using it to make a little worker.

I noticed that falsy values become a space instead of an empty string.
This can produce unexpected results when using variables in HTML attributes, for example:

let foo: string | null = request.searchParams.get("foo");
return html`<input type="text" value="${foo}" placeholder="hello world">`;

This gets rendered as:

<input type="text" value=" " placeholder="hello world">

which hides the placeholder and makes 1-1 correlation with <form method=GET> difficult.

Desired result:

<input type="text" value="" placeholder="hello world">

I was wondering if I am doing something wrong. It seems like there isn't a way to embed an empty string at the moment.

Apologizes if I missed something!

Ideas for loading state?

Hey there.

Playing around, and I'm wondering if we can do some Suspense-like behavior? Like, to put loading... somehow while the async stuff is done.

function render(req, context) {
  return new HTMLResponse(
    html`<h1>Hello World!</h1>
      <div>${context.data.hi} <span>${sleep(5000, html`<span>loading...</span>`)}</span></div>
    `
  );
}

Where sleep is a promise returning function in which until it is resolved it should show the "loading" state.

Probably not, but interested in thoughts. Because the other way would be to have the whole page reload (thus, re-render) and pass the resolved result to the context (somehow).

Maybe something like that.

function render(req, context) {
  return new HTMLResponse(
    html`<h1>Hello World!</h1>
      <div>${context.data.hi} <span>${context.data.someResult || sleep(5000, req, context)}</span></div>
    `
  );
}

Hm..

Skypack package fails in Deno and browser

When I run this example with deno run html_example.ts:

import { html } from 'https://cdn.skypack.dev/@worker-tools/[email protected]?dts';
const str = "1 < 2";
console.log(html`<p>${str}</p>`);

I get this error message:

error: Uncaught SyntaxError: The requested module '/new/[email protected]/dist=es2020,mode=imports?from=whatwg-stream-to-async-iter' does not provide an export named 'asyncIterableToStream'
import {asyncIterableToStream} from "/new/[email protected]/dist=es2020,mode=imports?from=whatwg-stream-to-async-iter";
        ~~~~~~~~~~~~~~~~~~~~~
    at <anonymous> (https://cdn.skypack.dev/-/@worker-tools/[email protected]/dist=es2020,mode=imports/optimized/@worker-tools/html/html-response.js:1:9)

Integrate with existing worker code?

How can I integrate this to an existing worker? That is, I'm already able to fetch and return a complete HTML document. But I'd like to use HTMLRewriter to append some HTML to the document. I can't quite figure it out from the getclaps example -- everything seems to be based around using the HTMLResponse function.

No conditional output mechanism

It seems like there's no way to evaluate a logical expression inside a template and output HTML based on its results (e.g. if-else), other than through creating a separate function, or there's no example covering it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.