Circularspace

An HTMX example appending content from a websocket

I want to present an example of appending content to the page when using HTMX, receiving data from a websocket. I couldn’t find a handy example to use, and so had to investigate myself.

Example server

We will set up an example server using FastAPI in Python. This server will show a basic HTML template containing a single text input for new messages:

The server itself looks like this:

import time

from fastapi import FastAPI, Request, WebSocket
from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="templates")


app = FastAPI()


@app.get("/")
async def get(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})


@app.websocket("/ws")
async def time_handler(websocket: WebSocket):
    await websocket.accept()
    # response content
    content = """
        <div hx-swap-oob="beforeend:#content">
        <p>{time}: {message}</p>
        </div>
    """
    while True:
        msg = await websocket.receive_json()
        await websocket.send_text(
            content.format(time=time.time(), message=msg["chat_message"])
        )

where the index.html template is as follows:

<!DOCTYPE html>
<html>
  <head>
    <!-- include htmx -->
    <script
      src="https://unpkg.com/htmx.org@1.6.0"
      crossorigin="anonymous"
    ></script>
  </head>

  <body>
    <h1>Hello world</h1>

    <!-- websocket connection -->
    <div hx-ws="connect:/ws">
      <!-- input for new messages, send a message to the backend
      via websocket -->
      <form hx-ws="send:submit">
        <input name="chat_message" />
      </form>
    </div>

    <!-- location for new messages from the server -->
    <div id="content"></div>

  </body>
</html>

Key points

The websocket connection is created with the hx-ws attribute. The syntax is: hx-ws="connect:<endpoint>".

<div hx-ws="connect:/ws">
</div>

Within this we include the single control that handles input from the user:

<form hx-ws="send:submit">
  <input name="chat_message" />
</form>

So far so htmx documentation. Note in the documentation, the example shows replacing an element with a new copy from the server. This is not the behaviour we want. Instead we want to add a new message to the page without removing the old ones - like a chat server.

The response from the server looks like this:

<div hx-swap-oob="beforeend:#content">
  <p>{{ time }}: {{ message }}</p>
</div>

The important part is hx-swap-oob="beforeend:#content". This means the response should update a different HTML element than the one specified. In addition, we are using beforeend:<css-selector> to select which element to update. beforeend means that the child element is appended to the last child element of the selected element. So the final HTML after adding a few messages will be:

<div id="content">
  <p>1635036095: message 1</p>
  <p>1635036109: message 2</p>
  <p>1635036117: message 3</p>
</div>

Resources

These links helped me understand this problem: