GithubHelp home page GithubHelp logo

hotwire-turbo-express's Introduction

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

hotwire-turbo-express's People

Contributors

twelve17 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

Watchers

 avatar  avatar

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.