Updating only a small nested element with `morph` over websockets leads to full update of the parent
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.
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