npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

hotwire-turbo-express

v0.0.8

Published

Hotwire Turbo ExpressJS backend

Downloads

11

Readme

hotwire-turbo-express

Node.js CI

ExpressJS middleware for sending turbo-stream HTML fragments to a hotwire Turbo client. It aims to perform a subset of functionality that turbo-rails provides with ERB templates, but with EJS templates.

Example App Screen Recording

Requirements

  • Node 14.x or newer

Installation

npm i hotwire-turbo-express

Overview

Per the Turbo Streams docs, When Turbo encounters a <turbo-stream> element in an HTML fragment delivered by a server over a "WebSocket, SSE or other transport", the DOM element with an id that matches the target attribute will be modified with the updates inside the <turbo-stream>.

Here are a few response transport scenarios:

HTTP Response Stream

In this scenario, a client submits a form.

  1. Turbo includes text/vnd.turbo-stream.html in the HTTP Accept header.
  2. The server detects the above header and responds with Content-Type: text/vnd.turbo-stream.html, and includes a HTML fragment with one or more <turbo-stream> elements.
  3. On the client, Turbo detects the Content-Type header, which signals to it to process the above response and update the matching DOM elements.

WebSocket & SSE

Here, a stimulus controller connected to an HTML element will open a Websocket or SSE connection to a server. Whenever a message comes in which has <turbo-stream> tags, Turbo will process its contents in the same fashion as in the HTTP Response Stream scenario.

Usage

  • turboStream - Middleware function for express.

    Options:

    • mimeType - The Turbo stream MIME type. Defaults to text/vnd.turbo-stream.html.
    import turboStream from 'hotwire-turbo-express';
    
    const app = express();
    
    app.use(turboStream());

The middleware will add a res.turboStream property with some functions:

  • append, prepend, replace, and update - These functions are the equivalent of the turbo-rails turbo_stream.append, turbo_stream.prepend, etc, methods, with a slightly different arguments to more closely match how EJS works in express:

    • turboStream.append(view, locals, stream, onlyFormat)

      arguments:

      • view and locals are the same arguments that would be passed to res.render.
      • stream is an object of which attributes will be added to the turbo-stream HTML element, with the exception of action, which will be set to the value matching the append/prepend/replace/update function.
      • onlyFormat - see sendStream

    Given the MessagesController rails example in the turbo docs, this would be the equivalent here:

    const upload = multer();
    
    app.post('/messages/create', upload.none(), async (req, res, next) => {
      const message = createMessage(...);
      const locals = { message };
      const view = 'messages/partials/message';
      const stream = { target: 'list' }
      return res.turboStream.append(view, locals, stream);
    });
  • renderViews - The append/prepend/replace/update functions send a single <turbo-stream> element in the response. However, you can "render any number of stream elements in a single stream message". renderViews providers this ability by accepting an array of objects, each which will result in a <turbo-stream> element with its own properties. Each entry is tied to a given EJS view to be rendered.

    • turboStream.renderViews(<array of stream spec objects>, <onlyFormat>)

    Stream spec array attributes:

    • view and locals accept the same arguments that would be passed to res.render.
    • stream is an object of which attributes will be added to the turbo-stream HTML element
    router.post('/page', upload.none(), async (req, res) => {
        const { hasMore, items } = await getItems();
        return res.turboStream.renderViews([
          {
            stream: {
              action: 'append',
              target: 'item-list',
            },
            locals: { items },
            view: 'item-list/partials/item-list',
          },
          {
            stream: {
              action: 'replace',
              target: 'item-list-more-button',
            },
            locals: { hasMore },
            view: 'item-list/partials/item-list-more-button',
          },
        ], true);
      });
    • onlyFormat - see sendStream
  • TurboStream - A simple class for creating <turbo-stream> HTML fragments.

    • constructor: new TurboStream(attributes, content)
      • attributes - An object of attributes to set in the <turbo-stream> tag.
      • content - A string with the content to place as the child element of the tag.
    • Instance methods:
      • toHtml() - Returns an HTML fragment string.
        > tag = new turboStream.TurboStream({ action: 'append' }, "hi there")
        > console.log(tag.toHtml())
      
          <turbo-stream action="append">
            <template>
              hi there
            </template>
          </turbo-stream>
      • toSseMessage() - Returns an HTML fragment string suitable for sending in a server sent event message. The Turbo client looks for the <turbo-stream> in the data attribute. The message will include two newline characters at the end, to signal a flush of the SSE response.
        > tag = new turboStream.TurboStream({ action: 'append' }, "hi there")
        > console.log(tag.toSseMessage())
        data: <turbo-stream action="append">    <template>      hi there    </template>  </turbo-stream>
      • toWebSocketMessage() - Returns an HTML fragment string suitable for sending in a WebSocket message.
        > tag = new turboStream.TurboStream({ action: 'append' }, "hi there")
        > console.log(tag.toWebSocketMessage())
        > <turbo-stream action="append">    <template>      hi there    </template>  </turbo-stream>
      While this will work, consider expanding the scope of these messages, e.g. to include signing messages to ensure they are not tampered with, as is done in turbo-rails.
  • compileViews - Same as renderView but returns the compiled HTML fragment instead of sending it to the client.

  • compileView - Same as compileViews but accepts a single stream spec object instead of an array of them.

  • sendStream - Convenience function that sends an HTML snippet string with the turbo-stream MIME type. args:

    • res - The express response object.
    • html - The rendered html.
    • onlyFormat (boolean, defaults to false) - If true, the response will be configured to only respond to requests which have the correct Turbo MIME type, otherwise, a HTTP 406 (Not Acceptable) response will be sent. If false, the stream response will be sent regardless of what is specified in the request's Accept HTTP header.

