• This repository has been archived on 25/Aug/2022
  • Stars
    star
    265
  • Rank 154,577 (Top 4 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 6 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Render lit-html templates on the server as Node.js streams

Warning
Moved to @popeindustries/lit

lit-html-server

Render lit-html templates on the server as strings or streams (and in the browser too!). Supports all lit-html types, special attribute expressions, and many of the standard directives.

Although based on lit-html semantics, lit-html-server is a great general purpose HTML template streaming library. Tagged template literals are a native JavaScript feature, and the HTML rendered is 100% standard markup, with no special syntax or runtime required!

Usage

Install with npm/yarn:

$ npm install --save @popeindustries/lit-html-server

...write your lit-html template:

const { html } = require('@popeindustries/lit-html-server');
const { classMap } = require('@popeindustries/lit-html-server/directives/class-map.js');
const { until } = require('@popeindustries/lit-html-server/directives/until.js');

function Layout(data) {
  return html`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>${data.title}</title>
      </head>
      <body>
        ${until(Body(data.api))}
      </body>
    </html>
  `;
}

async function Body(api) {
  // Some Promise-based request method
  const data = await fetchRemoteData(api);

  return html`
    <h1>${data.title}</h1>
    <x-widget ?enabled="${data.hasWidget}"></x-widget>
    <p class="${classMap({ negative: data.invertedText })}">${data.text}</p>
  `;
}

...and render (plain HTTP server example, though similar for Express/Fastify/etc):

const http = require('http');
const { renderToStream } = require('@popeindustries/lit-html-server');

http.createServer((request, response) => {
  const data = { title: 'Home', api: '/api/home' };

  response.writeHead(200);
  // Returns a Node.js Readable stream which can be piped to "response"
  renderToStream(Layout(data)).pipe(response);
});

Universal Templates

With lit-html-server and lit-html it's possible to write a single template and render it on the server, in a ServiceWorker, and in the browser. In order to be able to render the same template in three different runtime environments, it's necessary to change the version of html and directives used to process the template. It would certainly be possible to alias imports using a build process (so that import { html } from 'lit-html' points to @popeindustries/lit-html-server for bundles run in server/ServiceWorker), but a more flexible approach would be to pass references directly to the templates (dependency injection):

/**
 * layout.js
 */
import Body from './body.js';

export function Layout(context, data) {
  const {
    html,
    directives: { until }
  } = context;

  return html`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>${data.title}</title>
      </head>
      <body>
        ${until(Body(context, data.api))}
      </body>
    </html>
  `;
}

/**
 * server.js
 * (transpiler or experimental modules required)
 */
import { html, renderToStream } from '@popeindustries/lit-html-server';
import { Layout } from './layout.js';
import { until } from '@popeindustries/lit-html-server/directives/until.js';

const context = {
  html,
  directives: {
    until
  }
};

http
  .createServer((request, response) => {
    response.writeHead(200);
    renderToStream(Layout(context, data)).pipe(response);
  }

/**
 * service-worker.js
 * (bundler required)
 */
import { html, renderToStream } from '@popeindustries/lit-html-server/browser/index.js';
import { Layout } from './layout.js';
import { until } from '@popeindustries/lit-html-server/browser/directives/until.js';

const context = {
  html,
  directives: {
    until
  }
};

self.addEventListener('fetch', (event) => {
  const stream = renderToStream(Layout(context, data));
  const response = new Response(stream, {
    headers: {
      'content-type': 'text/html'
    }
  });

  event.respondWith(response);
});

/**
 * browser.js
 */
import { html, render } from 'lit-html';
import { Layout } from './layout.js';
import { until } from 'lit-html/directives/until.js';

const context = {
  html,
  directives: {
    until
  }
};

render(Layout(context, data), document.body);

API (Node.js)

html

The tag function to apply to HTML template literals (also aliased as svg):

const { html } = require('@popeindustries/lit-html-server');

const name = 'Bob';
html`
  <h1>Hello ${name}!</h1>
`;

All template expressions (values interpolated with ${value}) are escaped for securely including in HTML by default. An unsafe-html directive is available to disable escaping:

const { html } = require('@popeindustries/lit-html-server');
const { unsafeHTML } = require('@popeindustries/lit-html-server/directives/unsafe-html.js');

html`
  <div>${unsafeHTML('<span>danger!</span>')}</div>
`;

The following render methods accept an options object with the following properties:

  • serializePropertyAttributes: boolean - enable JSON.stringify of property attribute values (default: false)

renderToStream(result: TemplateResult, options: RenderOptions): Readable

Returns the result of the template tagged by html as a Node.js Readable stream of markup:

const { html, renderToStream } = require('@popeindustries/lit-html-server');

const name = 'Bob';
renderToStream(
  html`
    <h1>Hello ${name}!</h1>
  `
).pipe(response);

renderToString(result: TemplateResult, options: RenderOptions): Promise<string>

Returns the result of the template tagged by html as a Promise which resolves to a string of markup:

const { html, renderToString } = require('@popeindustries/lit-html-server');

const name = 'Bob';
const markup = await renderToString(
  html`
    <h1>Hello ${name}!</h1>
  `
);
response.end(markup);

renderToBuffer(result: TemplateResult, options: RenderOptions): Promise<Buffer>

Returns the result of the template tagged by html as a Promise which resolves to a Buffer of markup:

const { html, renderToBuffer } = require('@popeindustries/lit-html-server');

const name = 'Bob';
const markup = await renderToBuffer(
  html`
    <h1>Hello ${name}!</h1>
  `
);
response.end(markup);

API (Browser)

lit-html-server may also be used in the browser to render strings of markup, or in a Service Worker script to render streams of markup.

html

The tag function to apply to HTML template literals (also aliased as svg):

import { html } from '@popeindustries/lit-html-server/browser.mjs';

const name = 'Bob';
html`
  <h1>Hello ${name}!</h1>
`;

renderToStream(TemplateResult): ReadableStream

Returns the result of the template tagged by html as a ReadableStream stream of markup. This may be used in a Service Worker script to stream an html response to the browser:

import { html, renderToStream } from '@popeindustries/lit-html-server/browser.mjs';

self.addEventListener('fetch', (event) => {
  const name = 'Bob';
  const stream = renderToStream(
    html`
      <h1>Hello ${name}!</h1>
    `
  );
  const response = new Response(stream, {
    headers: {
      'content-type': 'text/html'
    }
  });

  event.respondWith(response);
});

NOTE: a bundler is required to package modules for use in a Service Worker

renderToString(TemplateResult): Promise<string>

Returns the result of the template tagged by html as a Promise which resolves to a string of markup:

import { html, renderToString } from '@popeindustries/lit-html-server/browser.mjs';
const name = 'Bob';
const markup = await renderToString(
  html`
    <h1>Hello ${name}!</h1>
  `
);
document.body.innerHtml = markup;

Writing templates

In general, all of the standard lit-html rules and semantics apply when rendering templates on the server with lit-html-server (read more about lit-html and writing templates here).

Template structure

Although there are no technical restrictions for doing so, if you plan on writing templates for use on both the server and client, you should abide by the same rules:

  • templates should be well-formed when all expressions are replaced with empty values
  • expressions should only occur in attribute-value and text-content positions
  • expressions should not appear where tag or attribute names would appear
  • templates can have multiple top-level elements and text
  • templates should not contain unclosed elements

Expressions

All of the lit-html expression syntax is supported:

  • text:
html`
  <h1>Hello ${name}</h1>
`;
//=> <h1>Hello Bob</h1>
  • attribute:
html`
  <div id="${id}"></div>
`;
//=> <div id="main"></div>
  • boolean attribute (attribute markup removed with falsey expression values):
html`
  <input type="checkbox" ?checked="${checked}" />
`;
//=> <input type="checkbox" checked> if truthy
//=> <input type="checkbox" > if falsey
  • property (attribute markup removed unless RenderOptions.serializePropertyAttributes = true ):
const value = { some: 'text' };
html`
  <input .value="${value}" />
`;
//=> <input />
html`
  <input .value="${value}" />
`;
//=> <input .value="{&quot;some&quot;:&quot;text&quot;}"/>
// (when render options.serializePropertyAttributes = true)
  • event handler (attribute markup removed):
const fn = (e) => console.log('clicked');
html`
  <button @click="${fn}">Click Me</button>
`;
//=> <button >Click Me</button>

Types

Most of the lit-html value types are supported:

  • primitives: String, Number, Boolean, null, and undefined

    Note that undefined handling is the same as in lit-html: stringified when used as an attribute value, and ignored when used as a node value

  • nested templates:

const header = html`
  <h1>Header</h1>
`;
const page = html`
  ${header}
  <p>This is some text</p>
`;
  • Arrays / iterables (sync):
const items = [1, 2, 3];
html`
  <ul>
    ${items.map(
      (item) =>
        html`
          <li>${item}</li>
        `
    )}
  </ul>
`;
html`
  <p>total = ${new Set(items)}</p>
`;
  • Promises:
const promise = fetch('sample.txt').then((r) => r.text());
html`
  <p>The response is ${promise}.</p>
`;

Note that lit-html no longer supports Promise values. Though lit-html-server does, it's recommended to use the until directive instead when authoring templates to be used in both environments.

Directives

Most of the built-in lit-html directives are also included for compatibility when using templates on the server and in the browser (even though some directives are no-ops in a server rendered context):

  • asyncAppend(value): Renders the items of an AsyncIterable, appending new values after previous values:
const { asyncAppend } = require('@popeindustries/lit-html-server/directives/async-append.js');

html`
  <ul>
    ${asyncAppend(someListIterator)}
  </ul>
`;
  • cache(value): Enables fast switching between multiple templates by caching previous results. Since it's generally not desireable to cache between requests, this is a no-op:
const { cache } = require('@popeindustries/lit-html-server/directives/cache.js');

cache(
  loggedIn
    ? html`
        You are logged in
      `
    : html`
        Please log in
      `
);
  • classMap(classInfo): applies css classes to the class attribute. 'classInfo' keys are added as class names if values are truthy:
const { classMap } = require('@popeindustries/lit-html-server/directives/class-map.js');

html`
  <div class="${classMap({ red: true })}"></div>
`;
  • guard(value, fn): no-op since re-rendering does not apply (renders result of fn):
const { guard } = require('@popeindustries/lit-html-server/directives/guard.js');

html`
  <div>
    ${guard(items, () =>
      items.map(
        (item) =>
          html`
            ${item}
          `
      )
    )}
  </div>
`;
  • ifDefined(value): sets the attribute if the value is defined and removes the attribute if the value is undefined:
const { ifDefined } = require('@popeindustries/lit-html-server/directives/if-defined.js');

html`
  <div class="${ifDefined(className)}"></div>
`;
  • repeat(items, keyfnOrTemplate, template)): no-op since re-rendering does not apply (maps items over template)
const { repeat } = require('@popeindustries/lit-html-server/directives/repeat.js');

html`
  <ul>
    ${repeat(
      items,
      (i) => i.id,
      (i, index) =>
        html`
          <li>${index}: ${i.name}</li>
        `
    )}
  </ul>
`;
  • styleMap(styleInfo): applies css properties to the style attribute. 'styleInfo' keys and values are added as style properties:
const { styleMap } = require('@popeindustries/lit-html-server/directives/style-map.js');

html`
  <div style="${styleMap({ color: 'red' })}"></div>
`;
  • unsafeHTML(value): render value without HTML escaping:
const { unsafeHTML } = require('@popeindustries/lit-html-server/directives/unsafe-html.js');

html`
  <div>${unsafeHTML("hey! it's dangerous! <script>boom!</script>")}</div>
`;
  • until(...args): renders one of a series of values, including Promises, in priority order. Since it's not possible to render more than once in a server context, primitive synchronous values are prioritized over asynchronous Promises. If no synchronous values are passed, the last value is rendered regardless of type:
const { until } = require('@popeindustries/lit-html-server/directives/until.js');

html`
  <p>
    ${until(
      fetch('content.json').then((r) => r.json()),
      html`
        <span>Loading...</span>
      `
    )}
  </p>
`;
// => renders <p><span>Loading...</span></p>

html`
  <p>
    ${until(
      fetch('content.json').then((r) => r.json()),
      isBrowser
        ? html`
            <span>Loading...</span>
          `
        : undefined
    )}
  </p>
`;
// => renders fetch result

Thanks!

Thanks to Thomas Parslow for the stream-template library that was the inspiration for this streaming implementation, and thanks to Justin Fagnani and the team behind the lit-html project!