bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
38.19k stars 1.3k forks source link

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

Open renardeinside opened 10 months ago

renardeinside commented 10 months ago

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/daisyui@4.4.22/dist/full.min.css" rel="stylesheet" type="text/css"/>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://unpkg.com/htmx.org@1.9.9"
            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.

Moses-Alero commented 10 months ago

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