--

TurboStream is also a named export, so it can be used outside of the middleware. Here is an example of sending a turbo stream message over a WebSocket:

import { TurboStream } from 'hotwire-turbo-express';
import WebSocket from 'ws';

/**
* Send a message to the WS server
* with a turbo stream of the given html.
*/
const sendItemWsMessage = (url, stream, html) => {
  const tag = new TurboStream(stream, html);
  const ws = new WebSocket(url);
  ws.on('open', async () => {
    ws.send(tag.toWebSocketMessage());
    return ws.close();
  });
};

JSDocs

example-app

The example app has complete implementations showing how to use this library to work with <turbo-stream>s. Explanation of the use cases are shown in the app itself.

  • Action initiated in one browser is reflected in other browsers connected via SSE/WebSocket:

    Example App Screen Recording

  • Action initiated from an external source, in this case a CLI tool that sends a message via WebSocket, is reflected in browsers connected to the same WebSocket endpoint:

    Example App Screen Recording 2

Setup and Run

# builds the NPM, installs it in the app
npm run example:setup

# calls npm start in the app
npm run example:start

Browse to http://localhost:3000

Turbo Stream Protocol Notes

Turbo is integrated with SSE or WebSockets by way of the connectStreamSource and disconnectStreamSource functions.

  • Make Turbo a client listening to WebSocket messages at a given endpoint:
connectStreamSource(new WebSocket('ws://foo/bar');
  • Make turbo a client listening to SSE messages at a given endpoint:
connectStreamSource(new EventSource('http://foo/bar');

Once connected, messages with <turbo-stream> HTML snippets will be processed by Turbo.

There is an example using stimulus in the example app, in src/controllers/stream-controller.

Server Sent Events (SSE) Payload Format

Payload format is: data: {html with turbo stream HTML in one line}:

data: <turbo-stream action='append' target='item-list'>  <template>    <p>My new Message</p>  </template>  </turbo-stream>

Express response:

# must be in one line, to conform to EventSource message format.
res.write("data: <turbo-stream action='append'...>")

See example in the /items/actions/stream route in example-app/app.mjs.

WebSocket Payload Format

Payload format is just the HTML in one line.

const ws = new WebSocket('ws://localhost:3000');
ws.on('open', async () => {
  ws.send("<turbo-stream><template>My new message</template></turbo-stream>");
  return ws.close();
});

See example server at the bottom of example-app/bin/www.mjs and client in example-app/lib/send-item-ws-message.mjs.

Development

Publishing

npm run release

Seems that np's contents flag does not work how I expected, and packito seems to not have publishing working yet, so the relase will run both packito and np without publishing, then delegate to npm publish ./dist.

🎩 Tip To