Skip to content
Render lit-html templates on the server as Node.js streams
JavaScript HTML
Branch: master
Clone or download
dependabot-preview and popeindustries Bump eslint from 6.1.0 to 6.2.0 (#70)
Bumps [eslint](https://github.com/eslint/eslint) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](eslint/eslint@v6.1.0...v6.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
Latest commit 6fdf34e Aug 19, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
scripts fix build May 23, 2019
src Add index.d.ts file (#55) Aug 1, 2019
test add TextEncoder polyfill for Node 10 Apr 27, 2019
.editorconfig initial commit Aug 27, 2018
.eslintrc initial commit Aug 27, 2018
.gitignore add browser build Feb 22, 2019
.npmignore add browser build Feb 22, 2019
.prettierignore initial commit Aug 27, 2018
.prettierrc add template result tests Jan 20, 2019
.travis.yml fix build May 23, 2019
LICENSE initial commit Aug 27, 2018
README.md Update README.md (#67) Aug 8, 2019
index.d.ts Add index.d.ts file (#55) Aug 1, 2019
package-lock.json Bump eslint from 6.1.0 to 6.2.0 (#70) Aug 19, 2019
package.json release 1.3.0 Aug 1, 2019

README.md

NPM Version Build Status

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>
`;

renderToStream(TemplateResult): 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(TemplateResult): 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(TemplateResult): 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';

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';

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';
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):
html`
  <input .value="${value}" />
`;
//=> <input />
  • 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.

Directives

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

NOTE: directives for use in the browser are imported from @popeindustries/lit-html-server/browser/directives

  • 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>
`;
  • 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 basis for this streaming implementation, and thanks to Justin Fagnani and the team behind the lit-html project!

You can’t perform that action at this time.