htmx icon indicating copy to clipboard operation
htmx copied to clipboard

Updating only a small nested element with `morph` over websockets leads to full update of the parent

Open renardeinside opened this issue 2 years ago • 1 comments

I'm trying to use htmx + morph in combination with FastApi to write a server-driven python app.

My template code is as follows:

<html data-theme="dark" lang="en">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Morphing</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/[email protected]"
            integrity="sha384-QFjmbokDn2DjBjq+fM+8LUIVrAgqcNW2s0PjAxHETgRn9l4fvX31ZxDxvwQnyMOX"
            crossorigin="anonymous"></script>
    <script src="https://unpkg.com/idiomorph/dist/idiomorph-ext.min.js"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
</head>
<body>
<div id="app" hx-ext="morph">
    <div class="h-50 flex flex-col justify-center items-center">
        <div hx-ext="ws" ws-connect="/events">
            <div class="card bg-base-200 outlined p-10 space-y-4">
                <p class="text-4xl">Example counter</p>
                <div class="flex flex-row space-x-4">
                    <button ws-send class="btn btn-primary" id="increment">
                        Increment
                    </button>
                    <button ws-send class="btn btn-secondary" id="decrement">
                        Decrement
                    </button>
                </div>
                <p id="view" class="text-center">
                    Clicked <span id="count"> {{ counter }}</span> times
                </p>
            </div>
        </div>
    </div>
</div>
</body>
</html>

And my app code is also quite simple:

from typing import Annotated

from fastapi import FastAPI, Depends
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from loguru import logger
from lxml import etree
from starlette.websockets import WebSocket

app = FastAPI()

templates = Jinja2Templates(directory="templates")


class Counter:
    def __init__(self):
        self._counter = 0

    def increment(self):
        self._counter += 1

    def decrement(self):
        self._counter -= 1

    @property
    def value(self):
        return self._counter


@app.get("/", response_class=HTMLResponse)
def index(counter: Annotated[Counter, Depends(Counter)]):
    return templates.get_template("index.html").render({"counter": counter.value})


@app.websocket("/events")
async def events(websocket: WebSocket, counter: Annotated[Counter, Depends(Counter)]):
    await websocket.accept()
    async for message in websocket.iter_json():
        logger.debug(f"Message received: {message}")
        trigger = message["HEADERS"]["HX-Trigger"]

        if trigger == "increment":
            counter.increment()
        elif trigger == "decrement":
            counter.decrement()

        re_render = templates.get_template("index.html").render({"counter": counter.value})
        parser = etree.HTMLParser()
        html_root = etree.fromstring(re_render, parser)
        results = html_root.xpath("//div[@id = 'app']")
        re_render = etree.tostring(results[0], encoding="utf-8").decode("utf-8")
        print(re_render)
        await websocket.send_text(re_render)

My idea is the following - whenever there is a user interaction, I'm re-rendering the whole page on the server side and sending a specific part of it back via the WebSocket (the div with id="app" attribute).

I wasa hoping that since all of the elements have an id, morph plugin would easily recognize that the only thing changing is the span and therefore refresh only it.

However, it updates the whole div id="app" element, which i can see by the animation on the buttons.

Is it possible to somehow setup the plugin to avoid calculating the partial update on the server side? Considering that the app is going to grow in size, it would be quite inconvenient to figure out the specific updates on the server side.

renardeinside avatar Dec 20 '23 23:12 renardeinside

I don't know if it is possible to avoid calculating the partial update on the server side but if you're only trying to update the span why not just send only the span tag back to the client instead of the entire div or is there something in your use case that requires you to return the entire div

Moses-Alero avatar Dec 22 '23 15:12 Moses-Alero